10 KiB
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:
- Storage warning header — a prominent set-once warning at the top of the Storage settings section.
- Growing-files SMB credentials + system CIFS mount — store an SMB username/password and have the capture stack mount the growing share itself (Approach A).
- 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 likes3_secret_keyare stored butGET /settings/s3returns onlys3_secret_key_exists(never the value). - Growing settings UI:
GrowingSettingsCardinservices/web-ui/public/screens-admin.jsx(rendered byStorageSectionalongsideMountHealthStripandS3SettingsCard). 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/growingoverGROWING_KEYS. - Global enable today: the
growing_enabledsetting (checkbox labelled "Capture writes to the local SMB share first; Premier can edit while it's still growing").recorders.jsreads this global key at recorder-start and passesGROWING_ENABLED=true/falseto the capture container. - Capture write path:
services/capture/src/capture-manager.jsreadsprocess.env.GROWING_ENABLEDandGROWING_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:
/growingis 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 (
--dangerborder + 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_KEYSwith the new keys (except the password is handled specially). GET /settings/growing: returngrowing_smb_mount,growing_smb_username,growing_smb_vers, andgrowing_smb_password_exists: boolean— never the password value. (Mirror the existings3_secret_key_existspattern.)PUT /settings/growing: upsert each provided key. Forgrowing_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_existsis 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:
- Write a root-only credentials file (e.g.
/run/smb-creds, mode0600) containing:
(Credentials go in the file, not the mount command line, to avoidusername=<GROWING_SMB_USERNAME> password=<GROWING_SMB_PASSWORD>ps/log exposure.) mkdir -p $GROWING_PATHthenmount -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.- If the mount succeeds → proceed writing the master to
$GROWING_PATH/...(existing behaviour). - If the mount fails → log the error and fall back to S3 streaming (
growingPath = null), so a recording is never lost. - 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 thesettingstable at start). - The dynamically-spawned capture container must get
/growingas 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 formount).
Security notes
- The password is stored plaintext in the
settingstable, identical to the existings3_secret_keyhandling — 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_enabledkey) fromGrowingSettingsCard. 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_enabledsettings 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 BOOLEANcolumn. New semantics (no global to defer to):TRUE= this recorder uses growing-files mode;NULL/FALSE= off. recorders.jsrecorder-start: read the recorder's owngrowing_enabled(defaultingNULL→off) and setGROWING_ENABLEDfor the capture container from that, instead of the global setting.- Add
growing_enabledtoRECORDER_FIELDSso create/update accept it.
UI
- New-recorder modal (
modal-new-recorder.jsx): add a "Growing-files mode" toggle that setsgrowing_enabledon 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/growingtreats a field value of the literal sentinel""with an explicitgrowing_smb_password_clear: trueflag 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_enabledis 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_keyin 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_daysin the UI (seeded in DB, still unsurfaced; unrelated to this work). - Playout HLS preview fix (handled by a separate parallel effort).