9cbc2cc274e719e03d592001eee002c880a7a7c0
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9cbc2cc274
|
ui: put the miner count before the height tile
Height is the one tile with no sparkline — it only goes up, and a straight diagonal reports nothing — so it sat in the middle of the row with an empty slot under it, splitting the four lines into two and two. Last, it stops interrupting them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5 |
||
|
|
7e5c2de936
|
feat: sparkline history under the headline tiles
Adds `GET /v1/chains/{chain}/series` and a sparkline under network hashrate,
difficulty, block time and miners. Height gets none — it only goes up, and a
straight diagonal reports nothing.
The chart is bucketed by height, not by time: every point is the same number of
blocks, so every point carries equal statistical weight and a stretch nobody
recorded stays the same width on the axis as one that was. A bucket is the
window over fifty, which makes a full window exactly fifty buckets — that is
what lets the rolling miner count at the last point be the same number printed
in the tile above it, rather than a per-bucket count that would sit well below
it. Buckets are anchored to absolute height for the same reason `miner_series`
bins from the epoch: anchored to the tip instead, every boundary slides by one
on every block and the chart shifts under a reader watching it.
Interval comes from `authored_at`, never `observed_at`. A gap fill writes a
whole batch of observation times within the same second, so deltas taken from
it read as a chain producing hundreds of blocks a second — the trap
`RollingWindow`'s `at_tip` flag exists to avoid, which until now lived only in
memory. It is a column as of `0002`, so the distinction survives a restart and
`observed_at` is interpretable at all.
Each point's interval is a three-bucket moving average. The mean of one
bucket's gaps still carries real Poisson error — 72 blocks of a 15 s interval
lands at ±1.8 s — and across fifty points that draws a chain that appears to
change size every few minutes and does not.
Fixes a wrong number found on the way in: `pallet_qpow` retargets in
`on_finalize`, so `QPoWApi_get_difficulty` at the latest state is the
difficulty for the *next* block, not the one just seen. Every stored difficulty
was off by one block — invisible, because a gradual retarget still looks
plausible. It is now asked at `header.parent_hash`, the same call the miner
made when it built the block. A node that has pruned that state gets `None`
rather than the current value: substituting it is exactly how a gap fill would
stamp today's difficulty across a stretch of old heights. The CLI backfill had
the same bug at greater scale, copying one reading across an entire range.
Unknowns are drawn as breaks in the line, never interpolated: an outage, a
difficulty a pruned node cannot supply, a point without a full window of
authors behind it. A chart that guesses across what was never recorded is the
same class of lie as a hashrate divided by a sync speed, and just as hard to
catch afterwards.
Read against the dataviz skill first, per CLAUDE.md. Single series, one hue;
emphasis on the newest point is `--data-bright`, a lighter step of the same
bronze, because the usual "current period in the accent" convention is the one
change that would break this palette. The hover readout borrows each tile's
note line — five floating tooltips across a row of 180px tiles is a pile, not a
hover layer — and the same readout is on keyboard focus and in the aria-label,
since the line does not start at zero and its height is not a quantity anyone
should be estimating.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
|
||
|
|
b552b08b10
|
docs: record the mesh-bind, hairpin and runner-label gotchas
Each of these failed a CI run this session and none of them looks like what it is: a loopback health probe refused by a healthy daemon, a proxy that cannot resolve its own public name, and 'cargo: command not found' on a node runner. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi |
||
|
|
3db86cfc66
|
fix(ci): health-probe the address the API actually binds, not loopback
The probe curled 127.0.0.1:25864 and got connection-refused against a daemon that was running perfectly — the journal it captured on failure showed it serving. The API binds the host's *mesh* address by design: the fleet has one firewalld default zone, so a wildcard bind plus the named service would publish it on every address the host carries, and the edge proxy is at another site, so loopback-only would not serve the site at all. `hostname -f` resolves to that mesh address, so the probe needs no value carried between steps. Verified against the running service: loopback refuses, the FQDN returns 200. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi |
||
|
|
151a30ae1a
|
ci: run the build on the rust runner, drop corepack, pin the site check to loopback
Three things that would each have failed the run seconds in (architecture/gitea-runners.md §4): - `runs-on: fedora-43` for a cargo build is `cargo: command not found` — the fedora images are node/shell runners with no Rust. `rust` is built on `runner-fedora-44`, so it carries node + pnpm as well and one job still builds both halves. musl stays: that runner is Fedora 44 and the targets are 43. - `corepack enable` is not bundled by any image and pnpm is already on PATH. - The site check curled the public name *from the edge proxy*, where it resolves to the site WAN address and dead-ends on the OPNsense LAN interface. Pinned to loopback, which still exercises the stream router, SNI, the vhost, the cross-site hop and the API. Deploy jobs stay on `infra`: §4 is about tooling, not routing, and these targets are mesh-only .internal names — same reasoning as lair/quantus and lair/mail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi |
||
|
|
36a10ac691
|
fix(api): allow every configured CORS origin, not just the last
`CorsLayer::allow_origin` replaces rather than appends, so folding over the configured list left only `blackbeard.internal` allowed and silently refused `blackbeard.observer` — the site's own public origin. It broke nothing, because the frontend is served same-origin and never consults CORS, which is precisely why it would have gone unnoticed until something else called the API. Verified on the deployed vhost: both configured origins are now echoed back and an unlisted one is refused. Also records the deployment gotchas this session turned up (exact-argument sudoers matching, the runas spec for the config check, the cross-site hop the loopback probe cannot see, and why a WebSocket upgrade test needs --http1.1). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi |
||
|
|
6a7253956a
|
fix(infra-setup): escape backticks in the closing message; report db verification honestly
The closing heredoc interpolates $EDGE_HOST, so backticks in its prose ran as
command substitution ('create: command not found').
A missing psql on the app host was reported as an authentication failure, which
sends you looking at the CN mapping instead of at a package that has no reason
to be installed. An unreachable standby is now a loud, specific warning: pg_ident
is not replicated, so a failover to a standby that never got the mapping locks
the app out during an outage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi
|
||
|
|
5f3aac2a13
|
feat(chains): discover every Quantus chain from telemetry; track the ones we can reach
The chain list is no longer config. substrate-telemetry announces every chain it knows — with live node counts — before and independent of any subscription, so one feed connection that never subscribes is a complete, self-maintaining index. A chain that launches tomorrow appears in the nav on its own. Config now contributes only RPC endpoints. Authorship lives in the `pow_` digest of a block *header*, and telemetry publishes hashes and heights but never headers — so a chain with no endpoint is listed with its node count and is not navigable, and the nav says why rather than offering an empty board. The two sets join on genesis hash, never on name. Two chains are tracked in full: Planck (our own node on bob) and Heisenberg (a1-heisenberg.quantus.cat, found via Quantus-Network/quantus-apps). Both decode identically — same digest shape, same QPoW runtime API — which is the proof that adding a chain is one [[chains]] entry and no new code. Quantus Staging Mainnet and Quantus Dirac Testnet are listed and disabled: no endpoint exists under the a1-/a2- pattern and none resolves under any plausible name. Nav is ordered by node count descending, ties broken on id so it cannot reshuffle between refreshes. Telemetry now also yields chain-wide best height, best finalized height, average block time and the client-version histogram (feed codes 1, 2, 12 and 22). Infra moves to a single site: oolon fronts blackbeard.observer, reverse-proxying cross-site over the mesh to the API beside the node on bob. That hop is the one the loopback health probe cannot see, so the deploy now checks it explicitly — a firewalld service scoped to bob's own /16 would leave a live site with a dead /v1 and nothing would fail. infra-setup gains `dns` and `cert` roles that run on oolon with the credential already there: the Cloudflare token is never copied off the proxy, and it refuses to repoint an apex record that already exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi |
||
|
|
110fbc3631
|
feat: blackbeard.observer — live Quantus mining leaderboard
Cargo workspace plus a Vite frontend, following ~/git/architecture/generic.md. Every block header carries its author's wormhole reward preimage in a `pow_` PreRuntime digest, so authorship for the whole network is derivable from headers alone — no indexer, no registration, no way for a miner to be left out. That decoding, the hashrate maths and the telemetry name attribution live in blackbeard-core with no I/O at all, so the parts that are easy to get subtly wrong are exercised by unit tests rather than only against a live chain. The browser holds one WebSocket: snapshot on subscribe, deltas thereafter. The head stream is itself a push (chain_subscribeNewHeads), so a block reaches the page the moment the node imports it. Messages are serialised once per broadcast, and leaderboards are recomputed only for windows a socket is actually watching. No RxJS — useSyncExternalStore is React's own contract for this. Verified against the live Planck testnet: 12/12 headers decoded, telemetry names attributed (quanpool-planck, baba-gorchitsa, …), warm start restoring 84 blocks and 5 held names across a restart. Three findings worth recording, all in CLAUDE.md: - substrate-telemetry sends its JSON in *binary* frames. A text-only client connects, subscribes, reports healthy and receives nothing at all — and a Python probe hides it, because json.loads accepts bytes. - Difficulty is a little-endian U512; decoding it big-endian gives a number wrong by ~10^150 that still renders fine. - Planck's real block interval is ~13-15s against a 6s target with enormous variance, so a measured interval needs 20 tip samples before it is publishable. Deploy assets, the Gitea Actions workflow and script/infra-setup.sh are included; port 25864 is registered in architecture/port-allocations.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi |