The counters said how MUCH and never which file, so a run that was working normally and one that had wedged produced the same screen: `done` stops moving either way. A photograph is around thirty encodes and a video is three plus a poster ladder, and none of that was visible from outside. So the run keeps a transcript and the button opens it. Every rung reports itself by CONSTRUCTION. Ten `variants.push` call sites became one `add`, which is the point — a rung added later is described without anybody remembering to describe it. The slow stages that produce nothing until they finish say so BEFORE they start instead: the HEIC decode, each video rung, the poster frame. Those are the pauses that read as a hang. `?since=<n>` is what keeps it cheap. The transcript is thousands of lines and polls once a second, so sending all of it every tick would be the same megabyte over and over; the client keeps what it has and asks for the rest. No `since` means everything, which is what a modal opened mid-run wants. The transcript lives in the shared slot, not the modal, so closing it does not throw away the run — somebody who looks, closes, and looks again is watching one run rather than two. `startedAt` is what identifies a run, because `n` restarts at 1 and appending across a boundary would interleave two. Polling, not a socket. Each line transfers once and the events are seconds apart, so a second transport to open, authenticate and reconnect would buy nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
82 lines
3.5 KiB
TypeScript
82 lines
3.5 KiB
TypeScript
import { useState } from 'react';
|
|
import { Loader2, RefreshCw } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { SyncModal } from './SyncModal';
|
|
import { useMediaSync, useMediaSyncFeed } from './useMediaSync';
|
|
import { useReady } from './useReady';
|
|
|
|
/**
|
|
* Sync, in the panel's bar beside Build and Publish.
|
|
*
|
|
* ── Why it left the card ──
|
|
*
|
|
* It sat under the two folders it acts on, which was the right place while it was a detail of setting
|
|
* fofos up. It is not one: `derived/` being stale is the reason a picture is missing from a page
|
|
* somebody is looking at, and the fix was three sections away, in a card about where the site lives.
|
|
* Beside Build and Publish it reads as what it is — the third thing you do to a site.
|
|
*
|
|
* The card keeps everything there is no room for up here: the progress bar, the failures, and the run
|
|
* history. Both read one slot, so they cannot disagree about what is true.
|
|
*
|
|
* ── It owns the fetching ──
|
|
*
|
|
* `useMediaSyncFeed` has exactly one caller and this is it, because the bar is mounted whenever
|
|
* anything in this plugin is open and the Administration card is mounted only sometimes. The thing
|
|
* that is always there should be the thing that asks.
|
|
*
|
|
* ── Pressing it OPENS rather than starts ──
|
|
*
|
|
* A sync is minutes of work on a folder somebody may not have looked at in a while, and starting it
|
|
* from a bar gave no way to see what it was doing — the counters said how much and never which file.
|
|
* So the button opens the transcript and the transcript has the verb in it. One press to look, one to
|
|
* commit, and the second one is next to the thing it will fill.
|
|
*
|
|
* ── The label is the state ──
|
|
*
|
|
* `Sync 3` rather than a badge or a dot: the number IS the reason to press it, and a button that says
|
|
* "Sync now" when there is nothing to sync invites a press that does nothing. Idle and current, it
|
|
* still says "Sync" — the action stays available, because a file changed outside the watcher's notice
|
|
* is exactly the case somebody needs it for.
|
|
*/
|
|
export const SyncButton = () => {
|
|
const { running, percent, outstanding } = useMediaSync();
|
|
const { rootSet } = useReady();
|
|
const [open, setOpen] = useState(false);
|
|
|
|
useMediaSyncFeed();
|
|
|
|
/*
|
|
Gated on the ROOT, not on `ready`.
|
|
|
|
Sync reads a folder on this machine and never speaks to fofos, so a broken connection is no reason
|
|
to withhold it — that distinction is the one `useReady` records as the bug that made `rootSet` a
|
|
separate answer in the first place.
|
|
*/
|
|
if (!rootSet) return null;
|
|
|
|
return (
|
|
<>
|
|
<SyncModal open={open} onClose={() => setOpen(false)} />
|
|
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="h-6 shrink-0 gap-1.5 px-2 text-xs"
|
|
onClick={() => setOpen(true)}
|
|
// Never disabled. It opens a window onto a run — which is most worth opening WHILE one is going,
|
|
// and a button that locks itself exactly then is the opposite of what it is for.
|
|
title={
|
|
running
|
|
? 'Building web-ready copies of everything in originals'
|
|
: outstanding > 0
|
|
? `${outstanding} file${outstanding === 1 ? '' : 's'} in originals have no current copy in derived`
|
|
: 'Rebuild anything in derived that is missing or out of date'
|
|
}
|
|
>
|
|
{running ? <Loader2 className="size-3 animate-spin" /> : <RefreshCw className="size-3" />}
|
|
{running ? `Syncing${percent === null ? '' : ` ${percent}%`}` : outstanding > 0 ? `Sync ${outstanding}` : 'Sync'}
|
|
</Button>
|
|
</>
|
|
);
|
|
};
|