From c8f026fb2ac5658fe7d1566f21743f28861b90dc Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Thu, 1 Jan 2026 07:56:37 +0000 Subject: [PATCH] fix: event-log route handles missing location_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/animaltrack/web/routes/events.py | 46 +++++++++++++-------- src/animaltrack/web/templates/events.py | 54 ++++++++++++++++++++++--- tests/test_web_events.py | 8 ++-- 3 files changed, 84 insertions(+), 24 deletions(-) diff --git a/src/animaltrack/web/routes/events.py b/src/animaltrack/web/routes/events.py index 5568087..2f7f2da 100644 --- a/src/animaltrack/web/routes/events.py +++ b/src/animaltrack/web/routes/events.py @@ -11,6 +11,7 @@ from starlette.requests import Request from starlette.responses import HTMLResponse from animaltrack.repositories.locations import LocationRepository +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 @@ -56,25 +57,36 @@ def event_log_index(request: Request): """GET /event-log - Event log for a location.""" 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 location_id = request.query_params.get("location_id") - if not location_id: - return HTMLResponse( - content="

Missing location_id parameter

", - 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) 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 - events = get_event_log(db, location_id) + # Find location name if we have a 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 is_htmx = request.headers.get("HX-Request") == "true" @@ -85,9 +97,11 @@ def event_log_index(request: Request): # Full page render return page( - event_log_panel(events, location_name), - title=f"Event Log - {location_name}", - active_nav=None, + event_log_panel(events, locations, location_id), + title="Event Log - AnimalTrack", + active_nav="event_log", + user_role=user_role, + username=username, ) diff --git a/src/animaltrack/web/templates/events.py b/src/animaltrack/web/templates/events.py index 19dda01..e4211fc 100644 --- a/src/animaltrack/web/templates/events.py +++ b/src/animaltrack/web/templates/events.py @@ -4,7 +4,7 @@ from datetime import UTC, datetime 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: @@ -120,11 +120,55 @@ def event_log_list(events: list[dict[str, Any]]) -> Any: return Ul(*items, cls="divide-y divide-stone-200") -def event_log_panel(events: list[dict[str, Any]], location_name: str) -> Any: - """Render the full event log panel.""" +def location_selector(locations: list[Any], selected_location_id: str | None) -> Any: + """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( - H3(f"Event Log - {location_name}", cls="text-lg font-semibold mb-4"), - event_log_list(events), + Label("Location", cls="text-sm font-medium text-stone-700"), + 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", id="event-log", ) diff --git a/tests/test_web_events.py b/tests/test_web_events.py index a5b7424..0596759 100644 --- a/tests/test_web_events.py +++ b/tests/test_web_events.py @@ -107,10 +107,12 @@ def create_cohort(animal_service, location_id, count=3): class TestEventLogRoute: """Tests for GET /event-log route.""" - def test_event_log_requires_location_id(self, client): - """Event log requires location_id parameter.""" + def test_event_log_without_location_shows_selector(self, client): + """Event log without location_id shows location selector.""" 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): """Event log returns empty state for location with no events."""