feat: an about route, and somewhere to say thanks
deploy / build (push) Successful in 8m29s
deploy / deploy-web (push) Successful in 5s
deploy / deploy-api (push) Successful in 15s

Adds `/:chain/about`, linked from the header just before the connection light
and the theme toggle — it is about the site rather than the chain, so it sits
there instead of in the section nav, and `header: true` on its `SECTIONS` entry
keeps it out while still letting the URL parse.

The page says what the observer is, that it and the CUDA-optimised miner are
built and freely sourced by a member of the Quantus user community rather than
by the project, and takes donations in QTC.

**The QR is generated in the browser from the address printed beside it, never
shipped as an image.** A supplied PNG next to a typed address is two
independent claims about where money goes, and nothing on the page could tell a
reader if they ever stopped agreeing — a mismatch sends funds somewhere
unrecoverable.

Verified rather than trusted. The address was put through this crate's own SS58
decoder: checksum and network prefix both hold, and it round-trips to the same
string. The QR matrix was then decoded from first principles — function-pattern
map, mask 3 unapplied, codewords de-interleaved across version 4-M's two blocks
— and reads back the address exactly. A note for whoever checks this again: an
independent encoder produces a different matrix for the same string at every
mask, because mode segmentation differs, so comparing matrices proves nothing
and only a decode does.

`the_donation_address_on_the_site_is_a_valid_quantus_address` reads the literal
out of the frontend source and validates it, so a typo fails CI rather than
costing somebody their money. Confirmed to fail on one changed character.

Also on the vesting page, from verifying it live: a "claimable now" figure and
a per-grant claimable column. Two grants began vesting at launch while the other
forty-six wait for 2027, so 68,871 QTC is already claimable and none of it has
been taken — a page that only said "first claimable" would have missed it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 16:48:21 +03:00
co-authored by Claude Opus 5
parent 2bbe8e4d5d
commit 6ed9a6c35a
10 changed files with 316 additions and 2 deletions
+14
View File
@@ -33,6 +33,7 @@ import { SectionNav } from './components/SectionNav'
import { ThemeToggle } from './components/ThemeToggle'
import { StateIndex } from './components/StateIndex'
import { StatBar } from './components/StatBar'
import { AboutPanel } from './components/AboutPanel'
import { VestingPanel } from './components/VestingPanel'
import { WormholeStage } from './components/WormholeStage'
import { measuredSpan, seconds, windowSpan } from './lib/format'
@@ -154,6 +155,18 @@ export default function App() {
</nav>
)}
{/* Before the connection light and the toggle, as the last thing in
the header that is about the site rather than about the chain. */}
{chain && (
<Link
to={href({ chain, index: 'about' })}
className="about-link"
aria-current={route.index === 'about' ? 'page' : undefined}
>
About
</Link>
)}
<div className={`status status-${state.connection}`}>
<span className="status-dot" />
{state.connection}
@@ -285,6 +298,7 @@ export default function App() {
<RuntimesIndex chain={chain} current={state.summary?.spec_version ?? null} />
)}
{route.index === 'node' && chain && <NodesIndex chain={chain} />}
{route.index === 'about' && <AboutPanel />}
{route.index === 'vesting' && chain && (
<VestingPanel
chain={chain}
+122
View File
@@ -0,0 +1,122 @@
/**
* Who made this, and how to say thanks.
*
* The QR is **generated in the browser from the same string the page prints**,
* never shipped as an image. A supplied PNG and the text beneath it are two
* independent claims about where money should go, and nothing on the page could
* tell you if they ever stopped agreeing — a mismatch sends funds somewhere
* unrecoverable. Generated, they cannot disagree: there is one address, and the
* code is a rendering of it.
*/
import qrcode from 'qrcode-generator'
/** Where donations go. Also the string the QR is built from — see above. */
export const DONATION_ADDRESS = 'qzkQq9ybiPm7gQR9E1ViQbW9RhqA2Rex9ZQkScU5zdJqjrre4'
const MINER_REPO = 'https://git.lair.cafe/quantus/miner'
const OBSERVER_REPO = 'https://git.lair.cafe/quantus/blackbeard.observer'
/**
* The address as a QR, drawn as one SVG path.
*
* Error-correction level `M` and an automatic version: the address is 49
* characters, which fits comfortably and leaves the code readable at the size
* a phone is held from a screen. Rendered as a single path of rectangles rather
* than a grid of elements — one node instead of a thousand, and it scales to
* whatever box the layout gives it.
*/
function AddressQr({ value }: { value: string }) {
const qr = qrcode(0, 'M')
qr.addData(value)
qr.make()
const count = qr.getModuleCount()
// A four-module quiet zone, which the spec requires and scanners rely on.
const quiet = 4
const size = count + quiet * 2
let path = ''
for (let row = 0; row < count; row += 1) {
for (let col = 0; col < count; col += 1) {
if (qr.isDark(row, col)) {
path += `M${col + quiet} ${row + quiet}h1v1h-1z`
}
}
}
return (
<svg
className="qr"
viewBox={`0 0 ${size} ${size}`}
role="img"
aria-label={`QR code for the donation address ${value}`}
>
{/* The light modules are drawn, not left transparent: a QR on a dark
surface with a transparent background does not scan, and this site has
a dark theme. */}
<rect x="0" y="0" width={size} height={size} className="qr-light" />
<path d={path} className="qr-dark" />
</svg>
)
}
export function AboutPanel() {
return (
<>
<section className="panel" style={{ marginBottom: 26 }}>
<div className="panel-head">
<h2 className="panel-title">About</h2>
<span className="eyebrow">who made this</span>
</div>
<div className="about-prose">
<p>
<strong>blackbeard.observer</strong> is a mining leaderboard and block explorer for the
Quantus network. It is built and its source provided freely by a member of the Quantus
user community — not by the Quantus project, and with no standing behind it beyond the
code being open and the numbers being checkable.
</p>
<p>
The same author maintains a{' '}
<a href={MINER_REPO} rel="noreferrer">
CUDA performance-optimised miner
</a>
, also free and open source. Between them they are the reason a good deal of this site
exists: a miner needs somewhere to see whether it is winning.
</p>
<p>
Everything the observer shows is derived from public block headers, chain state and the
network&apos;s own telemetry feed. The code that derives it is at{' '}
<a href={OBSERVER_REPO} rel="noreferrer">
git.lair.cafe/quantus/blackbeard.observer
</a>
, so any figure here can be traced to the read that produced it.
</p>
</div>
</section>
<section className="panel" style={{ marginBottom: 26 }}>
<div className="panel-head">
<h2 className="panel-title">Donations</h2>
<span className="eyebrow">entirely optional</span>
</div>
<div className="about-donate">
<AddressQr value={DONATION_ADDRESS} />
<div className="about-prose">
<p>
If the observer or the miner have been useful and you are inclined, donations toward
development, maintenance and hosting are gratefully accepted in QTC at:
</p>
<p>
<code className="about-address">{DONATION_ADDRESS}</code>
</p>
<p className="panel-note" style={{ padding: 0, border: 0 }}>
Nothing here is gated behind it, and nothing about the site changes if you do. The
code above is generated in your browser from the address printed beside it, so the two
cannot drift apart — scan it or copy the text, whichever you trust more.
</p>
</div>
</div>
</section>
</>
)
}
+1 -1
View File
@@ -52,7 +52,7 @@ export function SectionNav({
>
Genesis
</Link>
{SECTIONS.map((s) => (
{SECTIONS.filter((s) => !s.header).map((s) => (
<Link
key={s.id}
to={href({ chain, index: s.id })}
+25
View File
@@ -191,6 +191,18 @@ export function VestingPanel({
return null
}
})()
// Vested and not yet taken, summed across every grant. The headline figure on
// this chain today: two grants began vesting at launch while the other
// forty-six wait for 2027, so a page that only said "first claimable" would
// miss that a great deal already is.
const claimableNow = (() => {
try {
return data.schedules.reduce((sum, s) => sum + BigInt(s.claimable), 0n).toString()
} catch {
return null
}
})()
const firstCliff = data.schedules.reduce(
(a, s) => (a === null || s.cliff < a ? s.cliff : a),
null as string | null,
@@ -215,6 +227,11 @@ export function VestingPanel({
value={outstanding === null ? '—' : amount(outstanding)}
note="scheduled less claimed"
/>
<Field
label="Claimable now"
value={claimableNow === null ? '—' : amount(claimableNow)}
note="vested and not yet taken"
/>
<Field label="Grants" value={fmtHeight(data.schedules.length)} />
<Field
label="First claimable"
@@ -258,6 +275,12 @@ export function VestingPanel({
whole grant from its end, and linearly in between — so this is that arithmetic summed
across every schedule, not a model of what somebody intends.
</p>
<p className="panel-note">
The step at the left is not the whole story told small:{' '}
<strong>the grants do not share a cliff</strong>. Read the table for which ones have
started — a curve that rises early and then sits flat for a year is two grants vesting
from launch while the rest wait, not a gentle beginning.
</p>
</section>
<section className="panel" style={{ marginBottom: 26 }}>
@@ -275,6 +298,7 @@ export function VestingPanel({
</th>
<th scope="col">Grant</th>
<th scope="col">Vested</th>
<th scope="col">Claimable</th>
<th scope="col">Claimed</th>
<th scope="col" className="left">
Unlocks
@@ -294,6 +318,7 @@ export function VestingPanel({
</td>
<td className="numeral">{tokens(s.total, decimals, 0)}</td>
<td className="numeral">{tokens(s.vested, decimals, 0)}</td>
<td className="numeral">{tokens(s.claimable, decimals, 0)}</td>
<td className="numeral">{tokens(s.claimed, decimals, 0)}</td>
<td className="left">{day(s.cliff)}</td>
<td className="left">{day(s.end)}</td>
+84
View File
@@ -1267,6 +1267,90 @@ tr.mine .share-bar > i {
padding-left: 62px;
}
/* The About link, in the header beside the connection light. Quieter than a
section link: it is about the site rather than the chain, and it should not
compete with the nav below it for a reader's first glance. */
.about-link {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-muted);
padding: 4px 2px;
}
.about-link:hover,
.about-link[aria-current='page'] {
color: var(--data-bright);
}
.about-prose {
padding: 16px 18px;
max-width: 68ch;
line-height: 1.65;
}
.about-prose p {
margin: 0 0 14px;
}
.about-prose p:last-child {
margin-bottom: 0;
}
.about-prose a {
color: var(--data-bright);
text-decoration: underline;
text-decoration-color: var(--border-strong);
text-underline-offset: 3px;
}
.about-donate {
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
align-items: flex-start;
padding: 16px 18px;
}
.about-donate .about-prose {
padding: 0;
flex: 1 1 320px;
}
/* The address, whole and selectable. Never truncated: this is the one string on
the site where an abbreviation would be worse than useless, because a reader
copying `qzkQ…rre4` loses the money. */
.about-address {
display: inline-block;
font-size: 12px;
line-height: 1.6;
word-break: break-all;
background: var(--data-wash);
border: 1px solid var(--border);
padding: 8px 10px;
color: var(--text-primary);
}
/* The QR carries its own light background rather than inheriting the surface:
a code drawn in the accent hue on warm-black does not scan, whatever the
contrast checker says about text. Scanners want dark-on-light. */
.qr {
width: 172px;
height: 172px;
flex: 0 0 auto;
display: block;
border: 1px solid var(--border);
}
.qr-light {
fill: #ffffff;
}
.qr-dark {
fill: #000000;
}
/* A meter: one ratio against its limit.
*
* The track is the same hue at wash weight rather than a neutral grey, so the
+6 -1
View File
@@ -117,6 +117,7 @@ export type SectionName =
| 'network'
| 'wormhole'
| 'vesting'
| 'about'
/**
* The sections, in the order the nav shows them.
@@ -125,7 +126,7 @@ export type SectionName =
* a second page listing the same rows under a different address would be two
* answers to one question. `/:chain/miner/` redirects there.
*/
export const SECTIONS: { id: SectionName; label: string }[] = [
export const SECTIONS: { id: SectionName; label: string; header?: boolean }[] = [
{ id: 'block', label: 'Blocks' },
{ id: 'call', label: 'Calls' },
{ id: 'event', label: 'Events' },
@@ -147,6 +148,10 @@ export const SECTIONS: { id: SectionName; label: string }[] = [
// Last, and the only section that is about the future: every other page here
// reports what the chain has already done.
{ id: 'vesting', label: 'Vesting' },
// `header: true` keeps it out of the section nav. It is not a view of the
// chain — it is about the site — so it sits beside the connection light
// instead, and is listed here only so the URL parses.
{ id: 'about', label: 'About', header: true },
]
function section(name: string): SectionName | null {