fix: event-log route handles missing location_id

The /event-log route now shows a location selector dropdown instead of
returning a 422 error when no location_id is provided. This follows the
same pattern used by the Eggs page.

🤖 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 07:56:37 +00:00
parent 64bb99aa64
commit c8f026fb2a
3 changed files with 84 additions and 24 deletions

View File

@@ -11,6 +11,7 @@ from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
from animaltrack.repositories.locations import LocationRepository from animaltrack.repositories.locations import LocationRepository
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
@@ -56,25 +57,36 @@ 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
# Get auth info
auth = request.scope.get("auth")
username = auth.username if auth else None
user_role = auth.role if auth else None
# Get location_id from query params # Get location_id from query params
location_id = request.query_params.get("location_id") location_id = request.query_params.get("location_id")
if not location_id:
return HTMLResponse(
content="<p>Missing location_id parameter</p>",
status_code=422,
)
# Get location name # If no query param, try user defaults
if not location_id and username:
defaults = UserDefaultsRepository(db).get(username, "event_log")
if defaults:
location_id = defaults.location_id
# Get all locations for selector
location_repo = LocationRepository(db) location_repo = LocationRepository(db)
locations = location_repo.list_active() locations = location_repo.list_active()
location_name = "Unknown"
for loc in locations:
if loc.id == location_id:
location_name = loc.name
break
# Get event log # Find location name if we have a location_id
events = get_event_log(db, location_id) location_name = None
if location_id:
for loc in locations:
if loc.id == location_id:
location_name = loc.name
break
# Get event log if we have a valid location
events = []
if location_id and location_name:
events = get_event_log(db, location_id)
# Check if HTMX request # Check if HTMX request
is_htmx = request.headers.get("HX-Request") == "true" is_htmx = request.headers.get("HX-Request") == "true"
@@ -85,9 +97,11 @@ def event_log_index(request: Request):
# Full page render # Full page render
return page( return page(
event_log_panel(events, location_name), event_log_panel(events, locations, location_id),
title=f"Event Log - {location_name}", title="Event Log - AnimalTrack",
active_nav=None, active_nav="event_log",
user_role=user_role,
username=username,
) )

View File

@@ -4,7 +4,7 @@
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any from typing import Any
from fasthtml.common import H3, Div, Li, P, Span, Ul from fasthtml.common import H3, Div, Label, Li, Option, P, Select, Span, Ul
def format_timestamp(ts_utc: int) -> str: def format_timestamp(ts_utc: int) -> str:
@@ -120,11 +120,55 @@ def event_log_list(events: list[dict[str, Any]]) -> Any:
return Ul(*items, cls="divide-y divide-stone-200") return Ul(*items, cls="divide-y divide-stone-200")
def event_log_panel(events: list[dict[str, Any]], location_name: str) -> Any: def location_selector(locations: list[Any], selected_location_id: str | None) -> Any:
"""Render the full event log panel.""" """Render location selector dropdown."""
options = [Option("Select a location...", value="", selected=not selected_location_id)]
for loc in locations:
options.append(Option(loc.name, value=loc.id, selected=loc.id == selected_location_id))
return Div( return Div(
H3(f"Event Log - {location_name}", cls="text-lg font-semibold mb-4"), Label("Location", cls="text-sm font-medium text-stone-700"),
event_log_list(events), Select(
*options,
name="location_id",
cls="mt-1 block w-full rounded-md border-stone-300 shadow-sm "
"focus:border-amber-500 focus:ring-amber-500 sm:text-sm",
hx_get="/event-log",
hx_trigger="change",
hx_target="#event-log-content",
hx_swap="innerHTML",
hx_include="this",
),
cls="mb-4 max-w-xs",
)
def event_log_panel(
events: list[dict[str, Any]], locations: list[Any], selected_location_id: str | None
) -> Any:
"""Render the full event log panel."""
# Find location name for header
location_name = None
if selected_location_id:
for loc in locations:
if loc.id == selected_location_id:
location_name = loc.name
break
header_text = f"Event Log - {location_name}" if location_name else "Event Log"
return Div(
H3(header_text, cls="text-lg font-semibold mb-4"),
location_selector(locations, selected_location_id),
Div(
event_log_list(events)
if selected_location_id
else P(
"Select a location to view events.",
cls="text-stone-500 text-sm text-center py-4",
),
id="event-log-content",
),
cls="bg-white rounded-lg shadow p-4", cls="bg-white rounded-lg shadow p-4",
id="event-log", id="event-log",
) )

View File

@@ -107,10 +107,12 @@ def create_cohort(animal_service, location_id, count=3):
class TestEventLogRoute: class TestEventLogRoute:
"""Tests for GET /event-log route.""" """Tests for GET /event-log route."""
def test_event_log_requires_location_id(self, client): def test_event_log_without_location_shows_selector(self, client):
"""Event log requires location_id parameter.""" """Event log without location_id shows location selector."""
response = client.get("/event-log") response = client.get("/event-log")
assert response.status_code == 422 assert response.status_code == 200
# Should show location selector prompt
assert "Select a location" in response.text
def test_event_log_returns_empty_for_new_location(self, client, valid_location_id): def test_event_log_returns_empty_for_new_location(self, client, valid_location_id):
"""Event log returns empty state for location with no events.""" """Event log returns empty state for location with no events."""