From 24820e921ed732219f95610f3445ef2ee449865d Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 23 May 2026 04:12:42 +0000 Subject: [PATCH] polish: schedule past-time confirm, recorder name sanitization, asset detail player controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Schedule: if start_at is more than a minute in the past, confirm() before submitting (operator may want to fire immediately, but shouldn't do it accidentally) - Recorders: generateClipName now sanitizes the recorder name so the S3 key / SMB path / ffmpeg arg stays clean — spaces become underscores, anything outside [A-Za-z0-9._-] is dropped, capped at 40 - Asset detail: audio mute + fullscreen buttons now key off streamUrl state (rather than videoRef.current which is null on first render) so they reliably appear when a stream is available --- services/mam-api/src/routes/recorders.js | 9 ++++++++- services/web-ui/public/screens-asset.jsx | 4 ++-- services/web-ui/public/screens-ingest.jsx | 15 ++++++++++++--- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/services/mam-api/src/routes/recorders.js b/services/mam-api/src/routes/recorders.js index f188912..074f8c1 100644 --- a/services/mam-api/src/routes/recorders.js +++ b/services/mam-api/src/routes/recorders.js @@ -65,7 +65,14 @@ function generateClipName(recorderName) { const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); - return `${recorderName}_${year}${month}${day}_${hours}${minutes}${seconds}`; + // Strip filesystem-hostile characters out of the recorder name (spaces + // become underscores, anything outside [A-Za-z0-9._-] is dropped) so the + // clipName flows cleanly through S3 keys, SMB paths, and ffmpeg args. + const safe = String(recorderName || 'rec') + .replace(/\s+/g, '_') + .replace(/[^A-Za-z0-9._-]/g, '') + .slice(0, 40) || 'rec'; + return `${safe}_${year}${month}${day}_${hours}${minutes}${seconds}`; } // Sanitize an operator-provided clip name so it's safe as both an S3 key diff --git a/services/web-ui/public/screens-asset.jsx b/services/web-ui/public/screens-asset.jsx index 3d4950a..ea9f7e9 100644 --- a/services/web-ui/public/screens-asset.jsx +++ b/services/web-ui/public/screens-asset.jsx @@ -286,12 +286,12 @@ function AssetDetail({ asset, onClose }) { {msToTimecode(currentMs)} {asset.duration} - {videoRef.current && ( + {streamUrl && (
diff --git a/services/web-ui/public/screens-ingest.jsx b/services/web-ui/public/screens-ingest.jsx index 85ab303..456e50c 100644 --- a/services/web-ui/public/screens-ingest.jsx +++ b/services/web-ui/public/screens-ingest.jsx @@ -792,15 +792,24 @@ function NewScheduleModal({ recorders, onClose, onCreated }) { if (!form.recorder_id) return setErr('Pick a recorder'); if (!form.start_at) return setErr('Start time is required'); if (!form.end_at) return setErr('End time is required'); - if (new Date(form.end_at) <= new Date(form.start_at)) return setErr('End must be after start'); + const startD = new Date(form.start_at); + const endD = new Date(form.end_at); + if (endD <= startD) return setErr('End must be after start'); + + // Warn (but allow) start times in the past — the scheduler tick will fire + // them immediately, which is occasionally what the operator wants + // (e.g. "record the next 30 minutes starting now"). + if (startD < new Date(Date.now() - 60_000)) { + if (!confirm('Start time is in the past — recorder will fire immediately when saved.\nContinue?')) return; + } // Datetime-local inputs are in the browser's local zone; ship as ISO so // Postgres stores them as TIMESTAMPTZ properly. const body = { name: form.name.trim(), recorder_id: form.recorder_id, - start_at: new Date(form.start_at).toISOString(), - end_at: new Date(form.end_at).toISOString(), + start_at: startD.toISOString(), + end_at: endD.toISOString(), recurrence: form.recurrence, };