feat: a nodes index, so a node need not have mined to be visible
deploy / build (push) Successful in 7m23s
deploy / deploy-web (push) Successful in 6s
deploy / deploy-api (push) Successful in 16s

Reported: `baba-gorchitsa` shows its hardware on telemetry.quantus.com and
nothing here. Not a decoding failure — we held the data. Measured, the same node
on both chains at the same moment:

    quantus  attribution=carried, 0 votes, 33 blocks   on the feed: yes
    planck   attribution=telemetry, 1 vote, 2969 blocks on the feed: no

Exactly inverted. On mainnet we had the metadata and no join to it; on Planck
the join and no node. The mainnet name is *carried* from Planck, and
`Attribution::peer_id` is `None` for anything but a telemetry attribution
because there is no node behind a carried name — that reasoning stands. The
mistake was upstream: node metadata was reachable *only* through the mining
inference, and the feed hands us every node unconditionally. 288 on mainnet, of
which a handful have ever won a block.

So `/:chain/node` lists all of them and `/:chain/node/:peer` is one, neither
gated on mining. The index merges the live feed over the cache, so a node not
re-announced since our last restart still appears, faint, with its peer count
absent rather than stale — a peer count from an hour ago is not a peer count.
Live rows sort first, then by name, and the filter matches name, peer id, CPU,
version or country, because "find my node" is the question the page exists for.

The miner panel now links to the node's own page instead of being the only way
to see it.

`reported` beside the count is the feed's own `AddedChain` figure and will not
match in either direction — 284 against 288 listed, measured, because it is a
snapshot the feed refreshes on its own cadence. Shown next to our count rather
than instead of it, since the difference is a real fact about how each was
arrived at. My first comment claimed it was "usually larger"; it was smaller the
first time I looked at it.

One trap caught in the browser rather than by the type checker: "is this the
standings?" existed three times, and adding a section updated two. The redirect
in `App` judged `/quantus/node/<peer>` to be the standings and replaced it with
`/quantus/3600-blocks` — route parsed, page existed, address bar threw it away.
It is `isStandings` now, in one place.

Closes #16

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
This commit is contained in:
2026-09-13 19:19:58 +03:00
co-authored by Claude Opus 5
parent e7cfe42881
commit fac73f392c
17 changed files with 763 additions and 33 deletions
+1
View File
@@ -189,6 +189,7 @@ export function MinerPanel({
confidence={row?.confidence ?? null}
votes={row?.attribution_votes ?? null}
attribution={row?.attribution ?? null}
chain={chain}
/>
)}
</>
+14 -1
View File
@@ -18,7 +18,10 @@
import type { NodeInfo } from '../api/generated/NodeInfo'
import type { AttributionSource } from '../api/generated/AttributionSource'
import { Link } from 'react-router-dom'
import { ago, bandwidth, bytes, since } from '../lib/format'
import { href } from '../lib/routes'
/** A two-letter country code as its flag, by offsetting into the regional
* indicator block. Returns null for anything that is not two letters, so a
@@ -53,11 +56,15 @@ export function NodePanel({
confidence,
votes,
attribution,
chain,
}: {
node: NodeInfo
confidence: number | null
votes: number | null
attribution: AttributionSource | null
/** Set where the node's own page is somewhere else to go. Omitted on that
* page, where the heading would link to itself. */
chain?: string | undefined
}) {
const os = [node.target_os, node.target_arch, node.target_env].filter(Boolean).join(' · ')
const country = node.country_code
@@ -78,7 +85,13 @@ export function NodePanel({
<div>
<div className="eyebrow">Reported by</div>
<h2 className="panel-title" style={{ marginTop: 4, textTransform: 'none' }}>
{node.name}
{chain ? (
// Only when this panel sits under something else. On the node's
// own page the heading would link to where the reader already is.
<Link to={href({ chain, node: node.peer_id })}>{node.name}</Link>
) : (
node.name
)}
</h2>
</div>
{strength && (
+53
View File
@@ -0,0 +1,53 @@
/**
* One node, reached directly rather than through a miner.
*
* Renders the same panel the miner route does, so a node reached by its peer id
* and a node reached through the standings read identically — but with no
* attribution beside it, because there is none: nobody inferred anything to get
* here, the address *is* the node.
*/
import { useEffect, useState } from 'react'
import type { NodeInfo } from '../api/generated/NodeInfo'
import { RequestFailed, fetchNode } from '../api/rest'
import { NodePanel } from './NodePanel'
export function NodeRoute({ chain, peer }: { chain: string; peer: string }) {
const [node, setNode] = useState<NodeInfo | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const controller = new AbortController()
setNode(null)
setError(null)
fetchNode(chain, peer, controller.signal)
.then(setNode)
.catch((e: unknown) => {
if (controller.signal.aborted) return
setError(e instanceof RequestFailed ? e.message : 'Could not reach the observer.')
})
return () => controller.abort()
}, [chain, peer])
if (error) {
return (
<section className="panel" style={{ marginBottom: 26 }}>
<div className="panel-head">
<div>
<div className="eyebrow">Node</div>
<h2 className="panel-title" style={{ marginTop: 4, textTransform: 'none' }}>
<code>{peer}</code>
</h2>
</div>
</div>
<p className="empty">{error}</p>
</section>
)
}
if (!node) return <p className="empty">Asking telemetry…</p>
// No confidence, no votes, no attribution: this page was reached by naming
// the node, so there is nothing inferred to qualify.
return <NodePanel node={node} confidence={null} votes={null} attribution={null} />
}
+161
View File
@@ -0,0 +1,161 @@
/**
* Every node the chain's telemetry feed reports.
*
* The point of this page is that it has no entry condition. A node reaches the
* miner route only through the first-reporter voting — it has to have won
* blocks *and* had the feed resolve a clean lead on enough of them — and most
* nodes never mine at all. So the one door we had admitted almost nobody, while
* the feed was handing us every node on the chain unasked.
*
* Live rows first. A node the feed is reporting right now is a different claim
* from one we last heard about at some point in the past, and the `live` column
* says which without the reader having to work it out from a timestamp.
*/
import { useEffect, useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
import type { NodeIndex } from '../api/generated/NodeIndex'
import { RequestFailed, fetchNodes } from '../api/rest'
import { ago, bytes, height as fmtHeight } from '../lib/format'
import { href } from '../lib/routes'
/** A two-letter country code as its flag. Null for anything else, so a
* malformed code renders as nothing rather than as tofu. */
function flag(code: string | null): string | null {
if (!code || !/^[A-Za-z]{2}$/.test(code)) return null
const base = 0x1f1e6 - 'A'.charCodeAt(0)
return String.fromCodePoint(...[...code.toUpperCase()].map((c) => base + c.charCodeAt(0)))
}
export function NodesIndex({ chain }: { chain: string }) {
const [index, setIndex] = useState<NodeIndex | null>(null)
const [error, setError] = useState<string | null>(null)
const [filter, setFilter] = useState('')
useEffect(() => {
const controller = new AbortController()
setIndex(null)
setError(null)
fetchNodes(chain, controller.signal)
.then(setIndex)
.catch((e: unknown) => {
if (controller.signal.aborted) return
setError(e instanceof RequestFailed ? e.message : 'Could not reach the observer.')
})
return () => controller.abort()
}, [chain])
// Name, peer id and CPU all together: "find my node" is the question this
// page exists to answer, and a reader may remember any of the three.
const rows = useMemo(() => {
const needle = filter.trim().toLowerCase()
const all = index?.nodes ?? []
if (!needle) return all
return all.filter((n) =>
[n.name, n.peer_id, n.cpu, n.version, n.country_code]
.filter(Boolean)
.some((field) => field!.toLowerCase().includes(needle)),
)
}, [index, filter])
const liveCount = index?.nodes.filter((n) => n.live).length ?? 0
return (
<section className="panel" style={{ marginBottom: 26 }}>
<div className="panel-head">
<h2 className="panel-title">Nodes</h2>
{index && (
<span className="eyebrow">
{liveCount} reporting now
{index.reported !== null && ` · ${fmtHeight(index.reported)} on telemetry`}
</span>
)}
</div>
{error && <p className="empty">{error}</p>}
{!error && index === null && <p className="empty">Asking telemetry…</p>}
{index !== null && (
<>
<div className="pager" style={{ borderTop: 0, borderBottom: 'var(--rule)' }}>
<input
className="node-filter"
type="search"
value={filter}
placeholder="Filter by name, peer id, CPU, version or country"
aria-label="Filter nodes"
onChange={(e) => setFilter(e.target.value)}
/>
{filter && (
<span className="eyebrow">
{rows.length} of {index.nodes.length}
</span>
)}
</div>
<div className="scroll-x">
<table className="board">
<caption className="visually-hidden">
Every node this chain&apos;s telemetry feed reports
</caption>
<thead>
<tr>
<th scope="col" className="left">
Node
</th>
<th scope="col">Country</th>
<th scope="col" className="left">
Version
</th>
<th scope="col" className="left">
CPU
</th>
<th scope="col">Cores</th>
<th scope="col">Memory</th>
<th scope="col">Peers</th>
<th scope="col" className="event-when">
Heard
</th>
</tr>
</thead>
<tbody>
{rows.map((n) => (
<tr key={n.peer_id} className={n.live ? undefined : 'call-unused'}>
<td className="left">
<div className="call-cell">
<Link to={href({ chain, node: n.peer_id })}>{n.name}</Link>
<span className="runtime-type">{n.peer_id}</span>
</div>
</td>
<td title={n.country_code ?? 'not located'}>
{n.country_code ? `${flag(n.country_code) ?? ''} ${n.country_code}` : '—'}
</td>
<td className="left numeral">{n.version ?? '—'}</td>
<td className="left">{n.cpu ?? '—'}</td>
<td className="numeral">{n.core_count ?? '—'}</td>
<td className="numeral">{n.memory !== null ? bytes(n.memory) : '—'}</td>
<td className="numeral">{n.peer_count ?? '—'}</td>
<td className="numeral event-when" style={{ color: 'var(--text-muted)' }}>
{n.live ? 'now' : n.seen_at ? ago(n.seen_at) : '—'}
</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="panel-note">
Everything here is what each node reports about itself on the telemetry feed — never
verified, and never a claim the chain makes. A node is listed whether or not it has ever
won a block, which is the difference between this page and the standings. Rows shown
faintly are from this observer&apos;s cache: the feed is not reporting them right now,
so their peer counts are absent rather than stale. Location is resolved to a country and
no finer, because the feed&apos;s coordinates come from IP geolocation and are
unreliable at city resolution.
</p>
</>
)}
</section>
)
}
+4 -10
View File
@@ -15,7 +15,7 @@
import { Link } from 'react-router-dom'
import type { Window as WindowName } from '../api/generated/Window'
import { href, type Route, SECTIONS } from '../lib/routes'
import { href, isStandings, type Route, SECTIONS } from '../lib/routes'
export function SectionNav({
chain,
@@ -30,14 +30,7 @@ export function SectionNav({
}) {
if (!chain) return null
// The standings are current when nothing else is: no panel open, no index.
const onStandings =
route.index === null &&
route.block === null &&
route.miner === null &&
route.account === null &&
route.runtime === null &&
route.call === null &&
route.event === null
const onStandings = isStandings(route)
return (
<nav className="sections" aria-label="Section">
@@ -69,7 +62,8 @@ export function SectionNav({
aria-current={
route.index === s.id ||
(s.id === 'call' && route.call !== null) ||
(s.id === 'event' && route.event !== null)
(s.id === 'event' && route.event !== null) ||
(s.id === 'node' && route.node !== null)
? 'page'
: undefined
}