Sync says what it is doing, file by file

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>
This commit is contained in:
2026-08-31 12:16:52 +01:00
co-authored by Claude Opus 5
parent 78db209ea1
commit ddcba1c95d
6 changed files with 442 additions and 40 deletions
+33 -18
View File
@@ -1,5 +1,7 @@
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';
@@ -22,6 +24,13 @@ import { useReady } from './useReady';
* 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
@@ -30,8 +39,9 @@ import { useReady } from './useReady';
* is exactly the case somebody needs it for.
*/
export const SyncButton = () => {
const { running, percent, outstanding, busy, start } = useMediaSync();
const { running, percent, outstanding } = useMediaSync();
const { rootSet } = useReady();
const [open, setOpen] = useState(false);
useMediaSyncFeed();
@@ -45,22 +55,27 @@ export const SyncButton = () => {
if (!rootSet) return null;
return (
<Button
size="sm"
variant="outline"
className="h-6 shrink-0 gap-1.5 px-2 text-xs"
onClick={() => void start()}
disabled={busy || running}
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>
<>
<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>
</>
);
};