fix: use APIRouter for proper route function resolution in forms

Form actions were rendering as function repr strings (e.g.,
"<function product_collected at 0x...>") instead of route paths
because the register_*_routes() pattern didn't attach .to() method
to handler functions.

Migrated all route modules to use FastHTML's APIRouter pattern:
- Routes decorated with @ar("/path") get .to() method attached
- Form(action=handler) now correctly resolves to route path
- Removed register_*_routes() functions in favor of router.to_app()

This is the idiomatic FastHTML pattern for multi-file route organization
per the official documentation.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-01 09:39:40 +00:00
parent 14c68187f5
commit 82def73188
12 changed files with 154 additions and 214 deletions

View File

@@ -20,16 +20,16 @@ from animaltrack.web.middleware import (
request_id_before, request_id_before,
) )
from animaltrack.web.routes import ( from animaltrack.web.routes import (
register_action_routes, actions_router,
register_animals_routes, animals_router,
register_egg_routes, eggs_router,
register_events_routes, events_router,
register_feed_routes, feed_router,
register_health_routes, health_router,
register_location_routes, locations_router,
register_move_routes, move_router,
register_product_routes, products_router,
register_registry_routes, registry_router,
) )
# Default static directory relative to this module # Default static directory relative to this module
@@ -146,16 +146,16 @@ def create_app(
app.add_exception_handler(AuthenticationError, authentication_error_handler) app.add_exception_handler(AuthenticationError, authentication_error_handler)
app.add_exception_handler(AuthorizationError, authorization_error_handler) app.add_exception_handler(AuthorizationError, authorization_error_handler)
# Register routes # Register routes using APIRouter pattern
register_health_routes(rt, app) health_router.to_app(app)
register_action_routes(rt, app) actions_router.to_app(app)
register_animals_routes(rt, app) animals_router.to_app(app)
register_egg_routes(rt, app) eggs_router.to_app(app)
register_events_routes(rt, app) events_router.to_app(app)
register_feed_routes(rt, app) feed_router.to_app(app)
register_location_routes(rt, app) locations_router.to_app(app)
register_move_routes(rt, app) move_router.to_app(app)
register_product_routes(rt, app) products_router.to_app(app)
register_registry_routes(rt, app) registry_router.to_app(app)
return app, rt return app, rt

View File

@@ -1,26 +1,26 @@
# ABOUTME: Routes package for AnimalTrack web application. # ABOUTME: Routes package for AnimalTrack web application.
# ABOUTME: Contains modular route handlers for different features. # ABOUTME: Contains modular route handlers for different features.
from animaltrack.web.routes.actions import register_action_routes from animaltrack.web.routes.actions import ar as actions_router
from animaltrack.web.routes.animals import register_animals_routes from animaltrack.web.routes.animals import ar as animals_router
from animaltrack.web.routes.eggs import register_egg_routes from animaltrack.web.routes.eggs import ar as eggs_router
from animaltrack.web.routes.events import register_events_routes from animaltrack.web.routes.events import ar as events_router
from animaltrack.web.routes.feed import register_feed_routes from animaltrack.web.routes.feed import ar as feed_router
from animaltrack.web.routes.health import register_health_routes from animaltrack.web.routes.health import ar as health_router
from animaltrack.web.routes.locations import register_location_routes from animaltrack.web.routes.locations import ar as locations_router
from animaltrack.web.routes.move import register_move_routes from animaltrack.web.routes.move import ar as move_router
from animaltrack.web.routes.products import register_product_routes from animaltrack.web.routes.products import ar as products_router
from animaltrack.web.routes.registry import register_registry_routes from animaltrack.web.routes.registry import ar as registry_router
__all__ = [ __all__ = [
"register_action_routes", "actions_router",
"register_animals_routes", "animals_router",
"register_egg_routes", "eggs_router",
"register_events_routes", "events_router",
"register_feed_routes", "feed_router",
"register_health_routes", "health_router",
"register_location_routes", "locations_router",
"register_move_routes", "move_router",
"register_product_routes", "products_router",
"register_registry_routes", "registry_router",
] ]

View File

@@ -7,7 +7,7 @@ import json
import time import time
from typing import Any from typing import Any
from fasthtml.common import to_xml from fasthtml.common import APIRouter, to_xml
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
@@ -55,6 +55,9 @@ from animaltrack.web.templates.actions import (
tag_end_form, tag_end_form,
) )
# APIRouter for multi-file route organization
ar = APIRouter()
def _create_animal_service(db: Any) -> AnimalService: def _create_animal_service(db: Any) -> AnimalService:
"""Create an AnimalService with standard projections. """Create an AnimalService with standard projections.
@@ -80,6 +83,7 @@ def _create_animal_service(db: Any) -> AnimalService:
# ============================================================================= # =============================================================================
@ar("/actions/cohort")
def cohort_index(request: Request): def cohort_index(request: Request):
"""GET /actions/cohort - Create Cohort form.""" """GET /actions/cohort - Create Cohort form."""
db = request.app.state.db db = request.app.state.db
@@ -93,6 +97,7 @@ def cohort_index(request: Request):
) )
@ar("/actions/animal-cohort", methods=["POST"])
async def animal_cohort(request: Request): async def animal_cohort(request: Request):
"""POST /actions/animal-cohort - Create a new animal cohort.""" """POST /actions/animal-cohort - Create a new animal cohort."""
db = request.app.state.db db = request.app.state.db
@@ -218,6 +223,7 @@ def _render_cohort_error(
# ============================================================================= # =============================================================================
@ar("/actions/hatch")
def hatch_index(request: Request): def hatch_index(request: Request):
"""GET /actions/hatch - Record Hatch form.""" """GET /actions/hatch - Record Hatch form."""
db = request.app.state.db db = request.app.state.db
@@ -231,6 +237,7 @@ def hatch_index(request: Request):
) )
@ar("/actions/hatch-recorded", methods=["POST"])
async def hatch_recorded(request: Request): async def hatch_recorded(request: Request):
"""POST /actions/hatch-recorded - Record a hatch event.""" """POST /actions/hatch-recorded - Record a hatch event."""
db = request.app.state.db db = request.app.state.db
@@ -352,6 +359,7 @@ def _render_hatch_error(
# ============================================================================= # =============================================================================
@ar("/actions/promote/{animal_id}")
def promote_index(request: Request, animal_id: str): def promote_index(request: Request, animal_id: str):
"""GET /actions/promote/{animal_id} - Promote Animal form.""" """GET /actions/promote/{animal_id} - Promote Animal form."""
db = request.app.state.db db = request.app.state.db
@@ -376,6 +384,7 @@ def promote_index(request: Request, animal_id: str):
) )
@ar("/actions/animal-promote", methods=["POST"])
async def animal_promote(request: Request): async def animal_promote(request: Request):
"""POST /actions/animal-promote - Promote an animal to identified.""" """POST /actions/animal-promote - Promote an animal to identified."""
db = request.app.state.db db = request.app.state.db
@@ -474,6 +483,7 @@ def _render_promote_error(
# ============================================================================= # =============================================================================
@ar("/actions/tag-add")
def tag_add_index(request: Request): def tag_add_index(request: Request):
"""GET /actions/tag-add - Add Tag form.""" """GET /actions/tag-add - Add Tag form."""
db = request.app.state.db db = request.app.state.db
@@ -507,6 +517,7 @@ def tag_add_index(request: Request):
) )
@ar("/actions/animal-tag-add", methods=["POST"])
async def animal_tag_add(request: Request): async def animal_tag_add(request: Request):
"""POST /actions/animal-tag-add - Add tag to animals.""" """POST /actions/animal-tag-add - Add tag to animals."""
db = request.app.state.db db = request.app.state.db
@@ -700,6 +711,7 @@ def _get_active_tags_for_animals(db: Any, animal_ids: list[str]) -> list[str]:
return [row[0] for row in rows] return [row[0] for row in rows]
@ar("/actions/tag-end")
def tag_end_index(request: Request): def tag_end_index(request: Request):
"""GET /actions/tag-end - End Tag form.""" """GET /actions/tag-end - End Tag form."""
db = request.app.state.db db = request.app.state.db
@@ -736,6 +748,7 @@ def tag_end_index(request: Request):
) )
@ar("/actions/animal-tag-end", methods=["POST"])
async def animal_tag_end(request: Request): async def animal_tag_end(request: Request):
"""POST /actions/animal-tag-end - End tag on animals.""" """POST /actions/animal-tag-end - End tag on animals."""
db = request.app.state.db db = request.app.state.db
@@ -904,6 +917,7 @@ def _render_tag_end_error_form(db, filter_str, error_message):
# ============================================================================= # =============================================================================
@ar("/actions/attrs")
def attrs_index(request: Request): def attrs_index(request: Request):
"""GET /actions/attrs - Update Attributes form.""" """GET /actions/attrs - Update Attributes form."""
db = request.app.state.db db = request.app.state.db
@@ -937,6 +951,7 @@ def attrs_index(request: Request):
) )
@ar("/actions/animal-attrs", methods=["POST"])
async def animal_attrs(request: Request): async def animal_attrs(request: Request):
"""POST /actions/animal-attrs - Update attributes on animals.""" """POST /actions/animal-attrs - Update attributes on animals."""
db = request.app.state.db db = request.app.state.db
@@ -1113,6 +1128,7 @@ def _render_attrs_error_form(db, filter_str, error_message):
# ============================================================================= # =============================================================================
@ar("/actions/outcome")
def outcome_index(request: Request): def outcome_index(request: Request):
"""GET /actions/outcome - Record Outcome form.""" """GET /actions/outcome - Record Outcome form."""
db = request.app.state.db db = request.app.state.db
@@ -1151,6 +1167,7 @@ def outcome_index(request: Request):
) )
@ar("/actions/animal-outcome", methods=["POST"])
async def animal_outcome(request: Request): async def animal_outcome(request: Request):
"""POST /actions/animal-outcome - Record outcome for animals.""" """POST /actions/animal-outcome - Record outcome for animals."""
db = request.app.state.db db = request.app.state.db
@@ -1384,6 +1401,7 @@ def _render_outcome_error_form(db, filter_str, error_message):
# ============================================================================= # =============================================================================
@ar("/actions/status-correct")
@require_role(UserRole.ADMIN) @require_role(UserRole.ADMIN)
async def status_correct_index(req: Request): async def status_correct_index(req: Request):
"""GET /actions/status-correct - Correct Status form (admin-only).""" """GET /actions/status-correct - Correct Status form (admin-only)."""
@@ -1418,6 +1436,7 @@ async def status_correct_index(req: Request):
) )
@ar("/actions/animal-status-correct", methods=["POST"])
@require_role(UserRole.ADMIN) @require_role(UserRole.ADMIN)
async def animal_status_correct(req: Request): async def animal_status_correct(req: Request):
"""POST /actions/animal-status-correct - Correct status of animals (admin-only).""" """POST /actions/animal-status-correct - Correct status of animals (admin-only)."""
@@ -1600,40 +1619,3 @@ def _render_status_correct_error_form(db, filter_str, error_message):
), ),
status_code=422, status_code=422,
) )
# =============================================================================
# Route Registration
# =============================================================================
def register_action_routes(rt, app):
"""Register animal action routes.
Args:
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
# Creation actions
rt("/actions/cohort")(cohort_index)
rt("/actions/animal-cohort", methods=["POST"])(animal_cohort)
rt("/actions/hatch")(hatch_index)
rt("/actions/hatch-recorded", methods=["POST"])(hatch_recorded)
# Single animal actions
rt("/actions/promote/{animal_id}")(promote_index)
rt("/actions/animal-promote", methods=["POST"])(animal_promote)
# Selection-based actions
rt("/actions/tag-add")(tag_add_index)
rt("/actions/animal-tag-add", methods=["POST"])(animal_tag_add)
rt("/actions/tag-end")(tag_end_index)
rt("/actions/animal-tag-end", methods=["POST"])(animal_tag_end)
rt("/actions/attrs")(attrs_index)
rt("/actions/animal-attrs", methods=["POST"])(animal_attrs)
rt("/actions/outcome")(outcome_index)
rt("/actions/animal-outcome", methods=["POST"])(animal_outcome)
# Admin-only actions
rt("/actions/status-correct")(status_correct_index)
rt("/actions/animal-status-correct", methods=["POST"])(animal_status_correct)

View File

@@ -1,6 +1,7 @@
# ABOUTME: Routes for individual animal detail views. # ABOUTME: Routes for individual animal detail views.
# ABOUTME: Handles GET /animals/{animal_id} for animal detail page. # ABOUTME: Handles GET /animals/{animal_id} for animal detail page.
from fasthtml.common import APIRouter
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
@@ -8,7 +9,11 @@ from animaltrack.repositories.animal_timeline import AnimalTimelineRepository
from animaltrack.web.templates.animal_detail import animal_detail_page from animaltrack.web.templates.animal_detail import animal_detail_page
from animaltrack.web.templates.base import page from animaltrack.web.templates.base import page
# APIRouter for multi-file route organization
ar = APIRouter()
@ar("/animals/{animal_id}")
def animal_detail(request: Request, animal_id: str): def animal_detail(request: Request, animal_id: str):
"""GET /animals/{animal_id} - Animal detail page with timeline.""" """GET /animals/{animal_id} - Animal detail page with timeline."""
db = request.app.state.db db = request.app.state.db
@@ -40,8 +45,3 @@ def animal_detail(request: Request, animal_id: str):
title=title, title=title,
active_nav=None, active_nav=None,
) )
def register_animals_routes(rt, app):
"""Register animal detail routes."""
rt("/animals/{animal_id}")(animal_detail)

View File

@@ -7,7 +7,7 @@ import json
import time import time
from typing import Any from typing import Any
from fasthtml.common import to_xml from fasthtml.common import APIRouter, to_xml
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
@@ -27,6 +27,9 @@ from animaltrack.services.products import ProductService, ValidationError
from animaltrack.web.templates import page from animaltrack.web.templates import page
from animaltrack.web.templates.eggs import eggs_page from animaltrack.web.templates.eggs import eggs_page
# APIRouter for multi-file route organization
ar = APIRouter()
def resolve_ducks_at_location(db: Any, location_id: str, ts_utc: int) -> list[str]: def resolve_ducks_at_location(db: Any, location_id: str, ts_utc: int) -> list[str]:
"""Resolve all duck animal IDs at a location at given timestamp. """Resolve all duck animal IDs at a location at given timestamp.
@@ -68,6 +71,7 @@ def _get_sellable_products(db):
return [p for p in all_products if p.active and p.sellable] return [p for p in all_products if p.active and p.sellable]
@ar("/")
def egg_index(request: Request): def egg_index(request: Request):
"""GET / - Eggs page with Harvest/Sell tabs.""" """GET / - Eggs page with Harvest/Sell tabs."""
db = request.app.state.db db = request.app.state.db
@@ -109,6 +113,7 @@ def egg_index(request: Request):
) )
@ar("/actions/product-collected", methods=["POST"])
async def product_collected(request: Request): async def product_collected(request: Request):
"""POST /actions/product-collected - Record egg collection.""" """POST /actions/product-collected - Record egg collection."""
db = request.app.state.db db = request.app.state.db
@@ -228,6 +233,7 @@ async def product_collected(request: Request):
return response return response
@ar("/actions/product-sold", methods=["POST"])
async def product_sold(request: Request): async def product_sold(request: Request):
"""POST /actions/product-sold - Record product sale (from Eggs page Sell tab).""" """POST /actions/product-sold - Record product sale (from Eggs page Sell tab)."""
db = request.app.state.db db = request.app.state.db
@@ -340,18 +346,6 @@ async def product_sold(request: Request):
return response 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)
rt("/actions/product-sold", methods=["POST"])(product_sold)
def _render_harvest_error(request, locations, products, selected_location_id, error_message): def _render_harvest_error(request, locations, products, selected_location_id, error_message):
"""Render harvest form with error message. """Render harvest form with error message.

View File

@@ -6,7 +6,7 @@ from __future__ import annotations
import json import json
from typing import Any from typing import Any
from fasthtml.common import to_xml from fasthtml.common import APIRouter, to_xml
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
@@ -15,6 +15,9 @@ from animaltrack.repositories.user_defaults import UserDefaultsRepository
from animaltrack.web.templates import page from animaltrack.web.templates import page
from animaltrack.web.templates.events import event_log_list, event_log_panel from animaltrack.web.templates.events import event_log_list, event_log_panel
# APIRouter for multi-file route organization
ar = APIRouter()
def get_event_log(db: Any, location_id: str, limit: int = 100) -> list[dict[str, Any]]: def get_event_log(db: Any, location_id: str, limit: int = 100) -> list[dict[str, Any]]:
"""Get event log entries for a location. """Get event log entries for a location.
@@ -53,6 +56,7 @@ def get_event_log(db: Any, location_id: str, limit: int = 100) -> list[dict[str,
return events return events
@ar("/event-log")
def event_log_index(request: Request): def event_log_index(request: Request):
"""GET /event-log - Event log for a location.""" """GET /event-log - Event log for a location."""
db = request.app.state.db db = request.app.state.db
@@ -103,13 +107,3 @@ def event_log_index(request: Request):
user_role=user_role, user_role=user_role,
username=username, username=username,
) )
def register_events_routes(rt, app) -> None:
"""Register event log routes.
Args:
rt: FastHTML route decorator.
app: FastHTML app instance (unused, for consistency).
"""
rt("/event-log")(event_log_index)

View File

@@ -7,6 +7,7 @@ import json
import time import time
from typing import Any from typing import Any
from fasthtml.common import APIRouter
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
@@ -23,6 +24,9 @@ from animaltrack.services.feed import FeedService, ValidationError
from animaltrack.web.templates import page from animaltrack.web.templates import page
from animaltrack.web.templates.feed import feed_page from animaltrack.web.templates.feed import feed_page
# APIRouter for multi-file route organization
ar = APIRouter()
def get_feed_balance(db: Any, feed_type_code: str) -> int | None: def get_feed_balance(db: Any, feed_type_code: str) -> int | None:
"""Get current feed balance for a feed type. """Get current feed balance for a feed type.
@@ -41,6 +45,7 @@ def get_feed_balance(db: Any, feed_type_code: str) -> int | None:
return row[0] if row else None return row[0] if row else None
@ar("/feed")
def feed_index(request: Request): def feed_index(request: Request):
"""GET /feed - Feed Quick Capture page.""" """GET /feed - Feed Quick Capture page."""
db = request.app.state.db db = request.app.state.db
@@ -82,6 +87,7 @@ def feed_index(request: Request):
) )
@ar("/actions/feed-given", methods=["POST"])
async def feed_given(request: Request): async def feed_given(request: Request):
"""POST /actions/feed-given - Record feed given.""" """POST /actions/feed-given - Record feed given."""
db = request.app.state.db db = request.app.state.db
@@ -240,6 +246,7 @@ async def feed_given(request: Request):
return response return response
@ar("/actions/feed-purchased", methods=["POST"])
async def feed_purchased(request: Request): async def feed_purchased(request: Request):
"""POST /actions/feed-purchased - Record feed purchase.""" """POST /actions/feed-purchased - Record feed purchase."""
db = request.app.state.db db = request.app.state.db
@@ -390,18 +397,6 @@ async def feed_purchased(request: Request):
return response 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( def _render_give_error(
locations, locations,
feed_types, feed_types,

View File

@@ -1,56 +1,54 @@
# ABOUTME: Health and metrics endpoints for AnimalTrack. # ABOUTME: Health and metrics endpoints for AnimalTrack.
# ABOUTME: Provides /healthz for liveness and /metrics for Prometheus. # ABOUTME: Provides /healthz for liveness and /metrics for Prometheus.
from fasthtml.common import APIRouter
from starlette.requests import Request
from starlette.responses import PlainTextResponse from starlette.responses import PlainTextResponse
# APIRouter for multi-file route organization
ar = APIRouter()
def register_health_routes(rt, app):
"""Register health and metrics routes.
Args: @ar("/healthz")
rt: FastHTML route decorator def healthz(request: Request):
app: FastHTML application instance """Health check endpoint - verifies database is writable."""
try:
request.app.state.db.execute("SELECT 1")
return PlainTextResponse("OK", status_code=200)
except Exception as e:
return PlainTextResponse(f"Database error: {e}", status_code=503)
@ar("/metrics")
def metrics(request: Request):
"""Prometheus metrics endpoint.
Returns metrics in Prometheus text format.
Gated by settings.metrics_enabled (default: True).
""" """
if not request.app.state.settings.metrics_enabled:
return PlainTextResponse("Not Found", status_code=404)
@rt("/healthz") # Check database health for metric
def healthz(): try:
"""Health check endpoint - verifies database is writable.""" request.app.state.db.execute("SELECT 1")
try: db_healthy = 1
app.state.db.execute("SELECT 1") except Exception:
return PlainTextResponse("OK", status_code=200) db_healthy = 0
except Exception as e:
return PlainTextResponse(f"Database error: {e}", status_code=503)
@rt("/metrics") # Build Prometheus text format response
def metrics(): lines = [
"""Prometheus metrics endpoint. "# HELP animaltrack_up Whether the service is up",
"# TYPE animaltrack_up gauge",
"animaltrack_up 1",
"",
"# HELP animaltrack_db_healthy Whether database is healthy",
"# TYPE animaltrack_db_healthy gauge",
f"animaltrack_db_healthy {db_healthy}",
"",
]
Returns metrics in Prometheus text format. return PlainTextResponse(
Gated by settings.metrics_enabled (default: True). "\n".join(lines),
""" media_type="text/plain; version=0.0.4; charset=utf-8",
if not app.state.settings.metrics_enabled: )
return PlainTextResponse("Not Found", status_code=404)
# Check database health for metric
try:
app.state.db.execute("SELECT 1")
db_healthy = 1
except Exception:
db_healthy = 0
# Build Prometheus text format response
lines = [
"# HELP animaltrack_up Whether the service is up",
"# TYPE animaltrack_up gauge",
"animaltrack_up 1",
"",
"# HELP animaltrack_db_healthy Whether database is healthy",
"# TYPE animaltrack_db_healthy gauge",
f"animaltrack_db_healthy {db_healthy}",
"",
]
return PlainTextResponse(
"\n".join(lines),
media_type="text/plain; version=0.0.4; charset=utf-8",
)

View File

@@ -6,7 +6,7 @@ from __future__ import annotations
import json import json
import time import time
from fasthtml.common import to_xml from fasthtml.common import APIRouter, to_xml
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
@@ -21,6 +21,9 @@ from animaltrack.web.responses import success_toast
from animaltrack.web.templates import page from animaltrack.web.templates import page
from animaltrack.web.templates.locations import location_list, rename_form from animaltrack.web.templates.locations import location_list, rename_form
# APIRouter for multi-file route organization
ar = APIRouter()
def _get_location_service(db) -> LocationService: def _get_location_service(db) -> LocationService:
"""Create a LocationService with projections.""" """Create a LocationService with projections."""
@@ -35,6 +38,7 @@ def _get_location_service(db) -> LocationService:
# ============================================================================= # =============================================================================
@ar("/locations")
@require_role(UserRole.ADMIN) @require_role(UserRole.ADMIN)
async def locations_index(req: Request): async def locations_index(req: Request):
"""GET /locations - Location management page (admin-only).""" """GET /locations - Location management page (admin-only)."""
@@ -53,6 +57,7 @@ async def locations_index(req: Request):
# ============================================================================= # =============================================================================
@ar("/locations/{location_id}/rename")
@require_role(UserRole.ADMIN) @require_role(UserRole.ADMIN)
async def location_rename_form(req: Request, location_id: str): async def location_rename_form(req: Request, location_id: str):
"""GET /locations/{id}/rename - Rename location form (admin-only).""" """GET /locations/{id}/rename - Rename location form (admin-only)."""
@@ -70,6 +75,7 @@ async def location_rename_form(req: Request, location_id: str):
# ============================================================================= # =============================================================================
@ar("/actions/location-created", methods=["POST"])
@require_role(UserRole.ADMIN) @require_role(UserRole.ADMIN)
async def location_created(req: Request): async def location_created(req: Request):
"""POST /actions/location-created - Create a new location (admin-only).""" """POST /actions/location-created - Create a new location (admin-only)."""
@@ -126,6 +132,7 @@ async def location_created(req: Request):
# ============================================================================= # =============================================================================
@ar("/actions/location-renamed", methods=["POST"])
@require_role(UserRole.ADMIN) @require_role(UserRole.ADMIN)
async def location_renamed(req: Request): async def location_renamed(req: Request):
"""POST /actions/location-renamed - Rename a location (admin-only).""" """POST /actions/location-renamed - Rename a location (admin-only)."""
@@ -180,6 +187,7 @@ async def location_renamed(req: Request):
# ============================================================================= # =============================================================================
@ar("/actions/location-archived", methods=["POST"])
@require_role(UserRole.ADMIN) @require_role(UserRole.ADMIN)
async def location_archived(req: Request): async def location_archived(req: Request):
"""POST /actions/location-archived - Archive a location (admin-only).""" """POST /actions/location-archived - Archive a location (admin-only)."""
@@ -237,17 +245,3 @@ def _render_error_list(db, error_message: str) -> HTMLResponse:
content=to_xml(location_list(locations, error=error_message)), content=to_xml(location_list(locations, error=error_message)),
status_code=422, status_code=422,
) )
def register_location_routes(rt, app):
"""Register location management routes.
Args:
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
rt("/locations")(locations_index)
rt("/locations/{location_id}/rename")(location_rename_form)
rt("/actions/location-created", methods=["POST"])(location_created)
rt("/actions/location-renamed", methods=["POST"])(location_renamed)
rt("/actions/location-archived", methods=["POST"])(location_archived)

View File

@@ -7,7 +7,7 @@ import json
import time import time
from typing import Any from typing import Any
from fasthtml.common import to_xml from fasthtml.common import APIRouter, to_xml
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
@@ -24,6 +24,9 @@ from animaltrack.services.animal import AnimalService, ValidationError
from animaltrack.web.templates import page from animaltrack.web.templates import page
from animaltrack.web.templates.move import diff_panel, move_form from animaltrack.web.templates.move import diff_panel, move_form
# APIRouter for multi-file route organization
ar = APIRouter()
def _get_from_location( def _get_from_location(
db: Any, animal_ids: list[str], ts_utc: int db: Any, animal_ids: list[str], ts_utc: int
@@ -62,6 +65,7 @@ def _get_from_location(
return rows[0][0], rows[0][1] return rows[0][0], rows[0][1]
@ar("/move")
def move_index(request: Request): def move_index(request: Request):
"""GET /move - Move Animals form.""" """GET /move - Move Animals form."""
db = request.app.state.db db = request.app.state.db
@@ -104,6 +108,7 @@ def move_index(request: Request):
) )
@ar("/actions/animal-move", methods=["POST"])
async def animal_move(request: Request): async def animal_move(request: Request):
"""POST /actions/animal-move - Move animals to new location.""" """POST /actions/animal-move - Move animals to new location."""
db = request.app.state.db db = request.app.state.db
@@ -265,17 +270,6 @@ async def animal_move(request: Request):
return response return response
def register_move_routes(rt, app):
"""Register move routes.
Args:
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
rt("/move")(move_index)
rt("/actions/animal-move", methods=["POST"])(animal_move)
def _render_error_form(db, locations, filter_str, error_message): def _render_error_form(db, locations, filter_str, error_message):
"""Render form with error message. """Render form with error message.

View File

@@ -6,7 +6,7 @@ from __future__ import annotations
import json import json
import time import time
from fasthtml.common import to_xml from fasthtml.common import APIRouter, to_xml
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
@@ -19,6 +19,9 @@ from animaltrack.services.products import ProductService, ValidationError
from animaltrack.web.templates import page from animaltrack.web.templates import page
from animaltrack.web.templates.products import product_sold_form from animaltrack.web.templates.products import product_sold_form
# APIRouter for multi-file route organization
ar = APIRouter()
def _get_sellable_products(db): def _get_sellable_products(db):
"""Get list of active, sellable products. """Get list of active, sellable products.
@@ -34,6 +37,7 @@ def _get_sellable_products(db):
return [p for p in all_products if p.active and p.sellable] return [p for p in all_products if p.active and p.sellable]
@ar("/sell")
def sell_index(request: Request): def sell_index(request: Request):
"""GET /sell - Redirect to Eggs page Sell tab.""" """GET /sell - Redirect to Eggs page Sell tab."""
from starlette.responses import RedirectResponse from starlette.responses import RedirectResponse
@@ -47,6 +51,7 @@ def sell_index(request: Request):
return RedirectResponse(url=redirect_url, status_code=302) return RedirectResponse(url=redirect_url, status_code=302)
@ar("/actions/product-sold", methods=["POST"])
async def product_sold(request: Request): async def product_sold(request: Request):
"""POST /actions/product-sold - Record product sale.""" """POST /actions/product-sold - Record product sale."""
db = request.app.state.db db = request.app.state.db
@@ -142,17 +147,6 @@ async def product_sold(request: Request):
return response return response
def register_product_routes(rt, app):
"""Register product routes.
Args:
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
rt("/sell")(sell_index)
rt("/actions/product-sold", methods=["POST"])(product_sold)
def _render_error_form(products, selected_product_code, error_message): def _render_error_form(products, selected_product_code, error_message):
"""Render form with error message. """Render form with error message.

View File

@@ -1,6 +1,7 @@
# ABOUTME: Routes for Animal Registry view. # ABOUTME: Routes for Animal Registry view.
# ABOUTME: Handles GET /registry with filters, pagination, and facets. # ABOUTME: Handles GET /registry with filters, pagination, and facets.
from fasthtml.common import APIRouter
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
@@ -13,7 +14,11 @@ from animaltrack.web.templates.registry import (
registry_page, registry_page,
) )
# APIRouter for multi-file route organization
ar = APIRouter()
@ar("/registry")
def registry_index(request: Request): def registry_index(request: Request):
"""GET /registry - Animal Registry with filtering and pagination. """GET /registry - Animal Registry with filtering and pagination.
@@ -64,13 +69,3 @@ def registry_index(request: Request):
title="Registry - AnimalTrack", title="Registry - AnimalTrack",
active_nav=None, active_nav=None,
) )
def register_registry_routes(rt, app):
"""Register registry routes.
Args:
rt: FastHTML route decorator.
app: FastHTML application instance.
"""
rt("/registry")(registry_index)