`Quantus` and `Heisenberg` sat side by side, identically styled. One is the real
network. Somebody reading a balance, a reward or a transfer had no way to tell
from the chrome whether the number meant anything, and Planck makes it two
testnets against one mainnet.
Two labelled groups rather than a styling difference alone, because a lone
visual difference reads as decoration to anyone who has not been told what it
means:
MAINNET [ Quantus 329 ] TESTNETS [ Planck 110 ] [ Heisenberg 4 ]
The dashed border on a testnet chip is a second channel behind the heading
rather than the only one, and every chip's tooltip now says outright that a
testnet's balances and rewards are not real.
No backend work: `ChainInfo.mainnet` has been on the wire since the registry was
written and the frontend simply ignored it. It stays operator-asserted and is
never inferred from the name — a chain publishing itself as "Quantus Staging
Mainnet" must not collect the heading by having the word in its title.
A chain list with no mainnet in it renders one `TESTNETS` group rather than an
empty `MAINNET` heading.
Closes#5
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
The nav listed every chain telemetry names, with the ones we hold no RPC
endpoint for as disabled chips saying "no endpoint". The reasoning was that
they are part of the network and hiding them misrepresents its shape. In
practice it put five dead chips above the standings on every page — four of
which have never had anything behind them — and made the one live chain the
smallest thing in the row.
Nothing is blocked. `/:chain/...` still resolves for any chain the backend
knows, `/v1/chains` still reports every one of them with its `tracking`, and
promoting one is still a single `[[chains]]` entry. This is only about what the
page offers unprompted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
The hash router was the right size for a chain and a window. It stopped being
once a block, a miner and — shortly — an account each became a page worth
sending someone: a fragment never reaches a server, so those URLs cannot carry a
title, be crawled, or be resolved by anything but the page itself.
So paths, via react-router, which was already a dependency and unused. Both
vhosts already answer any path with `index.html`, so nothing outside the client
changed. `lib/routes` holds the grammar in one place and `fromLegacyHash`
rewrites the old form on load, so every `#/…` link already published still lands
where it meant to.
Segments are named — `/quantus/block/13160`, not `/quantus/13160` — rather than
told apart by shape. Shape works until two kinds of thing can look alike, and an
SS58 address is arbitrary base58 with no rule keeping it clear of a window name
forever. Six characters buys the whole class of problem away before accounts
arrive.
Everything navigable is an anchor now, chain chips and the window control
included, which is what "real URLs" has to mean in practice: middle-click opens
another chain's 24h view in a tab, and the back button walks the panels. The
miner panel had no address at all before this — opening one changed the page and
not the URL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
`cargo fmt --check` has gated the Rust half since the first commit; nothing
gated the TypeScript, so 25 of 31 files under web/src had drifted from the
.prettierrc that was already sitting in the repo. `pnpm format:check` is now the
counterpart, in the same step as `pnpm lint`.
The reformat is whitespace only — the emitted bundle hashes identically before
and after.
web/.prettierignore excludes src/api/generated. Those files are written by ts-rs
from the entities crate and CI already fails on any diff in them after `cargo
test`; formatting them here would be reverted by the next regeneration and turn
that drift gate red on every subsequent build. Verified by regenerating and
confirming the tree stays clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
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