diff --git a/CLAUDE.md b/CLAUDE.md index 7e83d09..255bb82 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -952,6 +952,26 @@ OPNsense LAN interface (`reverse-proxies.md` §2). Verify the vhost with `--resolve blackbeard.observer:443:127.0.0.1`, which still exercises the `:443` stream router, SNI, the vhost, the cross-site hop and the API. +**`/v1/healthz`'s `commit` only ever reports the API's.** It is stamped into +the binary, so a frontend-only push — which by design does not rebuild or +redeploy the API — leaves it reading the previous commit **forever**. A watcher +polling it for the sha you just pushed waits for something that will never +arrive, and reports "still deploying" over a site that finished deploying +minutes ago. That is exactly what happened while the wormhole console shipped. + +To verify a frontend deploy, compare what is *served* against what you built: + +```sh +asset=$(curl -s https://blackbeard.observer/ | grep -o '/assets/index-[A-Za-z0-9_-]*\.js' | head -1) +basename "$asset" # served +ls web/dist/assets/index-*.js | xargs -n1 basename # built +``` + +Vite hashes the bundle by content, so equal names mean the deployed frontend is +byte-identical to the local build. Unequal means it has not landed yet. Grepping +the fetched bundle for a string the change introduced is the same check by +another road, and works when the local build is not to hand. + **A frontend-only push skips the Rust build and the API deploy.** The `what changed` step in `deploy.yaml` diffs against `github.event.before` and sets one output; the gate, the musl build, the ts-rs drift check and the whole diff --git a/web/src/components/WormholeStage.tsx b/web/src/components/WormholeStage.tsx index b863359..87a9254 100644 --- a/web/src/components/WormholeStage.tsx +++ b/web/src/components/WormholeStage.tsx @@ -90,6 +90,16 @@ const CONSOLE_LINES = 12 * first line. */ const CONSOLE_LINES_PER_BLOCK = 4 + +/** + * How old the snapshot's block may be said to be before the figure is refused. + * + * The only seed available on load is the difference between two machines' + * clocks, so it is believed at all only while it stays plausible. Generous + * against a slow chain, tight enough that gross skew shows the plain text + * instead of a number nobody should read. + */ +const SEED_LIMIT_MS = 120_000 /** Most marks any single block may spawn, so one big block cannot fill the pool. */ const PER_PULSE_CAP = 28 @@ -218,30 +228,44 @@ export function WormholeStage({ const nextLineId = useRef(0) /** - * When *this page* saw the newest block, from the monotonic clock. + * Where the waiting counter counts from: a monotonic mark, plus however long + * the block was already old when we took it. * - * Not `observed_at`. That is the server's wall clock, and subtracting it from - * the browser's would fold in whatever the two disagree by — a viewer whose - * machine is a minute fast would watch the counter run backwards. - * `performance.now()` cannot be skewed or stepped, and "since this page saw - * it" is the honest claim anyway. + * The ticking is always `performance.now()`, which cannot be skewed or + * stepped by the system clock. Only the *seed* can be inexact, and only for + * the block the snapshot hands over on load — for that one the sole evidence + * of its age is `observed_at`, the server's wall clock, so the seed carries + * whatever the two machines disagree by. Every block that arrives afterwards + * seeds at zero and is exact. * - * `null` until a block actually arrives while the page is open: the first - * block the ticker hands over came from the snapshot and may be a minute old, - * so counting from the moment it was *rendered* would start at zero and lie. + * The first cut refused to seed at all and waited for a live arrival. That + * was more honest and completely useless: on mainnet the block that triggers + * it is the same block that puts the first row in the console, so the counter + * became visible and unnecessary in the same tick and **was never once + * seen**. Reported from the live site after a refresh. */ - const [blockSeenAt, setBlockSeenAt] = useState(null) + const [anchor, setAnchor] = useState<{ at: number; offsetMs: number; height: number } | null>( + null, + ) const lastHeight = useRef(null) useEffect(() => { if (lastBlock === null) return - if (lastHeight.current === null) { - // The snapshot's tail, not an arrival. Note it and wait for a real one. - lastHeight.current = lastBlock.height + if (lastBlock.height === lastHeight.current) return + const first = lastHeight.current === null + lastHeight.current = lastBlock.height + + if (!first) { + setAnchor({ at: performance.now(), offsetMs: 0, height: lastBlock.height }) return } - if (lastBlock.height !== lastHeight.current) { - lastHeight.current = lastBlock.height - setBlockSeenAt(performance.now()) + + // The snapshot's newest block, whose age only the server can tell us. + // Clamped rather than trusted: a clock disagreeing by more than a couple of + // minutes would show a wild figure or count backwards, and no counter beats + // a wrong one. + const seed = Date.now() - Date.parse(lastBlock.observed_at) + if (Number.isFinite(seed) && seed >= 0 && seed <= SEED_LIMIT_MS) { + setAnchor({ at: performance.now(), offsetMs: seed, height: lastBlock.height }) } }, [lastBlock]) @@ -254,12 +278,12 @@ export function WormholeStage({ */ const elapsed = useRef(null) useEffect(() => { - if (blockSeenAt === null || log.length > 0) return + if (anchor === null || log.length > 0) return let frame = 0 let timer = 0 const paint = () => { if (elapsed.current) { - const ms = Math.max(0, Math.round(performance.now() - blockSeenAt)) + const ms = Math.max(0, Math.round(anchor.offsetMs + performance.now() - anchor.at)) elapsed.current.textContent = ms.toLocaleString('en-US') } } @@ -278,7 +302,7 @@ export function WormholeStage({ if (frame) cancelAnimationFrame(frame) if (timer) window.clearInterval(timer) } - }, [blockSeenAt, log.length, reduced]) + }, [anchor, log.length, reduced]) useEffect(() => { if (chain === null) return @@ -647,14 +671,14 @@ export function WormholeStage({
    {log.length === 0 ? (
  1. - {blockSeenAt === null || lastBlock === null ? ( + {anchor === null ? ( 'Each note a block commits or releases will be listed here as it happens.' ) : ( <> 0 {' '} - ms since + ms since )}