From 3122dfd1b929c09601cd8ec27298d6d3627e6ccf Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 31 May 2026 14:42:07 -0400 Subject: [PATCH] docs(settings): storage warning + growing SMB auth + per-recorder growing design spec Co-Authored-By: Claude Opus 4.8 --- ...-31-storage-settings-growing-smb-design.md | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-31-storage-settings-growing-smb-design.md diff --git a/docs/superpowers/specs/2026-05-31-storage-settings-growing-smb-design.md b/docs/superpowers/specs/2026-05-31-storage-settings-growing-smb-design.md new file mode 100644 index 0000000..863aa9c --- /dev/null +++ b/docs/superpowers/specs/2026-05-31-storage-settings-growing-smb-design.md @@ -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= + 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).