From 82def73188944b31d8918faba33903f903224be6 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Thu, 1 Jan 2026 09:39:40 +0000 Subject: [PATCH] fix: use APIRouter for proper route function resolution in forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Form actions were rendering as function repr strings (e.g., "") 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 --- src/animaltrack/web/app.py | 42 ++++++------ src/animaltrack/web/routes/__init__.py | 40 +++++------ src/animaltrack/web/routes/actions.py | 58 ++++++---------- src/animaltrack/web/routes/animals.py | 10 +-- src/animaltrack/web/routes/eggs.py | 20 ++---- src/animaltrack/web/routes/events.py | 16 ++--- src/animaltrack/web/routes/feed.py | 19 ++---- src/animaltrack/web/routes/health.py | 88 ++++++++++++------------- src/animaltrack/web/routes/locations.py | 24 +++---- src/animaltrack/web/routes/move.py | 18 ++--- src/animaltrack/web/routes/products.py | 18 ++--- src/animaltrack/web/routes/registry.py | 15 ++--- 12 files changed, 154 insertions(+), 214 deletions(-) diff --git a/src/animaltrack/web/app.py b/src/animaltrack/web/app.py index 9726c56..cdeaad2 100644 --- a/src/animaltrack/web/app.py +++ b/src/animaltrack/web/app.py @@ -20,16 +20,16 @@ from animaltrack.web.middleware import ( request_id_before, ) from animaltrack.web.routes import ( - register_action_routes, - register_animals_routes, - register_egg_routes, - register_events_routes, - register_feed_routes, - register_health_routes, - register_location_routes, - register_move_routes, - register_product_routes, - register_registry_routes, + actions_router, + animals_router, + eggs_router, + events_router, + feed_router, + health_router, + locations_router, + move_router, + products_router, + registry_router, ) # 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(AuthorizationError, authorization_error_handler) - # Register routes - register_health_routes(rt, app) - register_action_routes(rt, app) - register_animals_routes(rt, app) - register_egg_routes(rt, app) - register_events_routes(rt, app) - register_feed_routes(rt, app) - register_location_routes(rt, app) - register_move_routes(rt, app) - register_product_routes(rt, app) - register_registry_routes(rt, app) + # Register routes using APIRouter pattern + health_router.to_app(app) + actions_router.to_app(app) + animals_router.to_app(app) + eggs_router.to_app(app) + events_router.to_app(app) + feed_router.to_app(app) + locations_router.to_app(app) + move_router.to_app(app) + products_router.to_app(app) + registry_router.to_app(app) return app, rt diff --git a/src/animaltrack/web/routes/__init__.py b/src/animaltrack/web/routes/__init__.py index 66d8415..e2e703f 100644 --- a/src/animaltrack/web/routes/__init__.py +++ b/src/animaltrack/web/routes/__init__.py @@ -1,26 +1,26 @@ # ABOUTME: Routes package for AnimalTrack web application. # ABOUTME: Contains modular route handlers for different features. -from animaltrack.web.routes.actions import register_action_routes -from animaltrack.web.routes.animals import register_animals_routes -from animaltrack.web.routes.eggs import register_egg_routes -from animaltrack.web.routes.events import register_events_routes -from animaltrack.web.routes.feed import register_feed_routes -from animaltrack.web.routes.health import register_health_routes -from animaltrack.web.routes.locations import register_location_routes -from animaltrack.web.routes.move import register_move_routes -from animaltrack.web.routes.products import register_product_routes -from animaltrack.web.routes.registry import register_registry_routes +from animaltrack.web.routes.actions import ar as actions_router +from animaltrack.web.routes.animals import ar as animals_router +from animaltrack.web.routes.eggs import ar as eggs_router +from animaltrack.web.routes.events import ar as events_router +from animaltrack.web.routes.feed import ar as feed_router +from animaltrack.web.routes.health import ar as health_router +from animaltrack.web.routes.locations import ar as locations_router +from animaltrack.web.routes.move import ar as move_router +from animaltrack.web.routes.products import ar as products_router +from animaltrack.web.routes.registry import ar as registry_router __all__ = [ - "register_action_routes", - "register_animals_routes", - "register_egg_routes", - "register_events_routes", - "register_feed_routes", - "register_health_routes", - "register_location_routes", - "register_move_routes", - "register_product_routes", - "register_registry_routes", + "actions_router", + "animals_router", + "eggs_router", + "events_router", + "feed_router", + "health_router", + "locations_router", + "move_router", + "products_router", + "registry_router", ] diff --git a/src/animaltrack/web/routes/actions.py b/src/animaltrack/web/routes/actions.py index 3f22b73..8e5d6d8 100644 --- a/src/animaltrack/web/routes/actions.py +++ b/src/animaltrack/web/routes/actions.py @@ -7,7 +7,7 @@ import json import time from typing import Any -from fasthtml.common import to_xml +from fasthtml.common import APIRouter, to_xml from starlette.requests import Request from starlette.responses import HTMLResponse @@ -55,6 +55,9 @@ from animaltrack.web.templates.actions import ( tag_end_form, ) +# APIRouter for multi-file route organization +ar = APIRouter() + def _create_animal_service(db: Any) -> AnimalService: """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): """GET /actions/cohort - Create Cohort form.""" 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): """POST /actions/animal-cohort - Create a new animal cohort.""" db = request.app.state.db @@ -218,6 +223,7 @@ def _render_cohort_error( # ============================================================================= +@ar("/actions/hatch") def hatch_index(request: Request): """GET /actions/hatch - Record Hatch form.""" 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): """POST /actions/hatch-recorded - Record a hatch event.""" 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): """GET /actions/promote/{animal_id} - Promote Animal form.""" 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): """POST /actions/animal-promote - Promote an animal to identified.""" db = request.app.state.db @@ -474,6 +483,7 @@ def _render_promote_error( # ============================================================================= +@ar("/actions/tag-add") def tag_add_index(request: Request): """GET /actions/tag-add - Add Tag form.""" 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): """POST /actions/animal-tag-add - Add tag to animals.""" 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] +@ar("/actions/tag-end") def tag_end_index(request: Request): """GET /actions/tag-end - End Tag form.""" 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): """POST /actions/animal-tag-end - End tag on animals.""" 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): """GET /actions/attrs - Update Attributes form.""" 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): """POST /actions/animal-attrs - Update attributes on animals.""" 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): """GET /actions/outcome - Record Outcome form.""" 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): """POST /actions/animal-outcome - Record outcome for animals.""" 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) async def status_correct_index(req: Request): """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) async def animal_status_correct(req: Request): """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, ) - - -# ============================================================================= -# 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) diff --git a/src/animaltrack/web/routes/animals.py b/src/animaltrack/web/routes/animals.py index c8bd45d..1dfcba4 100644 --- a/src/animaltrack/web/routes/animals.py +++ b/src/animaltrack/web/routes/animals.py @@ -1,6 +1,7 @@ # ABOUTME: Routes for individual animal detail views. # ABOUTME: Handles GET /animals/{animal_id} for animal detail page. +from fasthtml.common import APIRouter from starlette.requests import Request 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.base import page +# APIRouter for multi-file route organization +ar = APIRouter() + +@ar("/animals/{animal_id}") def animal_detail(request: Request, animal_id: str): """GET /animals/{animal_id} - Animal detail page with timeline.""" db = request.app.state.db @@ -40,8 +45,3 @@ def animal_detail(request: Request, animal_id: str): title=title, active_nav=None, ) - - -def register_animals_routes(rt, app): - """Register animal detail routes.""" - rt("/animals/{animal_id}")(animal_detail) diff --git a/src/animaltrack/web/routes/eggs.py b/src/animaltrack/web/routes/eggs.py index d0cac91..6aea78e 100644 --- a/src/animaltrack/web/routes/eggs.py +++ b/src/animaltrack/web/routes/eggs.py @@ -7,7 +7,7 @@ import json import time from typing import Any -from fasthtml.common import to_xml +from fasthtml.common import APIRouter, to_xml from starlette.requests import Request 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.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]: """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] +@ar("/") def egg_index(request: Request): """GET / - Eggs page with Harvest/Sell tabs.""" 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): """POST /actions/product-collected - Record egg collection.""" db = request.app.state.db @@ -228,6 +233,7 @@ async def product_collected(request: Request): return response +@ar("/actions/product-sold", methods=["POST"]) async def product_sold(request: Request): """POST /actions/product-sold - Record product sale (from Eggs page Sell tab).""" db = request.app.state.db @@ -340,18 +346,6 @@ async def product_sold(request: Request): 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): """Render harvest form with error message. diff --git a/src/animaltrack/web/routes/events.py b/src/animaltrack/web/routes/events.py index 2f7f2da..ad91a00 100644 --- a/src/animaltrack/web/routes/events.py +++ b/src/animaltrack/web/routes/events.py @@ -6,7 +6,7 @@ from __future__ import annotations import json from typing import Any -from fasthtml.common import to_xml +from fasthtml.common import APIRouter, to_xml from starlette.requests import Request 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.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]]: """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 +@ar("/event-log") def event_log_index(request: Request): """GET /event-log - Event log for a location.""" db = request.app.state.db @@ -103,13 +107,3 @@ def event_log_index(request: Request): user_role=user_role, 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) diff --git a/src/animaltrack/web/routes/feed.py b/src/animaltrack/web/routes/feed.py index d1f7fa3..1e20462 100644 --- a/src/animaltrack/web/routes/feed.py +++ b/src/animaltrack/web/routes/feed.py @@ -7,6 +7,7 @@ import json import time from typing import Any +from fasthtml.common import APIRouter from starlette.requests import Request 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.feed import feed_page +# APIRouter for multi-file route organization +ar = APIRouter() + def get_feed_balance(db: Any, feed_type_code: str) -> int | None: """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 +@ar("/feed") def feed_index(request: Request): """GET /feed - Feed Quick Capture page.""" 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): """POST /actions/feed-given - Record feed given.""" db = request.app.state.db @@ -240,6 +246,7 @@ async def feed_given(request: Request): return response +@ar("/actions/feed-purchased", methods=["POST"]) async def feed_purchased(request: Request): """POST /actions/feed-purchased - Record feed purchase.""" db = request.app.state.db @@ -390,18 +397,6 @@ async def feed_purchased(request: Request): 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, diff --git a/src/animaltrack/web/routes/health.py b/src/animaltrack/web/routes/health.py index fab38bf..52847dd 100644 --- a/src/animaltrack/web/routes/health.py +++ b/src/animaltrack/web/routes/health.py @@ -1,56 +1,54 @@ # ABOUTME: Health and metrics endpoints for AnimalTrack. # ABOUTME: Provides /healthz for liveness and /metrics for Prometheus. +from fasthtml.common import APIRouter +from starlette.requests import Request 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: - rt: FastHTML route decorator - app: FastHTML application instance +@ar("/healthz") +def healthz(request: Request): + """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") - def healthz(): - """Health check endpoint - verifies database is writable.""" - try: - 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) + # Check database health for metric + try: + request.app.state.db.execute("SELECT 1") + db_healthy = 1 + except Exception: + db_healthy = 0 - @rt("/metrics") - def metrics(): - """Prometheus metrics endpoint. + # 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}", + "", + ] - Returns metrics in Prometheus text format. - Gated by settings.metrics_enabled (default: True). - """ - 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", - ) + return PlainTextResponse( + "\n".join(lines), + media_type="text/plain; version=0.0.4; charset=utf-8", + ) diff --git a/src/animaltrack/web/routes/locations.py b/src/animaltrack/web/routes/locations.py index 8889d47..0acdc31 100644 --- a/src/animaltrack/web/routes/locations.py +++ b/src/animaltrack/web/routes/locations.py @@ -6,7 +6,7 @@ from __future__ import annotations import json import time -from fasthtml.common import to_xml +from fasthtml.common import APIRouter, to_xml from starlette.requests import Request 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.locations import location_list, rename_form +# APIRouter for multi-file route organization +ar = APIRouter() + def _get_location_service(db) -> LocationService: """Create a LocationService with projections.""" @@ -35,6 +38,7 @@ def _get_location_service(db) -> LocationService: # ============================================================================= +@ar("/locations") @require_role(UserRole.ADMIN) async def locations_index(req: Request): """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) async def location_rename_form(req: Request, location_id: str): """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) async def location_created(req: Request): """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) async def location_renamed(req: Request): """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) async def location_archived(req: Request): """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)), 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) diff --git a/src/animaltrack/web/routes/move.py b/src/animaltrack/web/routes/move.py index 8d72276..406c56f 100644 --- a/src/animaltrack/web/routes/move.py +++ b/src/animaltrack/web/routes/move.py @@ -7,7 +7,7 @@ import json import time from typing import Any -from fasthtml.common import to_xml +from fasthtml.common import APIRouter, to_xml from starlette.requests import Request 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.move import diff_panel, move_form +# APIRouter for multi-file route organization +ar = APIRouter() + def _get_from_location( db: Any, animal_ids: list[str], ts_utc: int @@ -62,6 +65,7 @@ def _get_from_location( return rows[0][0], rows[0][1] +@ar("/move") def move_index(request: Request): """GET /move - Move Animals form.""" 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): """POST /actions/animal-move - Move animals to new location.""" db = request.app.state.db @@ -265,17 +270,6 @@ async def animal_move(request: Request): 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): """Render form with error message. diff --git a/src/animaltrack/web/routes/products.py b/src/animaltrack/web/routes/products.py index c2b6941..487a62a 100644 --- a/src/animaltrack/web/routes/products.py +++ b/src/animaltrack/web/routes/products.py @@ -6,7 +6,7 @@ from __future__ import annotations import json import time -from fasthtml.common import to_xml +from fasthtml.common import APIRouter, to_xml from starlette.requests import Request 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.products import product_sold_form +# APIRouter for multi-file route organization +ar = APIRouter() + def _get_sellable_products(db): """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] +@ar("/sell") def sell_index(request: Request): """GET /sell - Redirect to Eggs page Sell tab.""" from starlette.responses import RedirectResponse @@ -47,6 +51,7 @@ def sell_index(request: Request): return RedirectResponse(url=redirect_url, status_code=302) +@ar("/actions/product-sold", methods=["POST"]) async def product_sold(request: Request): """POST /actions/product-sold - Record product sale.""" db = request.app.state.db @@ -142,17 +147,6 @@ async def product_sold(request: Request): 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): """Render form with error message. diff --git a/src/animaltrack/web/routes/registry.py b/src/animaltrack/web/routes/registry.py index 4b1d568..e673f62 100644 --- a/src/animaltrack/web/routes/registry.py +++ b/src/animaltrack/web/routes/registry.py @@ -1,6 +1,7 @@ # ABOUTME: Routes for Animal Registry view. # ABOUTME: Handles GET /registry with filters, pagination, and facets. +from fasthtml.common import APIRouter from starlette.requests import Request from starlette.responses import HTMLResponse @@ -13,7 +14,11 @@ from animaltrack.web.templates.registry import ( registry_page, ) +# APIRouter for multi-file route organization +ar = APIRouter() + +@ar("/registry") def registry_index(request: Request): """GET /registry - Animal Registry with filtering and pagination. @@ -64,13 +69,3 @@ def registry_index(request: Request): title="Registry - AnimalTrack", 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)