# Horizon lane 6: polish. Every shipped system against the best in its class Date: 6 October 2026, evening UK (written 20:00 to 21:00Z, while the live devnet's finality was still paused). Lane 6 of the Horizon programme. Worktree `/Users/joshm/Projects/igneum-wt-horizon` (branch `horizon`, HEAD c3aa502). Output: this file only. No file outside it was edited; nothing was built, deployed, posted or started; the installed app and every live service were read, never touched. the project lead's bar: "a level of polish that has not been seen before." This file is the honest audit: what each shipped system does, the named comparator and the exact screen or feature it has that we lack or do worse, the ledger rows that close the gap (95 rows, ids Q1 to Q105 with gaps, each with a file or screen, a severity, hours of agent work, an owner and a gate), and what is already better than the comparator. The first ten rows are what the project lead will notice first when he opens each product tomorrow. ## 0. What was read and run Read, in the worktree: `app/igneum-app` (src, ui, tests), `app/mac/IgneumMiner.swift`, `app/windows/host.cpp`, `docs/plans/miner-ui-2.md`, `miner-ui-3.md`, `miner-ui-3-audit.md`, `ember-tune.md`, `release-0.3.14.md`, `docs/design/app-screens/`, `docs/design/miner-tuning.md`, `docs/fud-ledger.md` (X17, X19, X21 to X28, M25 to M29, G7, G10, G13), `site/` (every page, `api/`, `lib/`, `verify/`, `build.mjs` read not run, `scrub.mjs`, `forbidden-strings.txt`, `downloads.json`, `journey.json`, `vercel.json`, `sitemap.xml`), `docs/design/site-polish-2026-10-04.md`, `docs/plans/site-ui-3-shots/`, `docs/plans/site-miner-2026-10-06.png`, `docs/review/round-4-reddit-2026-10-06.md`, `docs/plans/explorer.md` and `explorer/`, `relay/` (ui, api, lib, clients, playbooks, tests), `tools/relay.mjs`, `tools/console.mjs`, `tools/jobs.mjs`, `docs/design/relay/`, `docs/design/console/`, `pool/` (src, web, tests, README), `docs/plans/pool.md`, `docs/spec/09-pool-protocol.md`, `tools/community/discord-hooks.mjs` and its tests and fixtures, `docs/community/discord-hooks.md`, `tools/fleet/`, `tools/build-job.mjs`, `tools/build-remote.sh`, `tools/ci/`, `docs/plans/build-job.md`, `build-server.md`, `hands-on-build-1.md`, `node-changes.md`, `packaging/README-ship.md`, `packaging/ota/README.md`, `packaging/mac/README.md`, `packaging/windows/README.md`, `tools/observer/` (observer.mjs, README), `docs/spec/03` section 3.9, and this lane's siblings `finality-and-weight.md` (3.1, 6) and `frontier.md` (3.7). Read, read-only, in other agents' worktrees: `/Users/joshm/Projects/igneum-wt-ship0315/docs/plans/release-0.3.15.md` (the newest release notes; `release-0.3.15.md` does not exist on `horizon` or `master`), `/Users/joshm/Projects/igneum-wt-wallet*` (wallet 0.1.4 source, UI 3 branch), `/Users/joshm/Projects/igneum-wt-gpu-fleet/tools/fleet/`, `/Users/joshm/Projects/igneum/vendor/igneum-node-0310/consensus/src/processes/finality.rs` (the node's `finality_reason`). Run (nothing live, nothing built): `node --test app/igneum-app/ui/*.test.mjs` (35 pass), `node --test relay/test/*.test.mjs` per file (24 pass), `node --test tools/community/discord-hooks.test.mjs` (31 pass, every live poster stubbed), one `curl -s https://igneum.network/` (the allowed single fetch; saved to the scratchpad), and one read-only SQL pass over the observer's Neon tables (`live_checkpoints`, `live_events`, `live_state`) through the same HTTPS SQL endpoint `site/api/_neon.mjs` uses, to see what every surface had to render during tonight's pause. No browser automation. Comparator claims are from memory unless a repository or page is named, and are labelled approximate. ## 1. The ten rows the project lead will notice first | Rank | Id | Row | File or screen | Severity | Hours | Owner | Who it hits | Gate | |---|---|---|---|---|---|---|---|---| | 1 | Q1 | The pause has no cause anywhere. The node reports `finality_reason` (`active`, `window filling, N of M`, `paused`) but not the frozen-table share that held tonight's pause; the observer drops even the reason; the API has no reason field; `/live` computes "N% of weight silent" from the sliding table, which read 11 percent silent at 20:05Z while finality was paused (held by the table frozen at lock 6842, 57.3 percent signing). Add `finality_reason`, `held_by` (frozen index, its signing share, its expiry DAA) and lane 3's `finality_provisional` end to end | `vendor/igneum-node-0310/consensus/src/processes/finality.rs:1794-1807` (reason exists, no frozen share), `tools/observer/observer.mjs:720` (copies `finalityActive` only), `site/api/live.mjs:114-122` (no reason), `site/live.html:520` (the silent percentage) | the project lead-visible | 4 (node 1.5, observer and API 1, `/live` and `/api/stats` copy 1, spec 3.9 row 0.5) | consensus-engineer (node), site owner (observer, API, page) | holder, exchange: the one line that says whether a pause is a silent third, a split, or a table waiting to expire; miner: nothing lost, but the app can finally explain itself; operator: a pause with a cause and an end time | A forced pause on Devnet 2 (one slice over a third stopped): `/api/live.finality` carries `reason`, `held_by`, `provisional`; `/live` reads "finality paused since 18:40Z: 89% of the sliding weight is signing, held by the table frozen at lock 6842 (57% signing) until DAA 216,402 (about 20:40Z)"; the exchange guidance in spec 3.9 carries the provisional row (lane 3 proposal 4) | | 2 | Q2 | Ember never says "paused". The engine keeps only `last_lock`, `last_lock_at`, `age_s`, `votes`; the node line showed "#6842 · 1 h ago" under "a point the miners agreed can never be undone" for the whole two-hour pause. No `finality_active` anywhere in the app | `app/igneum-app/src/state.rs:247-248`, `src/engine.rs:3495-3497, 3896-3902` (lock parsed from the miner's `LOCK checkpoint` line only), `ui/app.js:1350-1351` | the project lead-visible | 3 | app owner (engine reads `getFinalityCheckpoints` or the miner's `FINALITY` line, spec 3.9 row; UI state and view test) | home miner and rig: the app tells them finality is paused and why, instead of a lock age that climbs; pool user: the same on the pool page later | Mock state with `finality_active:false, reason:"paused"` renders "Finality paused since 18:40Z, 89% of the 30-day weight signing, held by the frozen table until about 20:40Z" on the node line and in the pill; `view.test.mjs` case; seen on the Devnet 2 forced pause | | 3 | Q3 | A fresh machine's first 60 seconds start with a warning. macOS: ad hoc signature, no Developer ID, no notarisation; the engine strips `com.apple.quarantine` from its own bundle on start; the README's "Right-click > Open" bypass is gone on macOS 15 (approximate). Windows: installer and exes unsigned, SmartScreen "Windows protected your PC", then a UAC prompt for the firewall rule 20 to 50 s in before any screen explains it | `packaging/mac/build-dmg.sh:118-122`, `packaging/mac/README.md:51-53`, `app/igneum-app/src/main.rs:78`, `packaging/windows/README.md:76, 111`, `packaging/windows/build-installer.ps1:188`, `app/igneum-app/src/ota.rs:981-1015` | the project lead-visible | 6 (Developer ID signing plus `notarytool` and stapling in `build-dmg.sh` 3; Authenticode `signtool` step in `windows.yml` and `build-installer.ps1` 2; firewall step after `setup_done` with a sentence on the Cards screen 1), plus the project lead: an Apple Developer account and an OV or EV code-signing certificate (purchases, hours of the project lead's time, approximate) | app owner; the project lead (the certificates) | every new miner on every tier, Windows and macOS: Signal's and Tailscale's installers open with no warning (approximate); today ours open with two | Fresh macOS 15 VM: the DMG's app opens on double-click, `spctl -a -vv` says accepted and notarised; fresh Windows 11 VM: no SmartScreen interstitial, `signtool verify /pa` passes; the first UAC prompt appears after the Cards screen names it | | 4 | Q4 | Every update is "urgent" once an activation height is behind the node: `fork_is_close(Some(198000), 209000)` is true, so the manifest's stale `activation 198000` turned the 0.3.14 update into "the node is 0 blocks away. Installing now", stripped Later and skipped every safe-moment guard (the PC 1 install under a measurement job at 17:52:54Z). The same path has no finality input: an update applies through a pause | `app/igneum-app/src/manifest.rs:350-355` (`daa + 1800 >= h`), `:322-347` (`safe_to_apply`, no finality), `src/ota.rs:484`, `ui/app.js:217`, `docs/plans/release-0.3.14.md:79`, `release-0.3.15.md` section 6 | the project lead-visible | 3 (close means within 1,800 blocks ahead and not behind 1; the publisher drops a passed `activation_height` 0.5; `Moment.finality_paused` holds a non-urgent update 1.5) | app owner | home miner, rig: no surprise restart mid-measurement or mid-pause; operator: an activation that has passed is not an emergency | Unit tests: behind by any amount is not close; `publish-manifest.sh` refuses a passed height; a paused mock holds the update with the words "finality is paused; installing when it resumes"; the 0.3.16 manifest carries no stale height | | 5 | Q5 | The homepage says "final" while finality is paused, in a 90-word hero, under a share card that renders as a thumbnail. The chain scene takes `src.locked` and draws the dashed line labelled "final" at the newest locked block with no `finality.active` check (R4.6.3 was fixed on `/live` only); the hero is one 90-word paragraph with two bench deep links and "ships when the packaging row lands"; the miner section still says "a 24 GB NVIDIA card proves as well" while the hero and litepaper say 8 GB; home, litepaper, live and explorer share a 256 px `og-small.png` with `summary` cards | `site/index.html:810, 838` (final label), `:345` (hero), `:494` and `site/miner.html:7, 13, 21, 265, 382` (24 GB), `site/index.html:15-19` (OG) | the project lead-visible | 3 (final label 0.5, hero 1, 24 GB sentence 0.5, four 1200x630 cards 1) | site owner | every visitor; a holder or exchange reading "final" during a pause is the worst of them | No "final" label while `finality.active` is false (the `/live` rule); hero under 40 words with one link; one proving-tier sentence on every page; every page's shared card is 1200x630 | | 6 | Q6 | The hub shows stale data as live, said "active" for the first 20 unlocked checkpoints, says "paused" with no since or cause, and buried the pause under 401 miner_quiet and miner_back events. After the first load an API failure only changes the header to "offline: ..."; tiles, cards and "Refreshed" keep the old values. `finality_active` flips only after `presence_window` (20 on the devnet) indices without a lock, so 18:42 to 18:50Z read "active" in white with no lock forming. No `finality_paused` or `finality_resumed` event exists; `live_events` between 18:25Z and 21:30Z holds 203 `miner_quiet`, 198 `miner_back`, 55 `difficulty`, 30 `checkpoint_locked` (the last at 18:42:10Z) and no pause row. The checkpoint table also showed "% of active" above 100 (102.4 to 113.4 percent on indices 6900 to 6930) | `relay/ui.html:564` (stale), `:367-368` (tile), `relay/api/console.mjs:214`, `vendor/igneum-node-0310/consensus/src/processes/finality.rs:1794` and `vendor/igneum-node/consensus/core/src/finality.rs:63, 75` (`presence_window` 240 mainnet, 20 devnet), `tools/observer/observer.mjs:652` (the only finality event), `live_checkpoints.fraction_active` rows 6900 to 6930 | the project lead-visible | 4 (dim every panel with "last data N min ago" 1; amber on the first under-2/3 checkpoint 0.5; `finality_paused` and `finality_resumed` events, and collapse quiet/back pairs under 5 min 1.5; clamp or explain the active fraction 1) | fleet agent (hub), consensus-engineer (observer events, the fraction) | operator: the hub is the project lead's first screen; a holder reading the public API gets the same events | Cut the network: every hub panel dims within 15 s; a forced Devnet 2 pause turns the tile amber on the first checkpoint under two thirds, posts one `finality_paused` event with the cause and one `finality_resumed` with the duration; no fraction above 100 percent on any row for 24 h | | 7 | Q7 | Discord said nothing. The bot has no credentials file and its timer is not installed, so tonight's pause produced zero posts; had it been live, a condition already firing at the watcher's first look is marked `preexisting` and never opens an incident; the pulse hides the pause in its description with no colour or title change | `docs/community/discord-hooks.md:81-82`, `tools/community/discord-hooks.mjs:481-485`, `:252-255`, `:158`, `infra/build-server/discord-hooks/install.sh` | the project lead-visible | 3 (install and one live pulse 1; a pre-existing condition opens with "since at least " 1; paused pulse in a distinct colour with a title suffix 1) | miner-community-lead (community owner) | every Discord reader, which on launch day is every miner | `check` prints three "set"; one live pulse in #numbers; a fixture where the pause predates the first tick opens an incident; the paused pulse renders amber with "(finality paused)" in the title | | 8 | Q8 | The relay's security fixes are not on master. Commit 28c028b (X23 to X28: `relay/lib/guard.mjs`, `handler.mjs`, run signatures, retention) is on `fud-close` and the `ledger-*` branches only; `git merge-base --is-ancestor 28c028b master` is false. On this tree any of the three intake keys (one ships in every miner app) can post, sync and delete console items, read the whole feed, and every client puts the token in the URL path | `relay/api/console.mjs:249, 283-301`, `relay/lib/relay.mjs:34-46`, `relay/clients/agent.sh:10`, `igneum-agent.ps1:13, 72, 154`, `tools/relay.mjs:24` | operator-visible (security) | 2 (merge or cherry-pick with the 47 relay tests 1; handler test that an intake key gets 403 on post, sync, delete 1) | fleet agent (relay owner) | operator: the console is the control plane for every PC and the fleet; a miner app's intake key is in every install | `28c028b` is an ancestor of the deploying branch; 47 relay tests pass; the handler test above passes; `relay/README.md` names the deployed commit | | 9 | Q86 | The fleet page is blind to the standing fleet, to death and to finality. `page.py:43-47` publishes `standing` and `devnet2` and the page references neither (grep 0); a dead box reads "running" for up to 20 minutes because state comes from `boxes.json` and `standing.jsonl` is never read; no fleet or console script reads `finality_active`, so tonight's pause was found by hand. The night the project lead ruled that 22 boxes stay up permanently, the page that shows them cannot show them | `igneum-wt-gpu-fleet/tools/fleet/page.py:38-47`, dlsite `index.html:243`, `lib/standing.py:54-56`, `tools/console.mjs:124` | the project lead-visible | 6 (standing block with USD per day and the Devnet 2 gate line 2; "unreachable since HH:MM" within 2 minutes 2; finality on the page and in `console.mjs chain` 2) | fleet agent | operator (the project lead reads this page every evening); every tier indirectly: a dead standing box is weight that left silently, the class of tonight's pause | the page shows the standing count and spend; a box killed by hand reads "unreachable since" within 2 minutes; a forced Devnet 2 pause reads on the page and in the console | | 10 | Q9 | The wallet page describes 0.1.4 while UI 3 (five state words, light mode, pounds line) sits unmerged on `wallet-ui-3`; the MetaMask guide's testnet RPC is still labelled a placeholder, its devnet RPC port is the miner's node (26790) while a wallet-only Mac runs 26800, and `wallet_addEthereumChain` carries `blockExplorerUrls: []` and no `iconUrls`, so MetaMask shows no explorer link and a blank icon | `site/wallet.html:261-266, 312`, `igneum-wt-wallet-ui/docs/plans/wallet-ui-3.md:93, 112`, `site/metamask.html:197, 225, 233, 285-286`, `igneum-wt-wallet/app/igneum-wallet/src/node.rs:24-26` | the project lead-visible | 6 (wallet 0.1.5 with UI 3 published and the page rewritten 4; guide: both ports, the placeholder line removed, explorer URL and icon in the request 2) | app owner (wallet), site owner (guide) | holder: the first non-miner product; a wrong port or a placeholder RPC is a dead end at the first step | `downloads.json` wallet-mac 0.1.5; the page names pending, included, executed, proven, finalised; `eth_chainId` returns 0x116e from the guide's URL; MetaMask shows the explorer link after the add-chain button | Next in line, outside the ten: Q10 (the explorer knows nothing about finality and swallows failures after first load, 3 hours), Q80 (the Windows installer pipeline is dead on GitHub billing) and Q12 (Ember's light mode fails contrast on the private key and every live state). Hours for the ten: 40 agent hours, plus the project lead's certificate purchases for Q3. ## 2. Tonight's pause on every surface (the ledger row the project lead asked for) Times UTC. The chain-side facts are lane 3's (`finality-and-weight.md` 3.1) and the observer's own rows, read tonight: last lock 6842 at about 18:39:40Z; 6843 proposed at 53.1 percent of total; `finality_active` false from about 18:50Z (index 6862, twenty indices without a lock, `presence_window` 20); no lock after 6842 as of 20:05:03Z (`live_state.updated_at`), `total_weight` 7,167, `active_weight` 6,346 (88.5 percent signing on the sliding table), `voters` 85, and no `finality_reason` key in the stored JSON. Sampled `live_checkpoints` rows: | Index | First seen | State | Signed, percent of total | Signed, percent of active | Votes | |---|---|---|---|---|---| | 6820 | 18:28:30 | locked | 88.8 | 100.0 | 80 | | 6840 | 18:42:10 (ingested after the hub came back) | locked | 79.5 | 98.6 | 83 | | 6850 | 18:43:41 | proposed | 55.2 | 76.8 | 75 | | 6880 | 19:00:22 | proposed | 50.1 | 81.2 | 49 | | 6910 | 19:13:55 | proposed | 67.4 | 107.4 | 55 | | 6920 | 19:19:07 | proposed | 74.8 | 113.4 | 78 | | 6960 | 19:40:19 | proposed | 86.0 | 100.0 | 78 | | 7010 | 20:04:32 | proposed | 87.7 | 99.1 | 73 | From 6910 (19:13:55Z) every proposed checkpoint carried over two thirds of the sliding total and did not lock: the frozen table of Q5 held it (lane 3, 3.1). No surface could say so, because no field carries it. The "percent of active" above 100 on 6900 to 6930 is a display bug on the hub's checkpoint table (the active denominator lags the signers). | Surface | What it showed 18:40Z to 20:05Z (from the code path and the rows above) | Honest? | Fix row | |---|---|---|---| | Ember (`app.js:1350`) | "last lock #6842 · 1 h 25 min ago" with "a point the miners agreed can never be undone; locks at 2/3 of the 30-day weight". No pause word anywhere | No: the age climbed and the note contradicted the state | Q2 | | Site `/live` (`live.html:512-520`) | Status LIVE, blocks flowing, finality bar grey, "finality paused: 45% of weight silent" at 18:50Z falling to "11% of weight silent" by 20:05Z, no final region, no checkpoint ticks; "last lock #6842, 1 h ago" in the stats (`:404`, no active check); a hovered checkpoint block still read "locked" (`:450`) | Half: the pause is named (R4.6.3) but the percentage says the opposite of why it is paused once the stayers passed two thirds | Q1 | | Homepage (`index.html:810, 838, 879`) | Counter "last lock: paused"; the chain scene still drew the dashed line labelled "final" at the newest locked block | No: "final" and "paused" on one screen | Q5 | | Explorer (`explorer.html`, `block.html`) | Identical to a normal hour; stars on locked checkpoint rows; block pages of ordinary blocks "not a checkpoint" | No: nothing marked the pause | Q10 | | Hub (`relay/ui.html:367-368`) | 18:30 to 18:42Z: "observer STALE N s" in red, tiles frozen, finality "active" (the hub itself was down with the Mac). 18:42 to about 18:50Z: "active" in white, no lock forming. About 18:50Z onward: "paused" in amber, "last lock #6842" in amber, eight rows "proposed" at 53 to 89 percent, Events card full of `miner_quiet` and `miner_back` | Half: paused, but no since, no cause, no end; active for ten minutes of no lock | Q6 | | Discord | Nothing: the bot is not live. Had it been: the 20:00Z pulse would have read "Finality paused since 2026-10-06 18:4x UTC: nothing is final until two thirds of the 30-day weight signs again; mining and proving are paid as usual" in the description, and the watcher would have marked the pause `preexisting` and never opened an incident | No | Q7 | | Wallet 0.1.4 (`igneum-wt-wallet` `app.js:185, 340`) | Node card "paused" when a local node reports `finality_active` false, or "not checkpoint-checkable without a node" on the public RPC; transaction rows stay "in block N" with no mention | Half | Q35 | | Pool page (`pool/web/index.html`) | Not deployed; the code has no finality concept; it would have confirmed and paid on blueness throughout | n/a (a design choice, stated in Q50) | Q50 | The row for the ledger: **Q1 plus Q2, Q5, Q6, Q7, Q10**: every public and operator surface either hid the pause, named it without a cause, or contradicted it, for two hours, during the first finality pause the project has had in public. The gate across all of them is one forced pause on Devnet 2 with a screenshot of each surface, before the public testnet opens. ## 3. Per system ### 3.1 Miner app (Ember) **What it does today.** A Rust engine (`app/igneum-app/src/`) runs `igneumd` and one GPU worker per card, serves a tokenised dashboard on 127.0.0.1 (`src/server.rs:11-33`, routes `:184-359`: `api/state`, `api/log`, `detect`, `setup`, `key/saved`, `key/reveal`, `start`, `pause`, `resume`, `cards`, `tune/goal`, `settings`, `prove`, `update/check|install|auto|open`, `jobs/allow|check`, `power/apply|control`, `sweep/*`, `clock/*`, `quit`). Screens (`ui/index.html`): Welcome (line 71), Cards (102), Address (127), the one-time key sheet (438), then a rail with Mine, Earnings, Prove, Settings (164 to 413), a log drawer (460), a status strip (`Notices`, `ui/app.js:7-140`) and an update card (`UpdateCard`, `app.js:154-243`). Ember Tune (`src/ember.rs`, `src/sweep.rs`, `docs/plans/ember-tune.md`) tunes power and clocks per card against a goal. Updates are Ed25519-signed manifests (`src/ota.rs`, `src/manifest.rs`). Hosts: `app/mac/IgneumMiner.swift` (WKWebView, menu bar), `app/windows/host.cpp` (WebView2, tray). The shipped build is 0.3.14 with miner-ui-3; 0.3.15 (miner-ui-4: the big-number hero and the chain scene) is staged, its canary failed on block version 1026 and was rolled back (`release-0.3.15.md` section 5). UI tests: 35 pass. **Comparators and the exact gap** (all approximate, from memory of the products). | Comparator | Their screen or feature | Ours, and where it falls short | |---|---|---| | NiceHash QuickMiner | Profitability per day in fiat on the main screen; Rig Manager web view of your own rigs | Earnings reads "£0.00 earned: nothing is bought or sold on devnet" (`index.html:250`); mined IGN is never shown, only proving `paid_wei` (`app.js:1364`); no owner-facing remote view (the console is the operator's) | | HiveOS | Hash rate and temperature charts over hours; Telegram or Discord alert on a GPU fault or a rig offline; per-GPU fan and memory clock | A 10-minute blocks strip only (`app.js:1052-1127`); no outbound alert; manual control is the power slider (`app.js:1255`); the memory knob exists in the engine with no control | | lolMiner, T-Rex consoles | Per-GPU accepted, rejected, invalid and faults on every status line | Per card only "N blocks" (`app.js:306`); `rejected_session` a total in the hero sub-line (`app.js:1290`); `mismatched=` and `faults=`, which X21 made the engine read, never reach the row | | Nanominer | Restart-on-hash-drop watchdog with a visible threshold | `src/watchdog.rs` exists; the row says "stopped after repeated faults" (`app.js:296`) with no count or threshold | **Rows.** | Id | Row | File or screen | Severity | Hours | Owner | Gate | |---|---|---|---|---|---|---| | Q2 | Finality pause invisible (section 1) | `state.rs:247`, `engine.rs:3495`, `app.js:1350` | the project lead-visible | 3 | app owner | section 1 | | Q4 | Every update urgent once the activation is behind; no finality hold (section 1) | `manifest.rs:322-355` | the project lead-visible | 3 | app owner | section 1 | | Q11 | Offline start says "The update failed." A check error with no held manifest calls `set_error`; the cached manifest is loaded for the floor but never into `self.manifest`; the notice offers "Try again", which POSTs install | `ota.rs:649-654, 195-200`, `app.js:29, 1198` | user-visible | 1 | app owner | notices test: a check-class error renders "Could not check for updates" with no install action | | Q12 | Light mode fails contrast on every live text: molten `#B8731F` on white 3.81:1, on bone 3.38:1; ember `#E04A14` on bone 3.61:1; molten carries the private key, the address and the live states. `miner-ui-3.md` section 9 claims 4.5:1 everywhere | `app.css:29, 263, 338, 431` | user-visible | 1 | app owner | a node script over the token pairs asserts 4.5:1 for every text token in both schemes; CI runs it | | Q13 | Per-card rejects and faults never reach the row (the lolMiner line) | `app.js:291-309, 1290` | the project lead-visible on PC 1 | 2 | miner-community-lead | row meta "N rejected · M faults" when nonzero; view test | | Q14 | Windows first run raises a UAC prompt (firewall rule) before any screen explains it (part of Q3) | `ota.rs:981-1015`, `index.html:71-99` | user-visible | 1 | app owner | the firewall step runs after `setup_done`; the Cards screen names it | | Q15 | Mac first-run instruction stale: "Right-click > Open" (part of Q3) | `packaging/mac/README.md:51`, `site/miner.html` download copy | the project lead-visible on a fresh Mac | 1 | app owner | the download page and README name the Privacy and Security step until the certificate lands | | Q16 | No rate history: a 10-minute strip against HiveOS's hours | `app.js:1052-1127` | user-visible | 4 | app owner | one-hour per-card sparkline from an engine ring buffer; view test | | Q17 | Hill climb hidden until `tune_climb` is on the state | `index.html:341`, `app.js:1407` | operator-visible | 1 | app owner | the row shows whenever the engine answers `tune/goal` | | Q18 | Earnings never shows mined IGN; "£0.00 earned" is the first number on the tab | `index.html:250`, `app.js:1364` | user-visible | 2 | miner-community-lead | the tab shows blocks mined and the subsidy they earned in IGN, with the devnet line under it | | Q19 | 10 px type in five places, 9 px ruler | `app.css:196, 271, 272, 302, 491, 531` | cosmetic | 1 | app owner | no `font-size` under 11 px except the ruler | | Q20 | Design screens in `docs/design/app-screens/*.png` are a UI 1 app (v0.3.0 tiles, a Finality card the shipped UI lacks) | `docs/design/app-screens/` | cosmetic | 1 | app owner | screens regenerated from `?screen=` on the current build | | Q21 | AMD step line prints a percent as watts ("owed: a unit word for AMD") | `ember-tune.md:278` | operator-visible | 1 | miner-community-lead | the line reads "70% (an offset)"; unit test | | Q22 | A code comment naming the founder ships in the UI bundle (`// The list is ordered by performance (the project lead, 6 October 2026)`), and the ui bundle is outside every forbidden-string check | `app/igneum-app/ui/app.js:270`, `tools/ci/identity-check.sh` | cosmetic (identity rule) | 0.5 | app owner | `tools/ci` forbidden-string check covers `app/igneum-app/ui/`; the comment reads "(ruling of 6 October 2026)" | | Q23 | Two copy-law slips: "Votes lock the chain; leave it on." (aphorism), "The window can close. The miner keeps going" (two-beat) | `index.html:404, 81` | cosmetic | 0.5 | app owner | reworded; the grep below stays at 0 em dashes | **Error states, exact strings.** Engine down (four failed polls): pill "engine away" (`app.js:1437`); node line "Node: no answer", sub "the engine is not answering; the window reconnects by itself"; Mac host "The engine stopped. Quit and open Igneum Miner again." (`IgneumMiner.swift:233`); Windows "The engine stopped. Close this window and open Igneum Miner again." (`host.cpp:301`). Node down: "the node could not start" or the engine message, "the node is not running", "the external node went away" (`app.js:452-453`, `engine.rs:3163`); pill "node failed" (`app.js:1428`); digest box "the node is not running" (`app.js:1341`); canvas "waiting for the node to sync" (`app.js:1099`). Finality paused: nothing (Q2). Update server unreachable: "The update failed. Manifest signature: ." (`app.js:1360`, Q11). GPU lost: "Card removed: . Its worker stopped." (`app.js:95`), row word "removed", sub "unplugged; its worker stopped. The row goes in five minutes." (`app.js:303`); Code 43: ": not usable (Code 43). No worker runs on it." with "reboot with the card attached; if it persists, reinstall the driver with the card attached" (`app.js:100`, `detect.rs:55`). **Already better than the comparators.** Automatic efficiency tuning against a goal with the money consequence on screen (`app.js:408-419`; 37 to 41 percent more hashes per watt measured on PC 1, `ember-tune.md:254, 271`). Signed, hash-checked, rolled-back updates with a safe-moment rule and machine slots (`ota.rs`, `manifest.rs`). A reason word on every zero rate and hot-plug words (`ember-tune.md` 6a). A virtualised log drawer with time jump and error marks (`app.js:880-1050`). Confirmations in place, no dialogs. A tokenised local API with origin checks (`server.rs:102-125`). Dark-scheme contrast is good: bone 17.4:1, ash 7.0:1, ember 5.6:1, molten 11.0:1 on obsidian (`app.css:19`). Focus rings, `aria-live` strip, labelled switches and sliders, reduced motion honoured on every animation (`app.css:67, 113, 142, 148, 157, 420, 437, 466`; `app.js:1137`). None of the four comparators ship the first two. ### 3.2 Wallet **What it does today.** One desktop wallet (Igneum Wallet, Rust engine plus a local window; `igneum-wt-wallet/app/igneum-wallet/`: `engine.rs`, `vault.rs`, `hd.rs`, `tx.rs`, `finality.rs`, `node.rs`, `updater.rs`, `server.rs`; ui `index.html`, `app.js`, `lock-screen.js`, `update-card.js`), shipped as 0.1.4 (`site/downloads.json` wallet-mac, 19.6 MB; `release-0.3.15.md:90`). 0.1.5 (bundled 0.3.14 node, RPC on localhost) has a DMG and is unpublished (`release-0.3.14.md:82`). UI 3 is commit ab8dfdc on `wallet-ui-3` and `wallet-0.1.5`, not on master. No wallet inside the miner app (it holds the payout key, `wallet.json`, and a button "Open the wallet" to the site, `app/igneum-app/ui/app.js:826`). No web wallet. A MetaMask guide (`site/metamask.html`), an address page (`site/address.html`, noindex, balance plus blocks mined), a marketing page (`site/wallet.html`). Keys: BIP-39 (12 to 24 words, `hd.rs:23`), BIP-44 Ethereum path; vault Argon2id 64 MiB, 3 passes, XChaCha20-Poly1305 (`vault.rs:14-81`); key zeroised on lock (`engine.rs:392`); 24 words shown once, three typed back (`engine.rs:321`); raw key export behind password or Touch ID (`ui/index.html:271-275`). Chain ids 4461, 4462, 4463 (`metamask.html:233`). Signing: EIP-1559 value transfers only; no EIP-712, no `personal_sign`, no contract calls, no dapp provider, no hardware wallet (spec 8.5 item 2 decided, not built; `docs/fud-ledger.md:2058`). **Comparators and the exact gap** (approximate unless a standard is named). | Comparator | Their feature | Ours | |---|---|---| | MetaMask | Account list and HD derivation; address book; speed up and cancel a pending tx; `window.ethereum` provider; `eth_signTypedData_v4` | One account (`engine.rs:343` refuses a second import); an address field only (`ui/index.html:177`); no replace-by-fee; no provider; no EIP-712 | | Rabby | Pre-sign simulation and recipient risk flags | Review shows amount, fee and total (`app.js:293-295`) | | Frame | Ledger, Trezor, GridPlus signers; injected provider for any browser | Neither (spec 8.5 "MUST offer", no code) | | Monero GUI | View-only wallet; subaddresses; a settable node address with status | Node source engine-chosen, no settable endpoint (`wallet-ui-3.md:112`) | | KDX, Kaspium | Sync bar with peer count; per-transaction DAA score | "reading the chain, N of M blocks" (`app.js:175`), no peer count | | EIP-1193 | `request` provider | Used on the guide (`metamask.html:294`) | | EIP-6963 | Multi-provider discovery | Absent; the guide falls back to whichever extension owns `window.ethereum` (`:292`) | | EIP-3085 | `wallet_addEthereumChain` with `blockExplorerUrls`, `iconUrls` | `blockExplorerUrls: []`, no icon (`:225, 285-286`) | **Rows.** | Id | Row | File or screen | Severity | Hours | Owner | Gate | |---|---|---|---|---|---|---| | Q9 | Page vs UI 3; guide placeholders; empty explorer URL (section 1) | `site/wallet.html`, `site/metamask.html` | the project lead-visible | 6 | app owner, site owner | section 1 | | Q30 | Private key export copies to the clipboard with no clear and no timer | `igneum-wt-wallet/.../ui/app.js:129, 463` | user-visible | 2 | app owner | clipboard cleared after 60 s by the host; unit test on the timer | | Q31 | Wallet OTA trusts one key; no `revoked_keys`, unlike the miner's updater | `docs/plans/consequences-2026-10-05.md:36`, `updater.rs:7, 729` | operator-visible | 3 | app owner | updater carries `OTA_PUBLIC_KEYS [K1, K2]` and honours `revoked_keys`; the three-key test of the rig installer | | Q32 | No hardware wallet path despite spec 8.5 "MUST offer" | `docs/spec/08-client-security.md:36` | the project lead-visible | 24, or 0.5 to relabel | app owner (cryptographer reviews) | a Ledger signs one devnet transfer through the Ethereum app at chain id 4463; or the spec row reads "Designed, not shipped" | | Q33 | Single account, no second address, no address book | `engine.rs:343`, `ui/index.html:177` | user-visible | 6 | app owner | two derived accounts, a saved recipient reused in Send | | Q34 | `confirm()` on Remove wallet never renders in the Mac host | `app.js:598`, `wallet-ui-3-audit.md:189` | user-visible | 1 | app owner | in-page two-step card; snapshot test | | Q35 | Finality pause: node card "paused" or "not checkpoint-checkable without a node", rows stay "in block N"; public-RPC mode never says what to do | wallet `app.js:185, 321, 340` | user-visible | 2 | app owner | the row reads "in block N; finality is paused on the network" and the public-RPC line names the fix with a button | | Q36 | Reorged-out transaction: "in a block" then the row vanishes; no resend hint | `app.js:340`, `docs/fud-ledger.md:2154-2158` | user-visible | 3 | execution-engineer (node side), app owner (the row) | a transfer unwound in the P23 conformance run shows "reorged out, resending" and ends executed | | Q37 | The address page shows no finality state and "n/a" with a bare reason for balance | `site/address.html:205-209, 242-252` | cosmetic | 2 | site owner | the page shows the latest locked checkpoint and whether finality is active | | Q38 | The permanent phishing sentence differs between surfaces ("words or your key" on the wallet, "your key" in the miner) against spec 8.5's one verbatim sentence | `site/wallet.html:366`, wallet `ui/index.html:54`, `app/igneum-app/ui/index.html:97`, `08-client-security.md:39` | cosmetic | 0.5 | site owner, app owner | one sentence, grep-identical on all three | | Q39 | Vault file permissions not set explicitly (no `0o600` in `vault.rs`) | `vault.rs` | operator-visible | 0.5 | app owner | the file is created 0600; test | **Error states.** Node down: pill "no node", balance note "waiting for a node" (`app.js:160, 176`), the node line's raw engine message (`:186`; UI 3 replaces it with "the node stopped answering; trying again", `wallet-ui-3.md:199`). Pending versus final: "PENDING, waiting for a block", "IN A BLOCK, chain block N; final once a verified checkpoint covers it", "FINAL, under checkpoint N, certificate verified by this wallet", "FAILED, the execution failed; the fee was still paid" (`app.js:340`). Address page: "The API did not answer: " (`address.html:270`), "Not found." (`:247`). MetaMask page: "No Ethereum wallet found", "Waiting for the wallet...", "Cancelled in the wallet.", "The wallet refused: " (`metamask.html:292-296`). **Security notes.** Plaintext key only in engine memory, zeroised on lock and after each sign (`engine.rs:392, 563`); the window never holds the key (nonce-gated reveal, `server.rs:108`); local API on 127.0.0.1 with a random port, per-run token path and a host token (`server.rs:1, 39, 71`); chain id checked at quote and send (`engine.rs:549`). Testnet RPC is https (`testnet-go.md:13`); devnet is http to 127.0.0.1. Chain ids 4461 to 4463 were absent from chainid.network on 3 October (`docs/design/execution-layer.md:354`), not yet registered (`frontier.md:645`); from memory no listed chain uses them (approximate). **Already better.** The wallet verifies finality certificates itself with the node's consensus code (`finality.rs:101-121`) and never trusts the RPC's "locked"; MetaMask and Rabby trust the RPC's block tag (approximate). Touch ID is bound to the exact quote (address, value, nonce, chain id) with a 30 s one-use nonce (`engine.rs:539`). Argon2id at 64 MiB is above MetaMask's PBKDF2 default (approximate). The QR is drawn locally as SVG with the `ethereum:` URI and a checksummed address (`server.rs:140`). The MetaMask page says plainly that nothing on it asks for a seed (`metamask.html:172`). ### 3.3 Hub and relay **What it does today.** One Vercel project (`igneum-relay`, Neon database, Blob store; `relay/README.md:3-5`). `relay/ui.html` (583 lines) is the console at `/r//` (`relay/vercel.json:5-8`); `relay/index.html` is a placeholder. Functions: `relay/api/relay.mjs` (feed, items, files, inbox, register, name, role), `relay/api/console.mjs` (machines, jobs, builds, chain, log, results, tuning), `relay/api/wake.mjs` (public long-poll for the apps' jobs file, 30 a minute per IP, `relay/lib/wake.mjs:15, 42-54`). Auth: the 20-character token in the path or `x-relay-token`, or any of `RELAY_KEY`, `LOG_INTAKE_KEY`, `LOG_INTAKE_KEY_NEXT` in `x-igneum-key` (`relay/lib/relay.mjs:34-46`); only `task`, a `run` or `task` drop, `name`, `role`, `delete` demand the token (`relay/api/relay.mjs:114-115`). Two job models: relay `run` items polled every 20 s by `relay/clients/igneum-agent.ps1:165` and executed as administrator; and signed app jobs in `igneum-jobs.json`, whose status the console derives from `miner_logs` rows (`relay/api/console.mjs:172-188`). Seven tabs (`relay/ui.html:186-192`): Machines, Jobs, Builds, Chain, Work log, Results, Relay; refreshed every 15 s when visible (`:577`), server cache 10 s (`console.mjs:21`). Chain tiles: blocks, identities, hash estimate, difficulty, last lock, finality, peers, blocks per second (`:363-370`). Mac tools: `tools/relay.mjs`, `tools/console.mjs`, `tools/jobs.mjs`. Tests: 24 pass per file (`node --test relay/test` as a bare directory fails under Node 22.23.2 with MODULE_NOT_FOUND; the README's command needs the glob). **Comparators and the exact gap** (approximate). | Comparator | Their screen | Ours | |---|---|---| | Tailscale admin console, Machines | Last seen, OS and version, key expiry with "Disable key expiry", tags; an ACL editor with tests | Last seen and a red card after 180 s (`relay/ui.html:270, 273`); no credential state (one static token, `relay/lib/relay.mjs:36`), no per-machine key, no revoke; roles are labels never checked (`relay/api/relay.mjs:191-195`) | | GitHub Actions job view | Live streaming log with line anchors, per-step timing, re-run, artefacts, annotations at the top | One text blob in a modal (`relay/ui.html:319`); `duration_s` and the error lines that `tools/jobs.mjs:41-44` already parses are discarded by `console.mjs:184-187`; no re-run; results are separate items | | Vercel deploy log | Build steps, deployment list, instant rollback, diff between deployments | Builds tab shows the current manifest, the last CI fetch, 40 posts and a folder listing (`relay/ui.html:323-341`); no rollback, no manifest history, no diff | **Rows.** | Id | Row | File or screen | Severity | Hours | Owner | Gate | |---|---|---|---|---|---|---| | Q6 | Stale shown as live; "active" for 20 unlocked indices; "paused" with no since or cause; event flood (section 1) | `relay/ui.html:564, 367-368` | the project lead-visible | 4 | fleet agent, consensus-engineer | section 1 | | Q8 | X23 to X28 fixes off master; intake key can write and delete; token in URL (section 1) | `relay/api/console.mjs:249`, `relay/lib/relay.mjs:44` | operator-visible | 2 | fleet agent | section 1 | | Q40 | A hung job reads "running" forever: no elapsed time, no timeout, no "last line N min ago" | `relay/api/console.mjs:182-187`, `relay/ui.html:317` | the project lead-visible | 2 | fleet agent | a killed job reads "no report for 12 min" in red within one refresh | | Q41 | Jobs tab trusts the unsigned `igneum-jobs.json` while the apps verify the signed file | `relay/api/console.mjs:170` vs `tools/jobs.mjs:57-60` | operator-visible | 1.5 | fleet agent | the tab shows the envelope's signature state per file | | Q42 | Job result modal: no error lines first, no duration, no anchors (the Actions view) | `relay/ui.html:319`, `console.mjs:184-187` | the project lead-visible | 3 | fleet agent | a failed run opens with its five error lines and its duration first | | Q43 | Machines: no per-machine key state, no revoke, roles never enforced (the Tailscale view) | `relay/lib/relay.mjs:14`, `relay/api/relay.mjs:191-195` | operator-visible | 3 (after Q8) | fleet agent | a revoked PC's register is 403 and its card says "key revoked" | | Q44 | Builds: no rollback, no manifest history (the Vercel view) | `relay/ui.html:328-341` | operator-visible | 4 | fleet agent | one tap republishes the previous manifest and the fleet's OTA state shows it | | Q45 | "live feed unreachable: live feed unreachable" doubled; the whole Chain tab empties, losing Events and the infra card | `relay/api/console.mjs:210`, `relay/ui.html:357` | cosmetic | 0.5 | fleet agent | live feed down: tiles grey, Events and infra card still render | | Q46 | Replay: `register` and `task` have no nonce; a captured `task` POST re-queues a run | `relay/api/relay.mjs:153-167` | operator-visible (security) | 1 | fleet agent | a replayed `task` is refused; test | **Error states.** Relay API unreachable: header "offline: Failed to fetch" and a grey dot (`relay/ui.html:564, :32`); the panel is replaced only while it still shows "loading". Node down: a machine card goes red after 180 s with "silent 4h" (`:270, 273`; `SILENT_S`, `console.mjs:35`); a deliberate stop reads "stopped (quit) 2h ago" (`relay/lib/parse.mjs:134-140`); the live API down reads "live feed unreachable: " (`:357`); the observer stale reads "observer STALE N s" in red after 30 s (`:361`, `site/api/live.mjs:8`) while every tile keeps its last value. Finality paused: `tile('finality', f.active ? 'active' : 'paused', ...)` in amber `#FFB35C` (`:368, :17, :65`); the "last lock" tile amber too (`:367`); eight checkpoint rows with state and fraction (`:374`). Job hang: "running" until a `SUMMARY` with `finished_at` arrives (`console.mjs:179-182`); relay `run` items show "read" and never "done" (`tools/relay.mjs:58`). **Security, X23 to X28 on this tree.** X23: the tier split exists (`relay/api/relay.mjs:114-115`), no run signature, no per-machine HMAC, the intake key still writes the console (`console.mjs:249`). X24: every client builds `/r/$TOKEN/api` (`agent.sh:10`, `send.sh:11`, `igneum-agent.ps1:13`, `tools/relay.mjs:24`, `tools/console.mjs:27`). X25: `Arm-Restart` at top level and `/RL HIGHEST` (`igneum-agent.ps1:154, 72`). X26: feed to 500 with no retention; `delete` leaves blobs (`relay.mjs:48, 148-152`); `__DL_BASE__` substituted into bodies (`tools/relay.mjs:79`). X27: `from` is free text, `register` for any hostname (`relay.mjs:30, 153-167`). X28: constant-time compare and HSTS present (`relay/lib/auth.mjs:5-11`, `relay/vercel.json:38-41`); still a GET that acks (`relay.mjs:85`), `RELAY-REBOOT` anywhere in output (`igneum-agent.ps1:133`), no rate limit, `NOPASSWD:ALL` (`wsl-setup.ps1:40`), `sudo -S` with the password (`prover-setup.ps1:21`). All of this is what Q8 merges. **Already better.** One phone-first page, no login flow, seven tabs, 15 s refresh (`relay/ui.html:5, 577`); Tailscale, Actions and Vercel each take several screens on a phone (approximate). The machine card fuses miner STATUS, node log, app telemetry and OTA state and remembers an OTA line after it scrolls out (`console.mjs:61-84`); no comparator shows workload telemetry. Stale marking at 180 s drops the card from the total; stopped and silent are distinct words (`parse.mjs:16-21, 134-140`). Drop-anywhere upload direct to Blob with a progress bar and a 50 MB cap (`ui.html:506-534`). The wake long-poll is dependency-free and tested with a fake clock (`relay/lib/wake.mjs`). Headers: nosniff, frame DENY, noindex, no-referrer, no-store, HSTS on every path (`relay/vercel.json:21-43`). ### 3.4 Site **What it does today.** Thirteen static pages plus 404 (`site/index.html`, `litepaper.html`, `live.html`, `bench.html` 611 KB rendered from `docs/bench-log.md`, `evidence.html`, `ledger.html` 312 KB generated, `miner.html`, `miners.html` from `miner-bench.json`, `wallet.html`, `metamask.html`, `faucet.html`, `explorer.html`, `block.html`, `address.html`). `site/build.mjs:49-50, 357-372` injects `partials/head.html` (self-hosted fonts, tokens), `nav.html`, `footer.html`, stamps download links from `downloads.json`, inlines `journey.json`. `scrub.mjs` runs on bench and miners only (`build.mjs:171, 401`). Live data: index polls `/api/live` every 2 s (`index.html:704`; 10 s while off); the light client fetches `/api/checkpoint` and imports two noble libraries from cdn.jsdelivr (`verify/verify.js:5-6`); `live.html` polls every 2 s (`:665-666`); faucet POSTs `/api/faucet`. APIs (`site/api/`): `live`, `stats`, `supply`, `explorer`, `checkpoint`, `faucet`, `log`, all reading Neon tables written by `tools/observer`. Edge cache: live 1 s, stats and supply 10 s, explorer 5 s (`vercel.json`). The live HTML (one curl, 20:02Z) is 60,009 bytes, served with HSTS (preload), `X-Frame-Options: DENY`, nosniff, `Referrer-Policy: strict-origin-when-cross-origin`, no CSP; download buttons stamped "v0.3.14 · 45.4 MB"; "Public testnet: not yet open; the devnet build is here for people who want to look." **Comparators and the exact gap** (approximate unless a page is named). | Comparator | Their page or element | Ours | |---|---|---| | kaspa.org, ethereum.org | A Developers or Docs nav item | Nav is Litepaper, Live devnet, Engineering log, Miner, Wallet, Evidence, GitHub (`partials/nav.html:14-21`); builders land on a litepaper section (`index.html:335`) | | getmonero.org/downloads | Every platform, hashes and signing keys on one page | Split across `index.html:363-365`, `/miner#get`, `/wallet`; the homepage's Linux and HiveOS button links `/miner#get`, not a file; sha256 shown only for the HiveOS package (`miner.html:558`), none beside the Windows and Mac buttons, no signing key, no verify line | | z.cash, aztec.network | Named people, a transparency page | "one founder, pseudonymous" once in the litepaper; the Reddit review's key-powers table (round 4, section 3 row 8) absent | | kaspa.org, getmonero.org | Media kit, press kit, language switch, newsletter | None (`site/` has no `/press` or `/brand`) | | ethereum.org, kaspa.org | Light and dark | Dark only; only the litepaper honours `prefers-color-scheme` | | succinct.xyz, aztec.network | 1200x630 share cards | Home, litepaper, live, explorer use 256 px `og-small.png` with `summary` (`index.html:15-19`); miner, wallet, bench, evidence use the large card | **Lighthouse, estimated from source (approximate; no run).** Common: `lang="en"`, a skip link (`partials/nav.html:1`), one `:focus-visible` ring (`head.html:22`), self-hosted fonts with `font-display:swap`, two preloads and fallback metrics (`head.html:1-19`), no render-blocking third-party CSS, canonical, description, OG and twitter tags on every page. | Page | Performance | Accessibility | Best practices | SEO | What holds it down | |---|---|---|---|---|---| | index.html | 78 to 85 | 88 | 95 | 92 | 89 KB HTML, 35.0 KB inline JS, 24.0 KB CSS; two rAF canvas loops at display rate with no visibility or intersection stop (`:790, 853, 857`); 2,880 px webp with no srcset; h3 before the first h2 (`:344, 359`); ember on bone 3.08:1 at 15 px in the light section (`:119-120`), eyebrow `#7A776F` on bone 3.97:1 at 12 px (`:130`); 256 px OG card | | explorer.html | 88 to 93 | 90 | 100 | 85 | 32 KB, no images, no third party; `aria-live="polite"` on an 8-tile strip refreshed every 10 s (`:222`); not in the sitemap | | live.html | 85 to 90 | 88 | 100 | 85 | 64 KB, 35.6 KB JS; the canvas loop is capped at 30 fps and stops off screen (`:605-612`), good; `aria-live="polite"` on the 8-cell strip every 2 s (`:225`); canvas `aria-hidden` with no text alternative (`:241`) | **Rows.** | Id | Row | File or screen | Severity | Hours | Owner | Gate | |---|---|---|---|---|---|---| | Q5 | "final" during a pause; 90-word hero; 24 GB contradiction; thumbnail OG (section 1) | `index.html:345, 494, 810, 838, 15-19` | the project lead-visible | 3 | site owner | section 1 | | Q50 | No downloads page: hashes only for HiveOS, no signing key, no verify line, the Linux button goes to an anchor | `index.html:363-365, 516`, `miner.html:549-558` | user-visible | 2 | site owner | `/download` lists every artefact with version, size, sha256, the OTA key fingerprint and a verify command per OS; the Linux button links the file | | Q51 | `/miners` has 6 rows, two card models, "not measured" MH/W on every row; the eleven-card fleet table is not ingested | `miners.html:183`, `miner-bench.json` | user-visible | 2 | miner-community-lead | rows carry W and MH/W for every measured card | | Q52 | `dl\.igneum` in `forbidden-strings.txt` matches the public download host on three built pages; the scrub guards only bench and miners | `forbidden-strings.txt:25`, `build.mjs:171, 401` | operator-visible | 0.5 | site owner | the pattern becomes `dl\.igneum\.network/dl/(?!public/)` or `/dl/[0-9a-f]{12,}`, and the check runs on every built page | | Q53 | Sitemap misses `/miner`, `/explorer`, `/faucet`, `/ledger`; every lastmod is 4 or 5 Oct; 404 says "These four pages are everything the site has" | `sitemap.xml`, `404.html:138` | cosmetic | 0.5 | site owner | the sitemap is generated by the build from the page list | | Q54 | `aria-live="polite"` on strips refreshed every 2 s and 10 s (a screen reader hears eight values every refresh) | `live.html:225`, `explorer.html:222` | user-visible | 0.5 | site owner | `aria-live="off"` on the strips plus one polite status line on state change | | Q55 | Homepage canvases never stop off screen or cap their rate | `index.html:790, 853, 857` | user-visible | 1 | site owner | 30 fps cap and IntersectionObserver as `live.html:605-612` | | Q56 | Light-section contrast fails (3.08:1 links, 3.97:1 eyebrow) | `index.html:119-120, 130` | cosmetic | 0.5 | site owner | AA on every token pair; the same node script as Q12 | | Q57 | "devnet v0" eyebrow on the live page; the chain is v4 | `live.html:219` | cosmetic | 0.1 | site owner | matches the app's network word | | Q58 | Heading order (h3 before the first h2) | `index.html:359` | cosmetic | 0.2 | site owner | h2 or a styled div | | Q59 | No team, custody or key-powers page (Reddit round 4 artefact 8); "0 admin keys in consensus" tile unchanged | `index.html:611`; no file | user-visible | 1 (plus the project lead's policy call) | site owner | the key-powers table published; the tile links it | | Q60 | Roadmap dates versus the testnet: the journey says "Public testnet, Aug to Oct 2027" and the litepaper "Pools and the public testnet are August 2027", while `docs/plans/testnet-go.md` has `igneum-testnet-1` seeds up and a go checklist dated 5 October 2026 | `index.html:679`, `litepaper.html:719`, `docs/plans/testnet-go.md:1-4` | the project lead-visible | 0.5 (after the project lead decides which is true) | site owner | one date for the public testnet on the journey, the litepaper and the download notice | | Q61 | No Content-Security-Policy header on the site (the relay has none either); inline scripts throughout, two pinned CDN modules on the homepage | `site/vercel.json:7`, `relay/vercel.json:31-43`, `verify/verify.js:5-6` | operator-visible (security) | 2 | site owner | a CSP with hashes or nonces for the inline scripts and `script-src` limited to self and cdn.jsdelivr; every page renders with no console violation | | Q62 | Copy-law borderline headlines: "Mined by GPUs. Proven by fire." (the brand line, the project lead's call), "GPUs are back · for good", "Last hour's chip is already obsolete.", "Your coins. Final means final.", "Dates slip. Gates do not.", "Install. Start. The card mines and proves.", "Nothing is mined here." | `index.html:343, 344, 424`, `wallet.html` h1, `litepaper.html:678`, `miner.html` h1, `404.html` h1 | cosmetic | 1 | site owner (the project lead rules on the brand line) | each either kept by the project lead's word or reworded | **Reddit round 4, still open in the HTML.** Finding 6 (24 GB): half fixed (Q5). Finding 7 (eleven-card table): open (Q51). Finding 8 (admin keys): sentence added to the litepaper (`:659`), tile unchanged, no table (Q59). Finding 15 ("Monero's idea, finished for GPUs"): open (`index.html:444`, `litepaper.html:429`). Finding 16 ("devnet v0"): open (Q57). Finding 22: `/ledger` present, no nav item. Findings 2, 3, 5, 9, 10, 12, 13, 17, 19: fixed in the current files. "0% anyone else in the protocol" legend still live (`index.html:604`). **Error states, exact strings.** API unreachable: `/live` after three failed polls shows "OFFLINE", "api unreachable", the canvas frozen with "api unreachable, scene frozen" (`live.html:665, 600`); `/explorer` writes the fetch error into the table on first load and is silent after (`:342-345`); `/block` "The API did not answer: " (`:328`); the homepage "simulated preview · live feed unavailable" and "Live feed unavailable. This is a simulation and its counters count simulated blocks." (`index.html:870-872`). Observer stale over 30 s: "OFFLINE", "observer updated N min ago", "observer offline, last update N min ago" (`live.html:663`); explorer eyebrow "observer offline, last known" (`:318`); homepage "simulated preview · observer offline, last update N s ago" (`:708`). Finality paused: section 2. **Already better.** Self-hosted latin subsets with preloads and computed fallback metrics (`head.html:1-19`); ethereum.org and kaspa.org load heavier bundles (approximate). Zero third-party scripts except the two pinned noble modules; no analytics, no cookie banner. Honest live states: "simulated preview · observer offline", "finality paused: N% of weight silent", "api unreachable, scene frozen"; Etherscan shows nothing when its indexer lags (approximate). A browser-side BLS certificate verifier on the homepage (`verify/core.js`), which no comparator homepage has. A public API with a CI field contract (`api/stats.mjs:10-14`) and supply derived from the emission rule with an hourly coinbase check. HSTS with preload, frame DENY, nosniff, immutable font caching, skip link, reduced-motion rule on every page. A dated journey with pass or fail gates inlined at build, and a ledger page of every criticism; none of the six site comparators publish the equivalent. ### 3.5 Explorer **What it does today.** `explorer.html` polls `/api/explorer?blocks=50` every 5 s and `/api/stats` plus `/api/supply` every 10 s (`:342-343`); `block.html` fetches `?block=` or `?height=` once (`:276`); `address.html` fetches `?address=` once; `/block/:id` and `/address/:addr` are Vercel rewrites. Search classification is client-side (`lib/explorer.mjs:19-27`: hash, 0x address, bech32, height) with a server fallback for tx hashes (`api/explorer.mjs:49-59`). The data window is 24 hours of observer rows; balance needs `EXPLORER_EVM_RPC`, unset on the devnet deployment (`docs/plans/explorer.md` section 7). **Comparators and the exact gap** (approximate). | Comparator | Their feature | Ours | |---|---|---| | Etherscan, Blockscout | A transaction page: decoded input, logs, status, fee | None; a tx hash lands on `/block/#tx-` (`api/explorer.mjs:57`); `block.html:310` lists hash, from, to, value, bytes, with from and to "needs an EVM RPC" when unset | | Etherscan, Blockscout | Address page with transactions, token transfers, nonce, contract tab | Blocks mined, 24-hour earnings, balance only with an RPC (`address.html:209, 281`) | | Blockscout | Hosted API reference (Swagger), rate-limit statement | A card on `/explorer` (`explorer.html:232-240`) and `docs/api/public-stats.md` in the repo | | explorer.kaspa.org | DAG view inside the explorer; merge-set order per block | DAG view lives on `/live`; block page has blue work, parents, children, mergeset blues and reds (`block.html:285-325`), no prev and next navigation | | mempool.space | Mempool as the product, fee charts | Mempool count is one tile on `/live` (`live.html:233`); no charts on the explorer | | All four | Per-block status (final, confirmations) | None for an ordinary block (Q10) | **Rows.** | Id | Row | File or screen | Severity | Hours | Owner | Gate | |---|---|---|---|---|---|---| | Q10 | No finality state; failures swallowed (named under the ten in section 1) | `api/explorer.mjs:16-25`, `explorer.html:222, 309, 342-345` | user-visible | 3 | site owner | during a forced pause every block row carries "paused" and the strip tile reads the Q1 reason; cut the API: the table dims with "api unreachable, last update N s ago" within 10 s | | Q63 | No tx page, no hosted API reference | `api/explorer.mjs:57`, `explorer.html:232-240` | user-visible | 4 | site owner | `/tx/` with status, fee, from, to, input bytes, shard and proof state; `/api` page with every field and the cache times | | Q64 | Address page: no transaction list, no finality (Q37 covers the finality half) | `address.html` | user-visible | 3 | site owner | the last 50 transfers in and out, each with its block and state word | | Q65 | No prev and next block navigation; no block-by-height neighbours | `block.html` | cosmetic | 1 | site owner | two links from `selected_parent` and the first child | | Q66 | Blocks per second inverted against the other pages ("1.2 s per block" vs "0.9 blocks / s") and difficulty "126.81 M" with a space against "126.81M" on `/live` | `explorer.html:330-331`, `live.html:329` | cosmetic | 0.5 | site owner | section 4.3's one formatter | **Already better.** Block page depth for a DAG: parents, children, mergeset blues and reds, selected parent, shards with prover and lag, checkpoint weight fractions and certificates (`block.html:285-325`); explorer.kaspa.org shows less of the mergeset (approximate). Client-side search that accepts a height, a hash, an 0x or bech32 address with no round trip (`lib/explorer.mjs:19-27`). Five-second edge cache with a stale flag from the observer, so a stale page says so on first load. ### 3.6 Pool **What it does today.** `igneum-pool`, a Rust crate (`pool/Cargo.toml`, 12 source files) speaking the spec 09 protocol in its v0 form: newline JSON over plain TCP (`pool/src/server.rs:1-2`, `protocol.rs:1-9`), the project's own message set (`hello`, `welcome`, `authorize`, `seeds`, `template`, `job`, `share`, `share_result`, `solution`, `stats`; `protocol.rs:21-170`). Every share is CPU-verified on the node's warp verifier (`verify.rs:52-64`; 1.35 ms isolated, 2.1 ms under load, `docs/plans/pool.md` section 5). PPLNS only, window in blocks of weight, default 2 (`pplns.rs:1-5`, `config.rs:67`); fee 1 percent, minimum payout 1 IGN (`config.rs:65-66`); EIP-1559 transfers from the pool's coinbase key, at most 16 per round, failed receipts re-credited (`payout.rs:197-299`). One page, `pool/web/index.html`, baked into the binary (`api.rs:166-174`): a stat strip, connect card, address lookup, blocks, payments, API card. Stats API `/api/stats`, `/api/blocks`, `/api/payments`, `/api/miners/`, `/api/pool-stats`, `/health` (`api.rs:185-200`) with fixture-pinned field contracts (`api.rs:255-272`). Operator surface: a `STATUS` line every 30 s on stdout (`main.rs:152-174`). Deployment: a written, unexecuted systemd unit behind Caddy (`pool/README.md:75-116`; "not executed", `docs/plans/pool.md` section 8). Nothing is deployed. The horizon tree equals `igneum-wt-pool-rebase` HEAD 269364a. **Comparators and the exact gap.** | Comparator | Their feature | Ours | |---|---|---| | 2miners | Per-worker hash rate charts over time; payout history per miner; worker-offline alerts by email or Telegram; estimated earnings per day; luck | A single 10-minute and 1-hour number, samples kept 15 minutes and not persisted (`state.rs:704-705, 779-781`); the API returns 50 payments and the page renders none of them (`api.rs:148`, `web/index.html:181-194`); no contact field (`protocol.rs:50-60`); no earnings estimate; luck defined inverted against MiningPoolStats' convention (`api.rs:72-74`) | | WoolyPooly | Solo mode; worker offline alerts | `scheme` hard-coded `"pplns"` (`server.rs:53`, `api.rs:80`); no alerts | | Kaspa acc-pool (approximate) | Prometheus and Grafana | No `/metrics`; stdout only | | Stratum v2 reference (github.com/stratum-mining/stratum, not cloned, approximate) | Noise-encrypted transport, job negotiation, binary framing, header-only mining | Plain TCP while spec 9.3 demands TLS 1.3 and `binding` is sent empty (`server.rs:1-2`, `protocol.rs:58-60`, `09-pool-protocol.md:37, 43, 49`); mode A only; JSON lines, and the `template` ships the whole `RpcRawBlock` to every member every second (`node.rs:147-164`) | **Rows.** | Id | Row | File or screen | Severity | Hours | Owner | Gate | |---|---|---|---|---|---|---| | Q67 | Member transport is plaintext TCP against spec 9.3's TLS 1.3 with session binding; `binding` ignored | `server.rs:1-2, 115`, `protocol.rs:58-60` | user-visible (hijack risk) | 8 | consensus-engineer | rustls listener; a replayed `authorize` on a second connection is refused (O-9.7) | | Q68 | Node down shows "difficulty 0, DAA 0" and a ramp-day-0 reward; no "node unreachable" state | `node.rs:350-368`, `web/index.html:163-164` | the project lead-visible (once deployed) | 2 | miner-community-lead | the page renders "Node unreachable since