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:
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user