feat/fix: visuals.jsx — growing migrate flow + deltacast cleanup

This commit is contained in:
Zac Gaetano 2026-06-02 18:40:16 -04:00
parent 29238a339e
commit 5525041901

View file

@ -25,10 +25,6 @@ function AssetThumb({ asset, size = 'md' }) {
);
}
// Live/recording assets: once the capture sidecar has published a poster
// thumbnail (first frame of the recording), show that static frame instead
// of the HLS "connecting" player. Until the poster exists (the brief window
// before the first segment is grabbed), fall back to the live HLS preview.
if (asset.status === 'live' && asset.id) {
if (asset.thumbnail_s3_key || thumbUrl) {
const altText = asset.name ? `Thumbnail for ${asset.name}` : 'Live recording thumbnail';
@ -37,7 +33,6 @@ function AssetThumb({ asset, size = 'md' }) {
{thumbUrl
? <img src={thumbUrl} alt={altText} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
: <FauxFrame />}
{/* Keep the pulsing LIVE border so it still reads as recording */}
<div style={{ position: 'absolute', inset: 0, border: '2px solid var(--live)', pointerEvents: 'none', borderRadius: 'inherit' }} />
</div>
);
@ -45,6 +40,19 @@ function AssetThumb({ asset, size = 'md' }) {
return <LiveThumb assetId={asset.id} aspect={aspect} />;
}
if (asset.status === 'pending_migration') {
return (
<div className="asset-thumb" style={{ aspectRatio: aspect, position: 'relative', background: 'var(--bg-2)', overflow: 'hidden' }}>
<div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center', gap: 6,
color: 'var(--text-3)', fontSize: 11 }}>
<Icon name="upload" size={20} style={{ opacity: 0.5 }} />
<span>Awaiting migration</span>
</div>
</div>
);
}
const altText = asset.name ? `Thumbnail for ${asset.name}` : 'Asset thumbnail';
return (
<div className="asset-thumb" style={{ background: 'var(--bg-2)', aspectRatio: aspect, overflow: 'hidden', position: 'relative' }}>
@ -55,10 +63,6 @@ function AssetThumb({ asset, size = 'md' }) {
);
}
// Muted inline HLS preview for a live/recording asset tile. Attaches hls.js
// (or native HLS on Safari) to show the live feed inside the library card.
// Shows a "connecting" spinner while the manifest loads, falls back to a
// placeholder with a record icon if hls.js is unavailable or playback fails.
function LiveThumb({ assetId, aspect }) {
const videoRef = React.useRef(null);
const [ready, setReady] = React.useState(false);
@ -129,7 +133,6 @@ function LiveThumb({ assetId, aspect }) {
autoPlay
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
/>
{/* Pulsing red border while connecting or playing, matches LIVE badge colour */}
<div style={{ position: 'absolute', inset: 0, border: '2px solid var(--live)', pointerEvents: 'none', borderRadius: 'inherit' }} />
{!ready && !failed && (
<div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column',
@ -232,6 +235,7 @@ function StatusDot({ status }) {
done: { color: 'var(--success)', pulse: false },
failed: { color: 'var(--danger)', pulse: false },
stopped: { color: 'var(--text-4)', pulse: false },
pending_migration: { color: 'var(--warning)', pulse: false },
};
const s = map[status] || { color: 'var(--text-3)' };
return <span className={'status-dot ' + (s.pulse ? 'pulse' : '')} style={{ background: s.color, boxShadow: '0 0 0 3px ' + s.color + '30' }} />;
@ -250,19 +254,6 @@ function Elapsed({ seconds, live = false }) {
return <span className="mono">{String(h).padStart(2,'0')}:{String(m).padStart(2,'0')}:{String(s).padStart(2,'0')}</span>;
}
//
// ConfirmModal + useConfirm in-page replacement for window.confirm().
//
// Usage in a component:
// const [confirm, confirmModal] = useConfirm();
// ...
// if (!(await confirm({ title: 'Delete user?', message: '' }))) return;
// ...
// return (<>{confirmModal} ...rest of UI... </>);
//
// confirm(opts) returns a Promise<boolean>. Options:
// title, message, confirmLabel (default 'Delete'), cancelLabel ('Cancel'),
// danger (default true red confirm button).
function ConfirmModal({ title, message, confirmLabel = 'Delete', cancelLabel = 'Cancel', danger = true, onConfirm, onCancel }) {
React.useEffect(() => {
const onKey = (e) => {
@ -283,7 +274,7 @@ function ConfirmModal({ title, message, confirmLabel = 'Delete', cancelLabel = '
<div className="modal-body">
{typeof message === 'string'
? message.split('\n').map((line, i) => (
<div key={i} style={{ fontSize: 13, color: 'var(--text-2)', lineHeight: 1.5 }}>{line || ' '}</div>
<div key={i} style={{ fontSize: 13, color: 'var(--text-2)', lineHeight: 1.5 }}>{line || ' '}</div>
))
: <div style={{ fontSize: 13, color: 'var(--text-2)', lineHeight: 1.5 }}>{message}</div>}
</div>
@ -297,7 +288,7 @@ function ConfirmModal({ title, message, confirmLabel = 'Delete', cancelLabel = '
}
function useConfirm() {
const [state, setState] = React.useState(null); // { opts, resolve } | null
const [state, setState] = React.useState(null);
const confirm = React.useCallback((opts) => {
return new Promise((resolve) => {