tags/meeting_tags existed since migration 0001, completely unused.
Adds upsert_tag/tags_for_meeting (mirroring upsert_participant's
find-or-create pattern) and real set_tags/list_tags implementations.
Extends list_meetings with tag/participant/date filters via a new
MeetingFilter struct, replacing the old query-only signature — the
participant filter matches either a speaker mapped to that participant
or an attendee of the meeting's linked calendar event, since neither
signal alone covers "who was in this meeting." Tags are populated on
Meeting/MeetingListItem/SearchHit via a per-row lookup (N+1, but
meeting counts are small enough that this is simpler than GROUP_CONCAT
string-splitting).
The initial notes.md render after finalize/resume/recovery bypassed
the Store abstraction entirely (raw std::fs::write) — routing it
through update_notes means that first render also gets FTS-indexed and
bumps updated_at, instead of only later manual edits doing either.
meeting_fts existed since migration 0001 but nothing ever wrote to it.
Adds reindex_fts (called from finalize_meeting/update_notes so the
index can't drift from what's on disk), a one-time startup backfill
for meetings finalized before this feature existed, delete_meeting
cleanup (meeting_fts is a virtual table with no FK/CASCADE support),
and a real Store::search() replacing the hardcoded stub. Query input
is phrase-quoted per token so stray FTS5 operators in free-text search
input can't throw a MATCH syntax error.
start_recording now creates the EventSink alongside the existing frame
channel and forwards Level/DeviceChanged onto a small fire-and-forget
thread that emits "recording://level" and "recording://device".
Adds AudioLevel (rms/peak per chunk, throttled to ~20Hz) and
CaptureEvent::DeviceChanged emitted through a new EventSink alongside
the existing FrameSink. On a capture read failure, reopens against
whatever is now the default render device and continues the same WAV
file if the new format is compatible; otherwise fails loudly rather
than silently corrupting the recording.
Reports LLM endpoint/local-only status and enabled sync targets so the
UI can prove local-only handling, per the shape fixed in
docs/04-api-contracts.md. Sync targets stay empty until Phase 9 lands
SyncManager; list_sync_targets' not_implemented error is swallowed
rather than failing the whole self-check.