diff --git a/services/web-ui/public/screens-ingest.jsx b/services/web-ui/public/screens-ingest.jsx index ce29443..637be9a 100644 --- a/services/web-ui/public/screens-ingest.jsx +++ b/services/web-ui/public/screens-ingest.jsx @@ -1056,7 +1056,7 @@ function Monitors({ navigate }) { .catch(() => setChannels([])); }; refresh(); - const id = setInterval(refresh, 5000); + const id = setInterval(refresh, 3000); return () => clearInterval(id); }, []); @@ -1163,6 +1163,49 @@ function MonitorTile({ feed, seed }) { const [levels, setLevels] = React.useState([0.65, 0.78]); const isLive = feed.status === 'recording'; + // Persist the last known asset ID so we can show frozen HLS/thumbnail after + // a recording stops. Cleared only when a new recording starts on this recorder. + const [lastAssetId, setLastAssetId] = React.useState(feed.live_asset_id || null); + const [lastRecorderId, setLastRecorderId] = React.useState(feed.id); + const [frozenThumb, setFrozenThumb] = React.useState(null); + + React.useEffect(() => { + if (feed.live_asset_id) { + setLastAssetId(feed.live_asset_id); + setLastRecorderId(feed.id); + setFrozenThumb(null); // reset frozen thumb — live HLS takes over + } + }, [feed.live_asset_id]); + + // When recording stops, try to fetch a thumbnail for the last asset so we can + // show a frozen frame instead of blank noise. + React.useEffect(() => { + if (isLive || !lastAssetId) return; + if (frozenThumb) return; // already fetched + let cancelled = false; + window.ZAMPP_API.fetch('/assets/' + lastAssetId + '/thumbnail') + .then(data => { + if (!cancelled && data && data.url) setFrozenThumb(data.url); + }) + .catch(() => {}); + return () => { cancelled = true; }; + }, [isLive, lastAssetId]); + + // Tick elapsed while recording. Seed from feed.started_at (populated by Docker + // inspect on the mam-api side). Falls back to 0 if not yet available. + const [elapsedSecs, setElapsedSecs] = React.useState(0); + React.useEffect(() => { + if (!isLive) { setElapsedSecs(0); return; } + const tick = () => { + if (feed.started_at) { + setElapsedSecs(Math.max(0, Math.floor((Date.now() - new Date(feed.started_at).getTime()) / 1000))); + } + }; + tick(); + const id = setInterval(tick, 1000); + return () => clearInterval(id); + }, [isLive, feed.started_at]); + React.useEffect(() => { if (!isLive) return; const id = setInterval(() => { @@ -1171,6 +1214,8 @@ function MonitorTile({ feed, seed }) { return () => clearInterval(id); }, [isLive]); + const displayElapsed = _fmtElapsed(elapsedSecs * 1000); + if (feed.kind === 'audio') { return (