The blank /live (the project lead, 14:3x UK): IgneumDag 2.0.2 painted only from a requestAnimationFrame loop gated on document.hidden and the IntersectionObserver, so a page that loaded with document.hidden true (the desktop pane, a background tab) and whose embedder never fired visibilitychange took every push, reported live and never drew a frame. 2.0.3 paints the current picture synchronously on push, size and theme change; the motion loop alone waits for a visible document and an intersecting canvas. Confirmed headless on build-2 against the live feed: hidden document 0 lit pixels before, 110,007 after; the never-intersecting observer repaints on every push. Known-failed test tools/scene/paint-test.cjs (the 2.0.2 renderer draws nothing in the same world). The second 2.0.3 change: the phone rule (30 s window, four lanes) keys on the viewport width, not the canvas width; a 640 px hero on a 1,440 px laptop was rendering as a phone while the app's card beside it was not. scene/ is the one source: live-dag.js, proof-core.js, tokens.css (the fourteen palette tokens, the brand package's values, dark and light), feed-contract.md and .json (one JSON shape for the observer's /api/live and the app's api/live), a recorded reply as the fixture. tools/scene/sync.mjs writes the copies and the scene-tokens block into site.css and app.css; --check is the gate line (byte-equal scripts, an equal block, the names defined nowhere else, a print block excepted), --self-test fails five known cases first. The site's token definitions move out of the package's :root line into the block; no value changes on the site. tools/scene/feed-contract.mjs validates a reply against the key lists; its test refuses a miner rewritten to "you", a float now, a stray key. Three new lines in tools/ci/pre-push.sh. The app side (branch scene-parity, for 0.3.21) takes the same folder. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit f77435346179a232b7d1bcf6c8efd08c3bce454f)
4.8 KiB
The live feed contract
One JSON shape. The chain scene (scene/live-dag.js, scene/proof-core.js) reads it, and two producers write it:
| Producer | Where | Source of the rows |
|---|---|---|
| The observer | site/api/live.mjs (GET /api/live?window=N, igneum.network) |
Neon tables the observer (tools/observer/observer.mjs) fills from node 1 |
| The miner app | app/igneum-app/src/live.rs (GET api/live?window=N on the engine's port) |
The observer's reply, passed through; when the observer is unreachable, the local node's igneum_getRecentBlocks shaped into the same rows (partial: true) |
The machine-readable key lists live in scene/feed-contract.json. tools/scene/feed-contract.mjs validates a reply against them
(the gate runs it over the recorded fixture and over known-bad shapes); the app's Rust test reads the same file, so neither side
can add a field or drop one without the other noticing.
Top level
| Key | Type | Meaning |
|---|---|---|
ok |
true | A reply with ok: false carries error and nothing else |
now |
ISO 8601 string | The producer's clock when the reply was built |
state |
object | The network state, below |
blocks |
array | The window's blocks, oldest first, at most 1,800 |
miners |
array | Vote keys seen in the last 10 minutes |
events |
array | The last 30 observer events |
finality |
object | Checkpoints and weights |
proving |
object | The proving layer |
partial |
true, app only | The rows came from the local node: no parents, no proof state, no checkpoints |
state
stale, age_s, network, node_version, height (the chain block number), block_count, header_count, blue_score,
difficulty, hashes_per_second_estimate, peers, mempool, blocks_60s, blocks_per_second_60s, blocks_per_minute
(10 numbers, oldest first), miners_10m, observer_started_at, observer_lag_s, queue_depth, updated_at. A value the
producer does not know is null, never missing.
App extensions, additive and ignored by the scene: source (observer or node), you_blocks (this machine's blocks in
the window), node (the local node's own numbers for the Cards tab).
A block
| Key | Type | Meaning |
|---|---|---|
hash |
string, the first 16 hex | Both producers cut the hash the same way, so a hash reads the same everywhere |
number |
number or null | The chain block number once planned; null off the chain or before the plan |
blue_score, daa |
number or null | |
ts |
number | Header time in milliseconds |
rx |
number | When the producer first saw the block, milliseconds |
parents |
array of hash strings | Empty on a partial reply |
chain |
boolean | On the selected chain |
miner |
string, 8 hex | The vote key id. NEVER a word: the app marks its own blocks through the scene's mine option, not by rewriting this field |
color |
blue, red or pending |
GHOSTDAG inclusion |
locked |
boolean | A locked checkpoint block |
final |
boolean | At or before the newest lock |
shards |
array of {i, n, state, prover, lag, payout, pgas} |
state is planned, proving, verified or paid |
proven |
boolean | Every shard verified or paid |
miners[], events[], finality, proving
A miner is {id (8 hex), blocks, share (percent of the 10 minutes), last_seen (ISO or null), engine (string or null)}.
An event is {ts, kind, text}.
finality is {supported, active, next_index, latest_locked_index, latest_locked_hash, latest_locked_blue_score, params, weights, checkpoints}; a checkpoint is {index, hash, blue_score, daa, state (pending or locked), signed, active, total, fraction_active, fraction_total, votes, voters, locked_at, updated_at}.
proving is {supported: false, reason} or the supported object (active, activation_daa, tip_daa, verifier, pool,
paid_shards_total, shard_budget_pgas, blocks_10m, blocks_fully_proven_10m, shards_proven_10m, shards_paid_10m,
median_proof_lag_s, provers_10m).
What the scene does with it
IgneumDag.mount(canvas, opts).push(reply) normalises every block (unknown colours become pending, unknown shard states
unknown, a hash over 256 characters or a non-numeric ts drops the row), keeps the newest 340 s, and paints at once.
The window the scene shows (opts.window, 30 to 300 s) is independent of the window the page asks the producer for: both the
site and the app ask for 300 s so the viewer can pan and zoom out to the full range (?window=300).
The recorded fixture
scene/fixtures/live-2026-10-07.json is one real /api/live?window=300 reply (7 October 2026, 13:17 UTC, 905 blocks, 15
miners, 40 checkpoints, 14 proven blocks, 50 excluded). The contract test validates it; the parity test renders it through both
surfaces; the paint test pushes it into a hidden document.