Three S3 configurations had grown separately — S3_* for documents,
GENERATED_IMAGES_S3_* for images, and AUDIO_BACKUPS_S3_* after them — with
different key names and their own client construction. That is why moving
storage meant hunting through several files.
src/utils/objectStorage.js now resolves settings for any purpose: its own
variables first, then the shared S3_* ones, with a per-purpose bucket name
(S3_BUCKET_AUDIO_BACKUPS). One endpoint plus three bucket names is enough
for the whole app, and a purpose that needs its own account still overrides
everything. Audio backups and documents use it; generated images keeps its
own tested storage module, whose variable names the resolver already
understands.
Nothing existing has to change: S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY and
the AWS_* fallbacks still resolve, and path-style addressing keeps each
purpose's previous default — off for documents, so a Backblaze endpoint
behaves as before, on where a custom endpoint implies MinIO. A _FILE
credential now always beats an inline one, so a mounted secret cannot be
shadowed by an inherited environment variable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
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