Recording
- A screen wake lock is held for as long as a recording runs. Browsers drop
the lock whenever the page is hidden, so it is taken again on return —
without that, one glance away ended it for the session. The lock is
reference counted (two recorders cannot release each other's), never
requested while hidden (the request would just be rejected), and a denial
or an unsupported browser leaves the recording running.
- Signing out releases it and stops the recording; nothing is sent, because
the session that owned the audio is gone.
- start() on an already-running recorder is now a no-op instead of replacing
the MediaRecorder and silently dropping everything captured so far.
- A recording that ends by itself — recorder error, or the microphone taken
by another app, unplugged or revoked — takes the same path as pressing
Stop, so it is transcribed and stored rather than left in a tab that still
says "recording". Moving around the workspace already kept recording.
Retention
- Every recording is kept for 24 hours now, not only the ones whose
transcription failed. /api/transcribe already has the audio, so this costs
no second upload, and a storage failure is logged rather than thrown: it
must never lose the transcription someone is waiting for.
- One store (src/utils/audioBackupStore.js) is shared by /api/transcribe and
/api/audio-backups so the two cannot drift. Payload goes to object storage
when AUDIO_BACKUPS_S3_* is set and to the encrypted Postgres column
otherwise; metadata always stays in Postgres, so listing, ownership and
expiry behave the same either way. Object keys are scoped by owner, and
the expiry sweep deletes the object with the row.
Verified against the live database: round trip byte-identical, another user
reads null, 950 -> 48 bytes compressed, expired rows take their objects.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU