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
+17 -1
View File
@@ -395,9 +395,25 @@ router.post('/_config/media/sync', async (ctx) => {
}
});
/*
The sync's state, and the part of its transcript the caller does not have yet.
`?since=<n>` is what keeps a modal open on a long run cheap: the transcript is thousands of lines by
the end and polls once a second, so sending all of it every tick would be the same megabyte over and
over. The caller keeps what it has been given and asks for what came after.
Absent `since` means "everything", which is what a modal opening mid-run wants — a watcher that
arrived late should see what already happened rather than start from the next line.
*/
router.get('/_config/media/sync', async (ctx) => {
const user = ctx.get('user');
return ctx.json(readSync(user.email));
const state = readSync(user.email);
const raw = ctx.req.query('since');
const since = raw === undefined ? null : Number(raw);
if (since === null || !Number.isFinite(since)) return ctx.json(state);
return ctx.json({ ...state, log: state.log.filter((line) => line.n > since) });
});
/**