docs(settings): storage warning + growing SMB auth + per-recorder growing design spec

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Zac Gaetano 2026-05-31 14:42:07 -04:00
parent 3b2f9fe6a0
commit 3122dfd1b9

View file

@ -0,0 +1,148 @@
# Storage Settings Warning + Growing-Files SMB Auth + Per-Recorder Growing Mode
**Date:** 2026-05-31
**Branch:** `feat/playout-mcr` (Forgejo: WildDragonLLC/dragonflight)
**Status:** Approved, ready for implementation plan
---
## Scope
Three related refinements to the Settings page and growing-files capture pipeline:
1. **Storage warning header** — a prominent set-once warning at the top of the Storage settings section.
2. **Growing-files SMB credentials + system CIFS mount** — store an SMB username/password and have the capture stack mount the growing share itself (Approach A).
3. **Per-recorder growing mode** — remove the global "capture writes to local SMB share first" toggle; make growing-files mode a per-recorder setting.
All changes live on the `growing-files` / storage-settings path. No playout changes (handled separately).
---
## Background (current state)
- **Settings storage:** single key/value `settings(key TEXT PRIMARY KEY, value TEXT, updated_at)` table (migration 006). Secrets like `s3_secret_key` are stored but `GET /settings/s3` returns only `s3_secret_key_exists` (never the value).
- **Growing settings UI:** `GrowingSettingsCard` in `services/web-ui/public/screens-admin.jsx` (rendered by `StorageSection` alongside `MountHealthStrip` and `S3SettingsCard`). Current keys: `growing_enabled`, `growing_path`, `growing_smb_url`, `growing_promote_after_seconds`.
- **Settings API:** `services/mam-api/src/routes/settings.js``GET/PUT /settings/growing` over `GROWING_KEYS`.
- **Global enable today:** the `growing_enabled` setting (checkbox labelled "Capture writes to the local SMB share first; Premier can edit while it's still growing"). `recorders.js` reads this global key at recorder-start and passes `GROWING_ENABLED=true/false` to the capture container.
- **Capture write path:** `services/capture/src/capture-manager.js` reads `process.env.GROWING_ENABLED` and `GROWING_PATH` (default `/growing`). When on, it writes the master to `/growing/{projectId}/{clipName}.{ext}` instead of streaming to S3; the promotion worker uploads to S3 after stop.
- **Current mount model:** `/growing` is a pre-mounted host bind-mount; the app never authenticates to SMB.
- **Per-recorder column already exists:** migration 014 added `recorders.growing_enabled BOOLEAN DEFAULT NULL` ("NULL = use global"), but recorder-start logic ignores it and the new-recorder modal does not expose it.
---
## Part 1 — Storage warning header
Add a danger-styled banner at the **top of `StorageSection()`**, above `MountHealthStrip`.
- Visual: full-width banner, danger token styling (`--danger` border + subtle danger background), alert icon, bold uppercase text.
- Exact copy:
> **⚠ WARNING — THESE SETTINGS ARE MEANT TO BE SET ONCE AT INITIAL DEPLOYMENT. CHANGING STORAGE PATHS MID-CYCLE CAN CAUSE DATABASE CORRUPTION AND FILE LOSS. PLEASE USE WITH CAUTION.**
- Pure presentational; no backend, no dismiss state (always visible).
**Files:** `services/web-ui/public/screens-admin.jsx` (and a small style rule in the appropriate CSS file if needed).
---
## Part 2 — Growing-files SMB credentials + system CIFS mount (Approach A)
The growing share is **shared infrastructure**, so the SMB connection config is global.
### New settings keys
| Key | Purpose | Notes |
|-----|---------|-------|
| `growing_smb_mount` | CIFS source for the system mount, e.g. `//10.0.0.25/mam-growing` | Distinct from `growing_smb_url` |
| `growing_smb_username` | SMB user | Returned in GET (not secret) |
| `growing_smb_password` | SMB password | **Write-only** — never returned |
| `growing_smb_vers` *(optional)* | CIFS protocol version, default `3.0` | Avoids mount negotiation failures |
`growing_smb_url` (the `smb://…` string) is retained unchanged as the **editor-facing** display value (Premiere connect string).
### Settings API (`settings.js`)
- Extend `GROWING_KEYS` with the new keys (except the password is handled specially).
- `GET /settings/growing`: return `growing_smb_mount`, `growing_smb_username`, `growing_smb_vers`, and `growing_smb_password_exists: boolean`**never** the password value. (Mirror the existing `s3_secret_key_exists` pattern.)
- `PUT /settings/growing`: upsert each provided key. For `growing_smb_password`, only write it when a non-empty value is provided (an empty/omitted field leaves the stored password unchanged). Provide a way to clear it (explicit empty sentinel or a "clear" affordance) — see Resolved decisions below.
### Settings UI (`GrowingSettingsCard`)
Add three fields to the card:
- **SMB mount (CIFS):** text input bound to `growing_smb_mount`, placeholder `//10.0.0.25/mam-growing`.
- **SMB username:** text input bound to `growing_smb_username`.
- **SMB password:** masked password input. Shows a "saved" indicator when `growing_smb_password_exists` is true; typing a new value replaces it; leaving it blank keeps the existing one. `autoComplete="new-password"`.
### Capture image (`services/capture/Dockerfile`)
Add `cifs-utils` to the installed packages so `mount -t cifs` is available inside the capture container.
### Capture-manager (`capture-manager.js`)
On capture start, when growing mode is active **and** `GROWING_SMB_MOUNT` is set:
1. Write a root-only credentials file (e.g. `/run/smb-creds`, mode `0600`) containing:
```
username=<GROWING_SMB_USERNAME>
password=<GROWING_SMB_PASSWORD>
```
(Credentials go in the file, **not** the mount command line, to avoid `ps`/log exposure.)
2. `mkdir -p $GROWING_PATH` then `mount -t cifs $GROWING_SMB_MOUNT $GROWING_PATH -o credentials=/run/smb-creds,uid=0,gid=0,file_mode=0664,dir_mode=0775,vers=$GROWING_SMB_VERS`.
3. If the mount succeeds → proceed writing the master to `$GROWING_PATH/...` (existing behaviour).
4. If the mount **fails** → log the error and fall back to S3 streaming (`growingPath = null`), so a recording is never lost.
5. On capture stop/cleanup, unmount `$GROWING_PATH` (best-effort; ignore "not mounted").
Mount isolation: each recorder runs in its **own** capture container, so each container mounts CIFS at its own private `/growing` — no cross-recorder mount conflicts, no ref-counting needed.
### Recorder start (`recorders.js`)
- Pass new env to the spawned capture container: `GROWING_SMB_MOUNT`, `GROWING_SMB_USERNAME`, `GROWING_SMB_PASSWORD`, `GROWING_SMB_VERS` (read from the `settings` table at start).
- The dynamically-spawned capture container must get `/growing` as an **empty mountpoint** (not a host bind-mount) so the in-container CIFS mount lands cleanly. Confirm/adjust the container spec accordingly. The container is already privileged (required for `mount`).
### Security notes
- The password is stored plaintext in the `settings` table, identical to the existing `s3_secret_key` handling — acceptable within this app's current secret model.
- The password reaches the capture container as an env var (visible via `docker inspect`), same as S3 keys already are. The credentials **file** (not the command line) is used for the actual mount.
---
## Part 3 — Per-recorder growing mode (remove the global toggle)
### Remove global enable
- Delete the global "Capture writes to the local SMB share first…" checkbox (`growing_enabled` key) from `GrowingSettingsCard`. The card no longer carries a global on/off — it is **infrastructure-only**: container mount path, SMB URL (editor), SMB mount + credentials, promote threshold.
- The `growing_enabled` settings *key* is retired from the UI. (It may remain in the table harmlessly; recorder-start no longer reads it.)
### Per-recorder semantics
- Reuse the existing `recorders.growing_enabled BOOLEAN` column. New semantics (no global to defer to): `TRUE` = this recorder uses growing-files mode; `NULL`/`FALSE` = off.
- `recorders.js` recorder-start: read the **recorder's own** `growing_enabled` (defaulting `NULL`→off) and set `GROWING_ENABLED` for the capture container from that, instead of the global setting.
- Add `growing_enabled` to `RECORDER_FIELDS` so create/update accept it.
### UI
- **New-recorder modal** (`modal-new-recorder.jsx`): add a "Growing-files mode" toggle that sets `growing_enabled` on the created recorder (default off).
- **Recorder edit** (wherever recorders are edited): same toggle.
- Helper text on the toggle notes that growing-files requires the SMB share to be configured in Settings → Storage.
### Fallback
If a recorder has `growing_enabled = true` but `growing_smb_mount` is not configured globally, capture logs a warning and falls back to S3 streaming (same fallback path as a failed mount). Recording is never blocked.
---
## Files changed
| File | Change |
|------|--------|
| `services/web-ui/public/screens-admin.jsx` | Storage warning banner; SMB mount/username/password fields in `GrowingSettingsCard`; remove global growing-enable checkbox |
| `services/web-ui/public/modal-new-recorder.jsx` | Per-recorder "Growing-files mode" toggle |
| `services/mam-api/src/routes/settings.js` | New growing SMB keys; write-only password (`growing_smb_password_exists`) |
| `services/mam-api/src/routes/recorders.js` | Read per-recorder `growing_enabled`; pass SMB env to capture; `RECORDER_FIELDS` += `growing_enabled`; empty `/growing` mountpoint |
| `services/capture/Dockerfile` | Add `cifs-utils` |
| `services/capture/src/capture-manager.js` | CIFS mount-on-start (creds file), unmount-on-stop, fallback to S3 on failure |
| CSS (storage warning / fields) | Minor styles if needed |
No DB migration required (the `recorders.growing_enabled` column already exists; new settings are key/value rows).
---
## Resolved decisions
- **Clearing the SMB password:** `PUT /settings/growing` treats a field value of the literal sentinel `""` with an explicit `growing_smb_password_clear: true` flag as "remove the stored password"; a blank field with no clear flag leaves it unchanged. (Keeps the common "don't retype the password on every save" UX while still allowing removal.)
- **CIFS version:** default `growing_smb_vers = 3.0`; overridable via settings to support older NAS targets.
- **Recorders already recording when the toggle changes:** the per-recorder `growing_enabled` is read at **start** only; changing it mid-recording has no effect on the active session (consistent with how all recorder encode settings already behave).
---
## Out of scope (deferred)
- Encrypting secrets at rest (the app's existing model stores `s3_secret_key` in plaintext; SMB password follows the same model).
- A global "growing-files master kill switch" (removed by design — control is now per-recorder).
- Exposing `growing_retention_days` in the UI (seeded in DB, still unsurfaced; unrelated to this work).
- Playout HLS preview fix (handled by a separate parallel effort).