refactor: move route handlers to module level for idiomatic FastHTML

- Routes are now at module level, accessible for import by templates
- Templates accept action parameter (route function or URL string)
- Routes pass themselves to templates for type-safe form actions
- Changes DB access pattern from app.state.db to request.app.state.db
- Registration uses rt(...)(func) pattern instead of @rt decorator

This enables the idiomatic FastHTML pattern where forms can use
action=route_function instead of action="/path/string", providing
type safety and refactoring support.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 11:11:08 +00:00
parent 600d5003ed
commit b1bfdfb05c
4 changed files with 454 additions and 421 deletions

View File

@@ -49,33 +49,24 @@ def resolve_ducks_at_location(db: Any, location_id: str, ts_utc: int) -> list[st
return [row[0] for row in rows]
def register_egg_routes(rt, app):
"""Register egg capture routes.
Args:
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
@rt("/")
def index(request: Request):
def egg_index(request: Request):
"""GET / - Egg Quick Capture form."""
db = app.state.db
db = request.app.state.db
locations = LocationRepository(db).list_active()
# Check for pre-selected location from query params
selected_location_id = request.query_params.get("location_id")
return page(
egg_form(locations, selected_location_id=selected_location_id),
egg_form(locations, selected_location_id=selected_location_id, action=product_collected),
title="Egg - AnimalTrack",
active_nav="egg",
)
@rt("/actions/product-collected", methods=["POST"])
async def product_collected(request: Request):
async def product_collected(request: Request):
"""POST /actions/product-collected - Record egg collection."""
db = app.state.db
db = request.app.state.db
form = await request.form()
# Extract form data
@@ -148,7 +139,7 @@ def register_egg_routes(rt, app):
response = HTMLResponse(
content=str(
page(
egg_form(locations, selected_location_id=location_id),
egg_form(locations, selected_location_id=location_id, action=product_collected),
title="Egg - AnimalTrack",
active_nav="egg",
)
@@ -163,6 +154,17 @@ def register_egg_routes(rt, app):
return response
def register_egg_routes(rt, app):
"""Register egg capture routes.
Args:
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
rt("/")(egg_index)
rt("/actions/product-collected", methods=["POST"])(product_collected)
def _render_error_form(locations, selected_location_id, error_message):
"""Render form with error message.
@@ -181,6 +183,7 @@ def _render_error_form(locations, selected_location_id, error_message):
locations,
selected_location_id=selected_location_id,
error=error_message,
action=product_collected,
),
title="Egg - AnimalTrack",
active_nav="egg",

View File

@@ -38,18 +38,9 @@ def get_feed_balance(db: Any, feed_type_code: str) -> int | None:
return row[0] if row else None
def register_feed_routes(rt, app):
"""Register feed capture routes.
Args:
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
@rt("/feed")
def feed_index(request: Request):
def feed_index(request: Request):
"""GET /feed - Feed Quick Capture page."""
db = app.state.db
db = request.app.state.db
locations = LocationRepository(db).list_active()
feed_types = FeedTypeRepository(db).list_active()
@@ -63,15 +54,17 @@ def register_feed_routes(rt, app):
locations,
feed_types,
active_tab=active_tab,
give_action=feed_given,
purchase_action=feed_purchased,
),
title="Feed - AnimalTrack",
active_nav="feed",
)
@rt("/actions/feed-given", methods=["POST"])
async def feed_given(request: Request):
async def feed_given(request: Request):
"""POST /actions/feed-given - Record feed given."""
db = app.state.db
db = request.app.state.db
form = await request.form()
# Extract form data
@@ -191,6 +184,8 @@ def register_feed_routes(rt, app):
selected_feed_type_code=feed_type_code,
default_amount_kg=default_amount_kg,
balance_warning=balance_warning,
give_action=feed_given,
purchase_action=feed_purchased,
),
title="Feed - AnimalTrack",
active_nav="feed",
@@ -210,10 +205,10 @@ def register_feed_routes(rt, app):
return response
@rt("/actions/feed-purchased", methods=["POST"])
async def feed_purchased(request: Request):
async def feed_purchased(request: Request):
"""POST /actions/feed-purchased - Record feed purchase."""
db = app.state.db
db = request.app.state.db
form = await request.form()
# Extract form data
@@ -339,6 +334,8 @@ def register_feed_routes(rt, app):
locations,
feed_types,
active_tab="purchase",
give_action=feed_given,
purchase_action=feed_purchased,
),
title="Feed - AnimalTrack",
active_nav="feed",
@@ -359,6 +356,18 @@ def register_feed_routes(rt, app):
return response
def register_feed_routes(rt, app):
"""Register feed capture routes.
Args:
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
rt("/feed")(feed_index)
rt("/actions/feed-given", methods=["POST"])(feed_given)
rt("/actions/feed-purchased", methods=["POST"])(feed_purchased)
def _render_give_error(
locations,
feed_types,
@@ -388,6 +397,8 @@ def _render_give_error(
selected_location_id=selected_location_id,
selected_feed_type_code=selected_feed_type_code,
give_error=error_message,
give_action=feed_given,
purchase_action=feed_purchased,
),
title="Feed - AnimalTrack",
active_nav="feed",
@@ -416,6 +427,8 @@ def _render_purchase_error(locations, feed_types, error_message):
feed_types,
active_tab="purchase",
purchase_error=error_message,
give_action=feed_given,
purchase_action=feed_purchased,
),
title="Feed - AnimalTrack",
active_nav="feed",

View File

@@ -1,6 +1,9 @@
# ABOUTME: Templates for Egg Quick Capture form.
# ABOUTME: Provides form components for recording egg collections.
from collections.abc import Callable
from typing import Any
from fasthtml.common import H2, Form, Hidden, Option
from monsterui.all import Button, ButtonT, LabelInput, LabelSelect, LabelTextArea
from ulid import ULID
@@ -12,6 +15,7 @@ def egg_form(
locations: list[Location],
selected_location_id: str | None = None,
error: str | None = None,
action: Callable[..., Any] | str = "/actions/product-collected",
) -> Form:
"""Create the Egg Quick Capture form.
@@ -19,6 +23,7 @@ def egg_form(
locations: List of active locations for the dropdown.
selected_location_id: Pre-selected location ID (sticks after submission).
error: Optional error message to display.
action: Route function or URL string for form submission.
Returns:
Form component for egg collection.
@@ -82,7 +87,7 @@ def egg_form(
# Submit button
Button("Record Eggs", type="submit", cls=ButtonT.primary),
# Form submission via standard action/method (hx-boost handles AJAX)
action="/actions/product-collected",
action=action,
method="post",
cls="space-y-4",
)

View File

@@ -1,6 +1,9 @@
# ABOUTME: Templates for Feed Quick Capture forms.
# ABOUTME: Provides form components for recording feed given and purchases.
from collections.abc import Callable
from typing import Any
from fasthtml.common import H1, H2, A, Div, Form, Hidden, Li, Option, P, Ul
from monsterui.all import (
Button,
@@ -25,6 +28,8 @@ def feed_page(
give_error: str | None = None,
purchase_error: str | None = None,
balance_warning: str | None = None,
give_action: Callable[..., Any] | str = "/actions/feed-given",
purchase_action: Callable[..., Any] | str = "/actions/feed-purchased",
):
"""Create the Feed Quick Capture page with tabbed forms.
@@ -38,6 +43,8 @@ def feed_page(
give_error: Error message for give form.
purchase_error: Error message for purchase form.
balance_warning: Warning about negative inventory balance.
give_action: Route function or URL for give feed form.
purchase_action: Route function or URL for purchase feed form.
Returns:
Page content with tabbed forms.
@@ -76,11 +83,12 @@ def feed_page(
default_amount_kg=default_amount_kg,
error=give_error,
balance_warning=balance_warning,
action=give_action,
),
cls="uk-active" if give_active else "",
),
Li(
purchase_feed_form(feed_types, error=purchase_error),
purchase_feed_form(feed_types, error=purchase_error, action=purchase_action),
cls="" if give_active else "uk-active",
),
),
@@ -96,6 +104,7 @@ def give_feed_form(
default_amount_kg: int | None = None,
error: str | None = None,
balance_warning: str | None = None,
action: Callable[..., Any] | str = "/actions/feed-given",
) -> Form:
"""Create the Give Feed form.
@@ -107,6 +116,7 @@ def give_feed_form(
default_amount_kg: Default value for amount field.
error: Error message to display.
balance_warning: Warning about negative balance.
action: Route function or URL for form submission.
Returns:
Form component for giving feed.
@@ -196,7 +206,7 @@ def give_feed_form(
Hidden(name="nonce", value=str(ULID())),
# Submit button
Button("Record Feed Given", type="submit", cls=ButtonT.primary),
action="/actions/feed-given",
action=action,
method="post",
cls="space-y-4",
)
@@ -205,12 +215,14 @@ def give_feed_form(
def purchase_feed_form(
feed_types: list[FeedType],
error: str | None = None,
action: Callable[..., Any] | str = "/actions/feed-purchased",
) -> Form:
"""Create the Purchase Feed form.
Args:
feed_types: List of active feed types.
error: Error message to display.
action: Route function or URL for form submission.
Returns:
Form component for purchasing feed.
@@ -290,7 +302,7 @@ def purchase_feed_form(
Hidden(name="nonce", value=str(ULID())),
# Submit button
Button("Record Purchase", type="submit", cls=ButtonT.primary),
action="/actions/feed-purchased",
action=action,
method="post",
cls="space-y-4",
)