miner-ui-5: the plan, the captures, the first-share runbook for the shipper, the Discord rung shapes (not posted)

docs/plans/miner-ui-5.md (what is built, gate results, owed, the RPC fields the node does not expose yet, what the
site lane takes); docs/plans/miner-ui-5-shots/ (light and dark at 1440 and 390, the saved block card PNG);
docs/plans/miner-ui-5-first-share-runbook.md; docs/community/discord/ladder.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 09:05:52 +00:00
parent 32793b686a
commit f508274fe2
18 changed files with 241 additions and 0 deletions

View file

@ -0,0 +1,78 @@
# Discord: the ladder's rung announcements (message shapes, not posted)
7 October 2026, miner-ui-5 (mission item 6, docs/analysis/mission/mission.md 2.6). The shapes the bot would post for
each rung of the miner's ladder, so the words in Discord match the app and the site rung for rung. Nothing here is
posted by this lane; the Discord lane wires them into `tools/community/discord-hooks.mjs` behind its guard rails
(docs/community/discord-hooks.md: the forbidden-string guard, the limits, idempotency keys, never a mention). Every
number is a chain fact from the public API (`/api/live`, `/api/stats`), which is the observer's copy of the node's
`getFinalityWeights` and `getFinalityCheckpoints`; a post names nothing the chain does not say.
## The rules every rung post obeys
| Rule | How |
|---|---|
| The miner's object, never the project's | a post carries the key id, the block link, the rank and the day; never the network hashrate, the block count or a milestone of the project |
| A chain fact only | the trigger is a field of `getFinalityWeights` (`keys[].blocks`, `keys[].voter`, `keys[].participation`, `voters`) or `getFinalityCheckpoints` (`latestLockedIndex`), read through `/api/live finality.weights` |
| Opt-in for anything beyond the key id | the card model and the "(you)" link to the address page appear only when the miner switched "Make my page public" on in the app (settings.profile_public); without it the post is the key id and the block link |
| Short key id | the first 8 hex of the vote key hash, the same id the app, `/live` and the explorer show; never a payout address, never a machine name |
| One post per key per rung | idempotency key `rung:<rung>:<keyid>`; a rung is posted once for a key, ever (a key that drops and climbs back is not posted again) |
| Copy law | no em dashes, no two-beat antithesis, no aphorisms; the numbers in the line, nothing decorative |
| Volume | #first-blocks takes rungs 0 and 1 (one line each); #numbers takes the daily ladder summary; rungs 2 to 5 and P are never posted singly (one a minute at scale), they are counted in the summary |
## Channel: #first-blocks
### Rung 0, first block (trigger: a key's first blue block in the window; `keys[].blocks` goes 0 to 1 in `finality.weights`, and the block's `miner` is that key in `/api/live blocks[]`)
```
First block: key 6ad0e117
Block 1,284,117 · igneum.network/block/9f3a…c21e
RTX 4070 (the miner chose to show the card)
```
Without the opt-in the third line is absent. Embed: ember accent, title "First block", one field "Key" = `6ad0e117`, one field "Block" = the explorer link; footer the UK time with UTC in brackets (the hooks script's footer rule).
### Rung 1, the vote (trigger: `keys[].voter` goes false to true, that is `blocks` reaches `params.dust`)
```
Your key has a vote: 6ad0e117
100 blocks in the window (the line is 100) · the key now signs every checkpoint
```
On the devnet the line reads `5 blocks in the window (the line is 5)`: the number is `params.dust`, never a constant.
## Channel: #numbers, the daily ladder summary (09:00 UK, beside the daily digest)
```
The ladder, yesterday
First blocks: 14 keys found their first block
Votes: 9 keys crossed the dust line (now 1,213 voters)
Signatures: 1,201 keys signed a locked checkpoint (latest lock 184,220)
Full window: 406 keys at 30 of 30 days
Rank 1 by weight: 6e80f3ef with 2,592 blocks in the window
Signing streak, longest: 41 days unbroken (presence 100%)
Shards: 388 paid to 57 keys
```
Fields and sources, one per line:
| Line | Field |
|---|---|
| First blocks | keys whose `blocks` went from 0 to at least 1 between the two daily reads of `finality.weights.keys[]` |
| Votes | keys whose `voter` went false to true; `voters` for the total |
| Signatures | keys with `participation` above 0 at the day's last read; `latest_locked_index` for the lock |
| Full window | keys whose `blocks` span the whole `params.weightWindow` (owed from the node: `keys[].daysMined`; until then the line is left out, never estimated) |
| Rank 1 | `keys[]` sorted by `blocks`, the top row |
| Signing streak | owed from the node (`keys[].streakDays`); until then the line is left out |
| Shards | `/api/live proving.shards_paid_10m` summed over the day's reads, provers from `live_proofs.prover` |
A line whose field the node does not expose yet is left out of the post rather than estimated (the "no number a company server has to be trusted for" rule, reinvent.md section 6).
## Roles (the Discord lane, from the same fields)
| Role | Granted when | Revoked when |
|---|---|---|
| Voter | a message signed with the vote key names a key whose `voter` is true in `getFinalityWeights` | `voter` false at a daily read (a key under dust, or stripped: `strippedUntilDaa` above the DAA score) |
| Window | the key's blocks span the full `params.weightWindow` (`keys[].daysMined` owed; until then by hand from the ladder summary) | the key misses a day |
| Prover | a paid shard record names the key (`igneum_getProofRecords(block).paid[shard]`) | never (a paid record is a chain fact) |
The signed message is the proof of key ownership; the app does not sign it yet (owed: a "prove my key" button that signs a nonce with the vote key and copies it).

View file

@ -0,0 +1,67 @@
# Runbook: "first share within 10 minutes of download in 9 of 10 fresh Windows installs"
7 October 2026, miner-ui-5. The measurement behind the `/evidence` line in mission item 6 (docs/analysis/mission/mission.md
2.6, reinvent.md 3.1). Written for the shipper (ae892a8b0f78fe31c) to run at the Devnet 2 gate of each cut; the app
lane does not run it. Nothing here touches PC 1 or a live manifest.
## What is measured
Ten fresh Windows 11 installs, each timed from the download's first byte to the first accepted share or block, with
three timed steps between: the installer (download start to the app's first screen), the node (first screen to
`node.synced` true), the card (synced to the first `ACCEPTED block` line, or the first share once pool-0 exists). The
claim passes when 9 of 10 reach the first accepted block or share inside 600 s of the download's start.
## Where the numbers come from
| Step | Start mark | End mark | Read from |
|---|---|---|---|
| Download | the first byte of `Igneum-Miner-Setup-<v>.exe` (the fetch's own timestamp) | the file complete | the job script's clock |
| Install and first screen | the file complete | `api/state` answers with `phase` = welcome (the engine is up) | the app's URL file, then `api/state` |
| Setup | welcome | `setup_done` true (Continue, Make me an address, I have saved my key, Start mining: the playbook clicks or the job posts `api/setup`) | `api/state` |
| Node | `setup_done` | `node.synced` true and `node.state` synced | `api/state` |
| Card | synced | `ladder.first_block_at` set (the first `ACCEPTED block` line) or, with pool-0, the first `share_result` line | `api/state.ladder` |
| Total | download start | `ladder.first_block_at` (or the share) | both clocks, the box's and the VM's, read as UTC |
`api/state.ladder.first_synced_at` and `first_mining_at` carry the node and card marks from the engine's own clock; the
runbook takes those over the script's reads when both exist.
## The ten installs
- Ten fresh Windows 11 VMs (or ten fresh user profiles on the two PCs is NOT acceptable: a fresh profile shares the
machine's node data and its driver state). The fleet's Windows wave boxes are the candidates; PC 1 and PC 2 are not
(PC 1 is the project lead's desk, PC 2 holds a standing node).
- Each VM: the GPU driver installed, no Igneum folder under `%LOCALAPPDATA%`, the clock right (the clock card blocks
mining otherwise and the run is void, not a fail).
- The download is the public installer URL of the cut under test, through the dl host, never a scratch copy.
- The run is `node tools/build-job.mjs run --target <vm> --script relay/playbooks/first-share.ps1` with the playbook
recording the six marks as `RESULT` lines; the collect job brings `app/ladder.json` back.
## The playbook's RESULT lines
```
RESULT step=download start=<utc ms> end=<utc ms> bytes=<n>
RESULT step=install end=<utc ms>
RESULT step=setup end=<utc ms>
RESULT step=node end=<utc ms> synced_at=<ladder.first_synced_at>
RESULT step=card end=<utc ms> mining_at=<ladder.first_mining_at> first_block_at=<ladder.first_block_at> hash=<ladder.first_block_hash>
RESULT total_s=<end - download start> pass=<true|false>
```
A run without a `first_block_at` inside 1,800 s ends with `pass=false` and the step it stalled in. A Poisson miss is
a fail too: an 8 GB card at 100 GH/s has a 46 percent chance inside the first hour (reinvent 1.4), so the claim is
about the install and the node, and the card step is measured against the Poisson expectation from
`api/ladder.network.hashrate_hps` and the card's rate (`mining.hash_total`); a run whose card step passed the
expected gap by less than one gap is reported as "within expectation" beside the pass word.
## What is published
`/evidence` gets one table: ten rows (card model, driver, the four step times, the total, pass), the median of each
step, the pass count, the cut's version and the installer's sha256. The site lane owns the page; the shipper hands
it the table as a JSON file (`site/evidence/first-share-<version>.json`) with the same keys as the RESULT lines.
## Known holes
- Pool-0 is not built into the app yet (reinvent 3.5), so today the end mark is the first accepted block, not a share;
the claim's words say "first share or block" until the pool field ships.
- SmartScreen (reinvent 3.1) adds two clicks the playbook must make; the time they take counts in the install step.
- A VM without a GPU passthrough measures the node steps only and is reported as such, not as a pass.

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

96
docs/plans/miner-ui-5.md Normal file
View file

@ -0,0 +1,96 @@
# Igneum Miner UI 5: the ladder shown everywhere, the miner's first month
7 October 2026, 10:1x to 11:xx UK. Mission item 6 (docs/analysis/mission/mission.md 2.6; the brief is
docs/analysis/mission/reinvent.md 1, 2, 3.2 to 3.7 and 7). Branch `miner-ui-5` from the miner-ui-4 tip 5d9c55a2
(release-0.3.18, since renumbered 0.3.19), worktree `../igneum-wt-miner-ui-5`. Nothing published; the shipper cuts it.
The site lane (a4b202cabca2d95c0) owns `/live` and `/miners`; the node lane (a283f5f0d364ceef0) owns the RPC fields
listed in section 6.
## 1. What is built
| Part | Where | What |
|---|---|---|
| (a) The Poisson count-up | Overview, `#first-wait`; `View.poisson` | from the first job until the first block of this install: "A card like yours finds a block about every 1.6 hours on today's network. Chance so far: 31%." The gap is `/api/stats hashrate` over the fleet rate (`mining.hash_total`) at one block a second; the chance is `1 - e^(-elapsed / gap)` from the moment mining started this run. Over an 8-hour gap the pool line sits under it. The ring fills with the chance. Hover names the two fields and the formula. |
| (b) The block card | Overview, `#block-card`; `View.blockCard`; `src/ladder.rs` | raised by the engine on the first block of the install, every 100th, 1,000th and 10,000th, and the first block of a new card (`ladder.milestone`); in place, never a dialog; stays until dismissed (the key is stored, so a relaunch does not re-raise it). "Block 1,284,117 is yours." (the DAA score the node logged), the card and its rate and share, "4.88 IGN to 0xdd44…86E8 (72 producer points, 8 for signing, ramp 19%)", "rank 41 of 1,204 keys · day 23 of 30", the hash, Open in the explorer (`/block/<hash>`), Save the card, Copy the link. The hash joins the miner's `ACCEPTED block nonce=` line to the node's `PoW accepted <hash> … nonce` line by nonce (the node logs every block it validates). |
| Save the card | `drawCard` in app.js, `POST /api/card`, `src/card.rs`, `platform::reveal_file` | a 1200x630 PNG drawn on a canvas from the card's own words with the fonts the app ships (no network call): the mark, "Block N is yours", the card and rate, the IGN line, rank and day, the miner key and hash, the explorer URL. The miner's object, never the project's number: no network rate, no block count. The engine writes it under `<data root>/cards/` and reveals it in Finder or Explorer. `u00-block-card-1200x630.png` is the one the mock drew. |
| (c) Earnings in IGN first | Earnings, the money card; `View.earningsLines` | line 1 "7,680 IGN a day at your last hour's rate (4 blocks, 80.00 IGN each) · about 6,912 IGN expected at 100 MH/s" (measured first, the expectation beside it; the expectation alone before the first block); line 2 "2,592 blocks in 30 days, 207,360 IGN" from `getFinalityWeights keys[].blocks` at today's reward, with the shards; line 3 the balance (unchanged); line 4 the typed price: a £ per IGN the user types, kept in the page's storage, never fetched, "about £384 a day at £0.05 per IGN, your figure · no exchange lists IGN"; line 5 "your cards are 0.100% of the network · weight rank 41 of 1,204 keys · 23 of 30 days"; line 6 electricity unchanged plus "2,133 IGN per kWh"; the dev fee unchanged. Every number's `title` names its field. |
| The per-card line | Cards rows | an "IGN a day" cell per card from its rate, the network rate and the reward (hover: the fields) |
| (d) The ladder | Earnings `#ladder-card` (the rungs with their lines and hovers), Overview `#ladder-strip` (the seven chips and the current rung's line); `View.ladderRungs` | rungs: first block; your key has a vote; your signature is in checkpoint K; full window; rank by weight; signing streak; your card proved a shard. Every threshold is read from the node's `params` (`dust`, `weightWindow`, `presenceWindow`), never a constant: the devnet reads "5 blocks to a vote" and "2 of 2 hours", mainnet "100 blocks" and "30 of 30 days". The words, in copy law: "Your key has a vote." "Your signature is in checkpoint 184,220." "30 of 30 days. Full weight." "Rank 41 of 1,204 keys." "Signing 8 of 80 points, 41 days unbroken." "Your card proved shard 3 of block 1,284,117." |
| The chain facts | `GET /api/ladder`, `src/chainfacts.rs` | the node's `igneum_getFinalityWeights` on the EVM port first (OWED on the node, section 6), else the observer's `/api/live finality.weights` (the same reply relayed, keys cut to 64, ids as 8 hex) plus `/api/stats`; this machine's keys ranked by `blocks`; cached 10 s; the reply names its source and the dashboard says it ("through the observer" until the node answers). |
| The machine's record | `src/ladder.rs`, `<app dir>/ladder.json`, `api/state.ladder` | first block (time, card, hash, DAA), blocks per UTC day (the days-mined ring), the checkpoints signed and which locked (`VOTE index=` and `LOCK checkpoint` lines), the signing streak (a gap over the presence window ends it), the paid shards (`src/prover.rs`), the milestone, the first-hour marks (node synced, card mining). |
| (e) The public profile opt-in | Settings "Make my page public"; `settings.profile_public`, `api/settings {profile_public}` | off by default; the words say what becomes public (key ids, blocks, rank, presence, card model). The site reads the switch through the app's existing report path (owed to the site lane; today the switch is stored and shown, nothing leaves the machine). |
| (f) The first-hour timeline | Earnings "Your first hour"; `View.timeline` | Installed (`settings.installed_at`), App started, Node synced (`ladder.first_synced_at`), Card mining (`ladder.first_mining_at`), First block, First payout (the coinbase of the first block), each as "+N min" from the install. |
| Prove | `#shard-card` | "Your card proved shard 3 of block 1,284,117." with the IGN, on the newest paid record |
| Activity | `EVENT_MAP` | "Your first block, found by RTX 4070", "Block 1,000 of this machine, found by …", "First block on …" |
## 2. The chain scene (EMBER 02, taken from the site lane)
the project lead's renderer pack (11:3x UK): `ui/live-dag.js` (sha256 582f2e73…, IgneumDag 2.0.0) and `ui/proof-core.js` (c2a094a6…,
IgneumProof 2.0.0) are byte-identical copies of the site-ui-4 files; the engine serves `proof-core.js` beside
`live-dag.js`. The Overview card mounts the compact mode (`compact: true, window: 60, poll: false`); Inspect, or a
click on a block, opens the full scene under it with the block inspector in the site's words (Proof in progress /
Proof complete / Checkpoint locked / Block observed; Proof shards, Inclusion, Miner key, Blue score, DAA score,
Parent links, Header time, Checkpoint, Finality; the note on separate states), the filter tabs (All blocks, Selected
chain, Your blocks), the window stepper, Follow and Pause. One owner of the feed: the page reads the engine's
`api/live?window=120` every 2 s and pushes the reply into both scenes; `mine` matches every vote key id of this
machine's cards (the 8 hex the feed names as `b.miner`), never the payout address. `--included` and `--excluded` are on
`:root` for both themes. The fallback strip (this machine's own blocks) draws after three failed reads. The legend
reads "Your blocks" as in the reference.
## 3. Gate results
| Gate | Result |
|---|---|
| A Devnet 2 key walks every rung on screen in a view test | `view.test.mjs` "a Devnet 2 key walks every rung on screen": key 6ad0e117 under the finality params the devnet node reports (`api/live params`, 7 Oct 2026: dust 5, weightWindow 7,200, presenceWindow 20; Devnet 2's override file sets none of them, so it runs the same values) goes nothing → first block → vote → signature in checkpoint 8,496 → 2 of 2 hours → rank 3 of 4 → 15 min unbroken → shard 3 of block 1,284,117; every rung's `field` matches `getFinalityWeights|getFinalityCheckpoints|igneum_getProofRecords|/api/stats|this app`. The mainnet-sized walk (100 blocks, 30 days, the observer's cap of 64) is the next test. |
| Every number names its RPC field on hover | `View.FIELDS` (23 sentences) in every `title`; the rungs, the Earnings lines, the block card lines, the Cards cell, the count-up |
| The PNG opens; no network call is made to make it | `u00-block-card-1200x630.png` drawn by the mock page through Playwright's chromium; the page's only calls while drawing are `document.fonts.load` of the app's own woff2 files |
| "First share within 10 minutes of download in 9 of 10 fresh Windows installs" | written as a runbook for the shipper, not run: `docs/plans/miner-ui-5-first-share-runbook.md` |
| Tests | UI 47 (42 before; the five new ones); the app crate on the box 190 + 27 + 8 (ladder 10, chainfacts 4, card 3 among them) |
## 4. Captures (`docs/plans/miner-ui-5-shots/`, the ui-mock's scenarios firstwait, firstblock, ladder)
`u00-block-card-1200x630.png` the saved card; `u01-overview-firstwait-dark-1440` the count-up and the strip; `u02`/`u03`
the first block's card, dark and light; `u04`/`u14` the full scene with a block in the inspector, dark and light;
`u05`/`u06` Earnings with the ladder and the timeline open; `u07` the shard card; `u08` the Cards row with IGN a day;
`u09` Settings with the profile switch; `u10` to `u13` the Overview and Earnings at 390, both themes. Captured through
Playwright's own chromium (never Google Chrome.app) against `tools/ui-mock/server.mjs 4319`.
## 5. Owed by this lane, and what the site lane and the Discord lane take
| What | Who |
|---|---|
| The Cards setup screen's "about N blocks a day" from the priors file (reinvent 3.2, Cards rows) | this lane, once the priors ship in the signed manifest (the file lives on the site today) |
| The QR of the explorer link on the saved card (reinvent 3.3) | this lane; the URL is printed in full instead |
| The opt-in reaching the site (the address page's noindex lifted for a key whose app switched it on) | the site lane and this lane together: the switch rides the app's existing report upload as `profile_public` |
| `/live` weight leaderboard and `/miners` census with the same words | the site lane; the field list and the rung words were sent to it (section 7) |
| `#first-blocks` posts and the Voter and Window roles | the Discord lane; the shapes are in `docs/community/discord/ladder.md`, nothing posted |
| Sound on a block | not built (verdict table: watch) |
## 6. RPC fields the node does not expose yet (sent to the node lane)
| Field | Why the app needs it | What the app does meanwhile |
|---|---|---|
| `igneum_getFinalityWeights` on the EVM JSON-RPC port (the wRPC `getFinalityWeights` reply as is) | the engine carries no wRPC client; today the facts come through the observer's relay, which cuts the table to 64 keys and so cannot rank a key below 64 | reads `/api/live finality.weights` and says "through the observer" on hover; "Below the top 64 keys" when the key is not listed |
| `igneum_getFinalityCheckpoints` the same way | the locked index and the certificate list from this machine's node | `/api/stats finality.latest_locked_index` |
| `getFinalityCheckpoints checkpoints[].signers` (the key hashes in the certificate bitmap), or a per-key `lastSignedLockedIndex` | "Your signature is in checkpoint K" as a chain fact | the app's own `VOTE index=` lines, read as locked once a `LOCK checkpoint` line reaches them; the hover says so |
| `getFinalityWeights keys[].daysMined` (distinct days with a blue block in the window) and `keys[].firstBlockDaa` | "30 of 30 days" as a chain fact; the Window role; the `/live` days-mined column | the app's own day ring (`ladder.json`), which only counts what this install saw |
| `getFinalityWeights keys[].streakDays` or `keys[].lastMissedDaa` | the signing streak as a chain fact | the app's own unbroken-vote reading; `participation` is shown beside it |
| `getFinalityWeights keys[].rank` or a `getFinalityKey {keyHash}` reply with blocks, voter, participation, rank, daysMined, streak | one call per machine instead of the whole table | the table sorted on the client |
| `igneum_getProofRecords` by key (the paid shards of a key over a range) | "your card proved shard N of block M" for records this install did not watch | `src/prover.rs` reports what it submitted and saw paid |
## 7. Sent to the site lane (so `/live` and `/miners` match the app word for word)
The rung names and lines of section 1 (d); the fields: `getFinalityWeights params.dust / weightWindow / presenceWindow`,
`keys[].blocks / voter / participation / strippedUntilDaa / pubkey`, `totalWeight`, `activeWeight`, `voters`,
`checkpointIndex`; `getFinalityCheckpoints latestLockedIndex / nextIndex / finalityActive`; `/api/stats hashrate`,
`block_reward.miner_ign`, `block_reward.ramp_factor`; the leaderboard column order (rank, key id, blocks in window,
days mined, presence, last block) with "weight, never hashrate: a card that arrived today sits at the bottom".
## 8. Copy added (the copy law: no em dashes, no two-beat antithesis, no aphorisms; every line names a number or an action)
"Waiting for your first block." "A card like yours finds a block about every 1.6 hours on today's network. Chance so
far: 31%." "Above an 8-hour gap a pool pays by the share, every minute. The pool setting arrives with pool-0." "Block
1,284,117 is yours." "Open in the explorer" "Save the card" "Copy the link" "Your key has a vote." "61 of 100 blocks to
a vote." "Your signature is in checkpoint 184,220." "30 of 30 days. Full weight." "Rank 41 of 1,204 keys." "Below the
top 64 keys." "Signing 8 of 80 points, 41 days unbroken." "Your card proved shard 3 of block 1,284,117." "Proving is
off." "Type a price" "Use it" "Make my page public" "Your first hour" "The ladder".