fix: the waiting counter was never once visible
It refused to count until a block arrived while the page was open, on the grounds that the block the snapshot hands over on load may be a minute old and starting it at zero would lie. That was honest and completely useless: on mainnet the block that triggers the counter is the same block that puts the first row in the console, so it became visible and unnecessary in the same tick. Reported from the live site after a refresh — the placeholder was still there, because the counter had no window in which to exist. It now seeds from the snapshot's block. The ticking is still `performance.now()`, which cannot be skewed or stepped; only the seed can be inexact, and only for that first block, whose age nothing but the server's `observed_at` can attest. A seed outside a plausible range is refused outright and the plain text stands — no counter beats a wrong one. Measured against the live API: the counter appears within 800ms of load reading "26,378 ms since #43,588", climbs, and gives way to rows about two seconds later, which is how long the console is honestly empty. Also recorded, because it wasted a verification: `/v1/healthz`'s `commit` is stamped into the API binary, so a frontend-only push leaves it on the previous sha *forever*. A watcher polling it for the sha just pushed waits for something that will never arrive and reports "still deploying" over a site that finished minutes ago. Compare the served bundle name to the local build instead — Vite hashes it by content, so equal names mean byte-identical. Refs #17 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
20
CLAUDE.md
20
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
|
||||
|
||||
@@ -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<number | null>(null)
|
||||
const [anchor, setAnchor] = useState<{ at: number; offsetMs: number; height: number } | null>(
|
||||
null,
|
||||
)
|
||||
const lastHeight = useRef<number | null>(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<HTMLSpanElement>(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({
|
||||
<ol className="stage-log" aria-label="Recent notes through the wormhole">
|
||||
{log.length === 0 ? (
|
||||
<li className="stage-log-empty">
|
||||
{blockSeenAt === null || lastBlock === null ? (
|
||||
{anchor === null ? (
|
||||
'Each note a block commits or releases will be listed here as it happens.'
|
||||
) : (
|
||||
<>
|
||||
<span ref={elapsed} className="stage-elapsed">
|
||||
0
|
||||
</span>{' '}
|
||||
ms since <BlockLink chain={chain} height={lastBlock.height} />
|
||||
ms since <BlockLink chain={chain} height={anchor.height} />
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user