# 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).