App — Upload Entry Point and Finalized Meeting View
Upload flow UI, meeting list correctness, processing states, meeting detail CRUD, and finalized meeting view for the upload-finalizes-meeting milestone.
Owner: App engineer
Domain: App
Complexity: M
Prerequisite: Slice 1 merged and ./dev gen all run
Delivers the complete user-facing path from audio upload to finalized meeting view. Also covers the correctness of the meeting list — including search, the full card data set, and CRUD actions on the meeting detail. No live recording capability is required or included.
Expand the "New Meeting" button into a dropdown with two options: Upload Recording and Start Live Recording (disabled/hidden for this milestone — enabled in milestone 2)
Upload flow: file picker accepting audio/webm, audio/mp4, audio/m4a, audio/wav, audio/mpeg; on file selection, call POST /meetings then POST /meetings/{id}/audio; show upload progress bar while the multipart request is in flight
After upload completes, navigate to the meeting detail page and subscribe to EntityChangedEvent { entity: "transcription" } over the existing WebSocket connection; on receipt, re-fetch GET /transcriptions/{id} via SWR invalidation to advance the processing indicator
Render a processing indicator that maps transcription.status to the four visible states: pending → transcribing → synthesizing → completed
Failed state: when transcription.status === "failed", show the status_message from Core and provide a clear CTA (retry upload or contact support)
Page state persists on refresh: a meeting in transcribing state still shows the correct indicator after a hard reload (derived from REST, not in-memory WebSocket state)
Title — primary label on every card; truncated gracefully at narrow widths
Date and time — grouped by day (Today / Yesterday / weekday + date for older); time shown as h:mm a
Attendee badges — stacked avatar initials for up to 3 attendees; +N overflow badge when more than 3 are present; no attendees renders gracefully (empty state, not an error)
Headline — displayed beneath the title when meeting.headline is present; omitted (no placeholder text) when absent
Source type indicator — icon distinguishing upload, live recording, text, and notes entries
Processing badge — shown when transcription.status is pending, transcribing, or synthesizing; resolved (badge removed) when EntityChangedEvent { entity: "transcription" } arrives and status becomes completed or failed; badge label reflects the current status, not a generic "Processing" string
Search — text input filters the visible meeting list client-side by title and headline; clearing the input restores the full list; search state is not persisted to the URL for this milestone
Edit title — title in the detail header is inline-editable for non-live meetings; on blur or Enter, call PATCH /meetings/{id} with { title }; optimistic update with revert on error; success does not show a toast (consistent with existing live recording behaviour)
Delete meeting — a delete action is accessible from the meeting detail (e.g. overflow menu or dedicated button); triggers a confirmation dialog before calling DELETE /meetings/{id}; on confirm, navigate back to the meeting list; on cancel, dismiss the dialog without any mutation
Deletion of a meeting that has an active recording must be blocked: show an explanatory error (409 from Core) rather than optimistically removing the item
Overview tab: render headline, summary, and tasks once transcription.status === "completed"; notes textarea is always visible regardless of processing state; trigger re-fetch of summary and tasks on SynthesisUpdatedEvent
Transcript tab: render transcript_segments (speaker label + timestamp per segment), topics (sidebar navigation and inline), and talking points (per topic) once processing completes; trigger a full segment list reload on TranscriptRevisedEvent; trigger topics and talking points re-fetch on SynthesisUpdatedEvent