import { useEffect } from 'react'; import { CircleAlert } from 'lucide-react'; import { useMediaSync } from './useMediaSync'; import { SyncButton } from './SyncButton'; /** * What syncing `derived/` is DOING, shown where the two folders are. * * ── The button is back, because the bar it left for no longer exists ── * * It moved out to the panel's bar beside Build and Publish, and then the bar was removed — so for a * while there was no way to start a sync from anywhere in this admin. Worse quietly: `SyncButton` is * the only caller of `useMediaSyncFeed`, so with it unmounted this card was not being polled either * and showed a run that was going nowhere. * * Here is where it was before and where it belongs: beside the two folders it operates on, in the * section somebody opens to look after the media. Both read one `useGlobal` slot, so the label and * this card cannot disagree about what is true. * * `[open]` Sync, Build and Publish still want a home of their own where all three are to hand without * opening Administration. This is the one that could not wait for it. * * ── 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 (
{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.'}
{/* Under the sentence rather than beside it: the line above can run to two clauses, and a button sharing that row moves as the counts change. */}
{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 && (