import { useEffect } from 'react'; import { CircleAlert } from 'lucide-react'; import { useMediaSync } from './useMediaSync'; /** * What syncing `derived/` is DOING, shown where the two folders are. * * ── The button is not here any more ── * * It moved to the panel's bar, beside Build and Publish — see SyncButton for why. What stayed is * everything that needs room to read: how far a run has got, which files failed, and the history. * * Both read one `useGlobal` slot, so the button's label and this card cannot disagree about what is * true. Nothing here fetches: the bar is mounted whenever the plugin is open and does the asking. * * ── Built and skipped are counted separately ── * * Re-running is normal and cheap: everything already correct is skipped, because each derived folder * carries the size and mtime it was built from. "412 already current" is the system saying it had * nothing to do, which is a different and more useful answer than "412 done". * * ── Failures stay ── * * One bad file in four hundred is exactly what a progress bar hides, and it is the only part of this * anybody has to act on. */ /** Recent runs worth showing. Enough to see a pattern, few enough to stay a footnote. */ const SHOWN_RUNS = 5; const clock = (iso: string) => new Date(iso).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); const day = (iso: string) => new Date(iso).toLocaleDateString(undefined, { day: 'numeric', month: 'short' }); /** `1m 12s`, or `8s`. Minutes matter here and hours have never happened. */ const took = (from: string, to: string) => { const seconds = Math.max(0, Math.round((Date.parse(to) - Date.parse(from)) / 1000)); return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`; }; export const MediaSync = ({ onSettled }: { onSettled?: (settled: boolean) => void }) => { const { state, ledger, running, percent, outstanding, known, error } = useMediaSync(); const reconciliation = ledger?.reconciliation ?? null; /* One bit, upward: is there anything left to do? Reported rather than lifted, because the parent needs the single fact it cannot compute — whether this part of the setup is finished — and nothing else about the sync belongs to it. */ useEffect(() => { if (known) onSettled?.(outstanding === 0); }, [known, outstanding, onSettled]); return (
{/* Not what the last run did — what is TRUE NOW. A run's counts age the moment somebody uploads a file, and "62 built" three days ago says nothing about whether derived/ is current. This is recomputed from the filesystem, using the same freshness test the sync itself uses. Full width, because the button that used to sit beside it is in the bar now. */}

{running ? state?.total === null || state?.total === undefined ? 'Reading originals…' : `${state.done} of ${state.total}${state.current.length ? ` · ${state.current.join(', ')}` : ''}` : reconciliation ? outstanding === 0 ? `${reconciliation.current} of ${reconciliation.originals} current` : [ reconciliation.missing.length ? `${reconciliation.missing.length} missing` : '', reconciliation.stale.length ? `${reconciliation.stale.length} changed` : '', ] .filter(Boolean) .join(' · ') : 'Web-ready copies of everything in originals.'}

{running && (
{/* Indeterminate until the walk has counted: a bar that starts full and shrinks is worse than one that has not started. */}
)} {state?.fatal && (

{state.fatal}

)} {state && state.errors.length > 0 && ( )} {error &&

{error}

} {/* The ledger. Survives a restart, which the live state above does not — and it is the only place a failure from a run nobody watched is ever recorded, since a file that did not build leaves no folder and no manifest behind it. */} {ledger && ledger.runs.length > 0 && ( )}
); };