feat: Add backend-driven page events with runtime lookup and maintenance task fixes
Summary
This MR adds backend-driven runtime events for tool pages and fixes adjacent backend issues discovered during integration.
The main goal is to let the frontend resolve page-specific review filters from
the backend using GET /api/v1/events/current and the X-Page-Route header,
instead of relying only on client-side hardcoded defaults.
In addition to the event feature, this MR also includes:
- timezone-safe cleanup logic in maintenance tasks
- a test-safe fix for
create_categories.py - unit coverage for the new event behavior and cleanup regressions
What Changed
1. Added backend event subsystem
This MR introduces a new Event model and associated API endpoints.
New event fields:
uidnamedescriptionpage_contextfilterslimitis_activestart_dateend_datecreated_bycreated_atupdated_at
The filters field stores a JSONB representation of
RecordReviewFilters. This allows the backend to persist the same review-filter
shape used by the record review endpoint.
2. Added event endpoints
New routes under /api/v1/events:
POST /GET /GET /{event_id}PUT /{event_id}DELETE /{event_id}GET /current
Behavior:
- CRUD is admin-only
-
GET /currentis runtime-facing - it reads
X-Page-Route - it matches that value against
Event.page_context - it returns the single active in-window event for that page
- returns
nullwhen no event matches - returns
409when multiple active events match the same page
3. Added filter serialization for stored event filters
RecordReviewFilters can contain enums, UUIDs, and dates, so this MR adds
explicit JSON-safe serialization before persistence.
Examples:
-
category_idsare stored as strings -
media_type,release_rights, andextraction_typeare stored as enum values -
published_dateis ISO-serialized
This ensures the event payload can be stored in JSONB and returned consistently.
4. Added and restored Alembic migration chain
This branch now uses the restored multi-step event migration chain instead of a single collapsed migration.
Included:
c4d5e6f7a8b9_merge_event_and_language_heads.pyd5e6f7a8b9c0_add_event_service_tables.py
This was restored because the earlier collapsed version would have been unsafe for environments that had already seen the older event migration history.
5. Fixed maintenance cleanup datetime handling
app/tasks/maintenance.py previously compared timezone-aware cutoff values
against naive timestamps from:
- DB rows
- file mtimes
- chunk mtimes
This caused failures in cleanup tasks. The MR normalizes those values to UTC before comparison.
6. Removed import-time dotenv side effect in category utility
app/utils/create_categories.py no longer calls dotenv.load_dotenv() at
import time. The load now happens in main(), which prevents unit-test
failures caused by import-time file access.
Why This Change
This MR moves page-level review configuration into the backend so we can:
- change review filters without redeploying the frontend
- configure page-specific review campaigns centrally
- support time-bound events
- make the frontend runtime behavior more predictable and testable
The adjacent maintenance and utility fixes were included because they were exposed while validating the event flow and CI.
Files Changed
alembic/versions/c4d5e6f7a8b9_merge_event_and_language_heads.pyalembic/versions/d5e6f7a8b9c0_add_event_service_tables.pyapp/api/v1/api.pyapp/api/v1/endpoints/events.pyapp/models/event.pyapp/models/__init__.pyapp/schemas/event.pyapp/tasks/maintenance.pyapp/utils/create_categories.pytests/unit/api/v1/endpoints/test_events.pytests/unit/tasks/test_maintenance_v2.py
Testing
Validated locally with:
uv run pytest tests/unit/api/v1/endpoints/test_events.py
uv run pytest tests/unit/tasks/test_maintenance_v2.py
Coverage added for:
- missing header ->
null - active matching event
- future event ignored
- expired event ignored
- duplicate active matches ->
409 - review-filter serialization
- naive timestamp handling in maintenance cleanup
Notes
-
GET /api/v1/events/currentalready matchesX-Page-Routedirectly againstEvent.page_context - if
start_dateandend_dateare omitted, the event has no time restriction - the current validation is structural/schema-level; business rules like "only one active event per page" are enforced at runtime, not at DB-schema level