dragonflight/docs/superpowers/specs/2026-05-31-storage-settings-growing-smb-design.md
2026-05-31 14:50:36 -04:00

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:

  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.jsGET/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: booleannever 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).