polish: schedule past-time confirm, recorder name sanitization, asset detail player controls
- 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
This commit is contained in:
parent
47ad01d0b2
commit
24820e921e
3 changed files with 22 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -286,12 +286,12 @@ function AssetDetail({ asset, onClose }) {
|
|||
<span className="mono" style={{ fontSize: 11.5, color: "var(--text-2)", minWidth: 70 }}>{msToTimecode(currentMs)}</span>
|
||||
<PlaybackBar current={currentMs} total={totalMs} onSeek={seek} comments={visibleComments} />
|
||||
<span className="mono" style={{ fontSize: 11.5, color: "var(--text-3)", minWidth: 70, textAlign: "right" }}>{asset.duration}</span>
|
||||
{videoRef.current && (
|
||||
{streamUrl && (
|
||||
<React.Fragment>
|
||||
<div style={{ width: 1, height: 18, background: "var(--border-strong)" }} />
|
||||
<button
|
||||
className="icon-btn"
|
||||
title={videoRef.current.muted ? 'Unmute' : 'Mute'}
|
||||
title="Toggle mute"
|
||||
onClick={function() { if (videoRef.current) { videoRef.current.muted = !videoRef.current.muted; } }}>
|
||||
<Icon name="audio" size={14} />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue