Surface every live-interpretation event as a browsable archive in the UI, backed by a single B2 prefix per event. Two pages:
/events— grid ofEventCards; sample-specific Library equivalent./events/[id]— single-event detail view: source audio playback, per-language transcripts / captions, artifact listing.
GET /events?limit=<n>— paged enumeration of eventsGET /events/{id}— manifest + artifact listing for one eventDELETE /events/{id}— cascade-delete every artifact underevents/<id>/GET /events/{id}/source-audio— inline presigned URL for the source recordingGET /events/{id}/transcript[?lang=<bcp47>]— attachment presigned URL forsource-transcript.txt(nolang) or<lang>/transcript.txtGET /events/{id}/captions?lang=<bcp47>&fmt=vtt|srt— attachment presigned URL for captions
JSON shapes mirror the Pydantic models in services/api/app/types/events.py:
Event— manifest + artifact listEventArtifact— single B2 object (kind / lang / size / content type / created_at)EventStatus—scheduled|live|ended
events/<event-id>/event.json — manifest (status, languages, timestamps)
events/<event-id>/source.<wav|ogg|opus> — source audio (always archived)
events/<event-id>/source-transcript.txt
events/<event-id>/source-transcript.vtt
events/<event-id>/<bcp47>/transcript.txt
events/<event-id>/<bcp47>/captions.vtt
events/<event-id>/<bcp47>/captions.srt
events/<event-id>/<bcp47>/audio.<wav|opus> — optional, per-language opt-in (default off)
- Frontend
/eventspage callsuseEvents()->getEvents()->GET /events. - Backend service enumerates
events/common prefixes viaListObjectsV2(Delimiter='/'). - For each event id, HEADs
event.jsonin parallel viahead_event_artifacts_parallel. - Reads each manifest, hydrates an
Event, sorts bycreated_atdesc, slices tolimit.
For the detail page:
useEvent(id)->getEvent(id)->GET /events/{id}.- Service lists every object under
events/<id>/, classifies each one (source-audio,source-transcript,captions[<lang>],transcript[<lang>],translated-audio[<lang>],manifest), and combines with the manifest.
- Orphan prefix (no
event.json): the events list still surfaces the id withstatus=endedand an empty target list. The detail page renders a "manifest missing" placeholder with the raw artifact listing intact. - Malformed event id in the URL: the runtime layer rejects with 400 before any B2 call.
- Concurrent delete during list: the parallel HEAD silently drops 404s rather than failing the request.
services/api/tests/test_events.py::test_list_events_empty— empty bucket returns[].services/api/tests/test_events.py::test_list_events_returns_manifest— manifest hydration round-trip.services/api/tests/test_events.py::test_create_event_persists_manifest—POST /eventswritesevent.json.services/api/tests/test_error_handling.py::test_event_id_traversal_is_rejected— malformed id 400.apps/web/e2e/events-list.spec.ts— empty-state copy.