feat: section indexes, so the routes can be found
deploy / build (push) Successful in 7m33s
deploy / deploy-web (push) Successful in 5s
deploy / deploy-api (push) Successful in 15s

Every route added lately was reachable only by already knowing an address — a
block hash, a preimage, an SS58 string, a spec_version. Fine for a link somebody
sent you, useless for discovering the pages exist. A kind with nothing after it
is now the index of that kind, and a nav under the chain chips names them.

Not the same page three times. Blocks is the record behind the live ticker,
paged by height. Accounts ranks by tokens earned over the whole indexed record,
which is a different question from the standings' blocks-per-window and here a
different answer: difficulty rose 346-fold inside mainnet's first day, so an
early block cost a three-hundredth of a current one. Runtimes is the only list
with no other home — a block does not name its runtime and neither do the
standings.

No `/:chain/miner`: the standings already are the index of miners, and a second
page of the same rows under another address would be two answers to one
question. That path redirects there, along with anything else that resolves to
the front page without being spelled like it, so the address bar and the section
nav agree about where the reader is.

Two things caught by looking at the rendered pages:

The block index's gaps came from `observed_at` and read *three milliseconds*
between blocks on a chain targeting twelve seconds — exactly the trap CLAUDE.md
records, since a gap fill writes a whole batch within one second. They come from
`authored_at` now, the chain's own clock.

The first page of that index came from the live ticker, which is ordered
oldest-first because that is the order it is pushed in, so page one climbed and
every page after it descended. The index sends a cursor for its first page too.

Accounts shows the address beside any node name: one node legitimately reports
for several payout addresses, and the first cut had two rows reading
`pearl-prover` with nothing to tell them apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
This commit is contained in:
2026-09-09 18:07:11 +03:00
co-authored by Claude Opus 5
parent 289a80a7eb
commit efbe901646
16 changed files with 1082 additions and 31 deletions
+56
View File
@@ -0,0 +1,56 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AttributionSource } from "./AttributionSource";
import type { BigUintDec } from "./BigUintDec";
import type { MinerId } from "./MinerId";
/**
* One row of the accounts index: an account and what it has earned.
*/
export type AccountRow = {
/**
* Position in the ranking, 1-based.
*/
rank: number,
/**
* The SS58 address, which is what a reader recognises and what the page
* for it is addressed by.
*/
address: string,
/**
* The raw account id, `0x` hex.
*/
account: string,
/**
* The reward preimage behind it, when the observer recorded a block it was
* paid for. Absent for an account whose blocks all predate this observer.
*/
miner: MinerId | null,
/**
* A telemetry name for that miner, if one is held.
*/
display: string | null,
/**
* Where `display` came from. Carried for the same reason the leaderboard
* carries it: a name here is an inference from which node reported a block
* first, and a row that renders a guess identically to a settled name is
* making a claim the observer cannot support.
*/
attribution: AttributionSource,
/**
* Agreement behind the held name, 0.0–1.0. Meaningless when `attribution`
* is `Preimage`.
*/
confidence: number,
/**
* How many votes are behind it. A fraction alone cannot tell one vote of
* one from nine of nine, and those are not the same claim.
*/
attribution_votes: number,
/**
* How many blocks it was paid for.
*/
blocks: number,
/**
* Their sum, in the chain's smallest unit.
*/
total: BigUintDec, };
+23
View File
@@ -7,9 +7,11 @@
*/
import type { AccountDetail } from './generated/AccountDetail'
import type { AccountRow } from './generated/AccountRow'
import type { ApiError } from './generated/ApiError'
import type { BlockDetail } from './generated/BlockDetail'
import type { ChainSeries } from './generated/ChainSeries'
import type { RecentBlock } from './generated/RecentBlock'
import type { MinerDetail } from './generated/MinerDetail'
import type { RuntimeDetail } from './generated/RuntimeDetail'
import type { RuntimeSummary } from './generated/RuntimeSummary'
@@ -102,6 +104,27 @@ export function fetchAccount(
)
}
/** Accounts ranked by what they have been paid, most first. */
export function fetchAccounts(chain: string, signal?: AbortSignal): Promise<AccountRow[]> {
return get<AccountRow[]>(`/chains/${encodeURIComponent(chain)}/accounts`, signal)
}
/**
* A page of block history, newest first.
*
* Without `before` this is the live ticker out of the API's memory, which is
* what the front of the list is; with it, the record. Same question, and the
* boundary is whether the caller is at the front.
*/
export function fetchBlockPage(
chain: string,
before?: number,
signal?: AbortSignal,
): Promise<RecentBlock[]> {
const cursor = before === undefined ? '' : `?before=${before}`
return get<RecentBlock[]>(`/chains/${encodeURIComponent(chain)}/blocks${cursor}`, signal)
}
/** Every runtime this chain has been seen running, oldest first. */
export function fetchRuntimes(chain: string, signal?: AbortSignal): Promise<RuntimeSummary[]> {
return get<RuntimeSummary[]>(`/chains/${encodeURIComponent(chain)}/runtimes`, signal)