igneum/docs/plans/miner-ui-4.md
igneum-labs 59bbf46689 miner-ui-4: an external node that goes away hands the ports to the app after 60 s
The mode (own | external | none) is re-decided every 5 s while the app reads or refuses another node; gone for 60 s, the app starts its own node and says so in Activity. node.mode and node.mode_reason in api/state and on the Node details. Pure extnode::step with the goes-away test first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 23:31:42 +00:00

12 KiB
Raw Blame History

Igneum Miner UI 4: Overview, Cards, Ember on the row

6 October 2026, evening. the project lead: "add Cards under its own tab, and have Overview and the cool mining demo on the main page with the revamped C option", then "incorporate Ember into the Cards tab beautifully", then "I would like the new miner UI to go into .15". Branch miner-ui-4 from the 0.3.15 tip 6ea9497 (untouched), worktree ../igneum-wt-miner-ui-4. Rust: one route (GET /live-dag.js, server.rs), nothing else.

1. The rail

Overview, Cards, Earnings, Prove, Settings; Logs and Quit in the foot; a bottom tab bar under 720 px.

2. Overview

Block What
The hero (the project lead's pick, C) the fleet rate at 84 px on a graphite-to-obsidian fade, W with MH/W, £ a day at the user's price, blocks (this run, lifetime) beside it; Start/Stop as the full-width bar under it with its state sentence at the right
The chain, live the site's live-dag.js (a byte copy at app/igneum-app/ui/live-dag.js, served by the engine at /live-dag.js), mounted on the engine's own api/live with mine: 'you' so this machine's lane reads "you" and its blocks take a molten ring; the legend words in HTML under the canvas: yours, pending, included, excluded, proven, locked; Pause; a tooltip per block. The engine's /api/live is OWED (it maps the local node's RPC into the site's reply shape: {ok, state, blocks[{hash, ts, blue_score, daa, parents, chain, color, miner, locked, final, shards, proven}], finality.checkpoints}); until it exists the module reports failed and the app's own 10-minute blocks strip draws in its place with the note "The full chain view needs the next engine update".
The node line as 0.3.15, Details under it
Activity the event feed in the app's voice (the strip canvas moved into the chain card as the fallback)

3. Cards

Block What
The Tuning strip the goal control (Efficiency, Balanced, Maximum) with "about £X a day at Y W"; the fleet saving ("Ember saves 84 W, £0.56 a day over 1 tuned card", View.fleetSaving); Tune all; the schedule line; Power control only when an NVIDIA card has it off
The rows as 0.3.15 (state word with its reason, MH/s, W with MH/W, £ a day, °C, the switch, the chevron) with Ember as the second layer
The flame mark the brand mark's two polygons, 16 px, outline when not tuned, filling bottom-up with step k of n while tuning (molten), solid ember when tuned, half grey when measured only
The Ember line "Tuned 2 h ago · 2470 MHz at 80% · next check Sunday · saves 84 W, 0.16% of rate"; "Tuning: step 4 of 9 · about 7 min left" with the bar; "Measured, not tuned · Power control is off"; "Not tuned yet · starts after 2 min of steady mining"; the fleet pause and a stop in the same place
The W cell before → after when tuned (tune_before_watts → the live draw)
The MH/W cell a 44 px sparkline of the ladder's MH/W per step, the chosen step as a dot
Tune / Retune / Stop the row's button; Retune once tuned
The chevron the cap slider, the applied or Retry line, memory and clocks, the card's own goal (As the fleet, Efficiency, Balanced, Maximum: POST api/tune/goal {key, goal}, "" clears; a build without it gets a toast), Retune, the Tuned line, the curve (W across, MH/s up, every step a dot, the chosen ringed, a rejected one hollow), identities, the facts

Fields read (Ember Tune 5429e82, on the 0.3.16 lane): tune_before_watts, tune_before_mhs (step 0 of the last full plan; 0 = never; the UI falls back to tune_curve[0] only for a full plan on an older build and claims no saving for a confirm or baseline plan), sweep_watts, sweep_mhs, sweep_eff, tune_curve, tune_goal, tune_step, tune_steps, tune_eta_s, sweep_state, sweep_note, tune_floor (asked for). View.ember and View.beforeOf carry the rules; view.test.mjs covers every state.

4. Screenshots (the mock on port 4318 with the rig state, the 5090 tuned, the 4070 at step 4 of 9)

docs/plans/miner-ui-4/u01-overview-dark-1440.png, u02-overview-light-1440.png, u03-cards-dark-1440.png (the 5090's details open: cap, goal, Retune, the curve), u04-cards-light-1440.png, u05-overview-dark-390.png, u06-cards-dark-390.png, u07-overview-light-390.png, u08-cards-light-390.png, u09-overview-mac-dark.png. The chain scene in the shots is the mock's /api/live (four lanes, one block a second, "you" ringed); on a real 0.3.15 engine the fallback strip shows until /api/live lands.

5. Owed

What Who
GET /api/live on the engine: the local node's blocks over the window, mapped into the site's shape, this machine's vote keys as the lane "you" engine
card.tune_floor Ember (said yes)
watts_total, pounds_per_day for the tray tip; address.balance_wei; price_gbp_per_ign engine (from miner-ui-3)
Re-copy site/live-dag.js over app/igneum-app/ui/live-dag.js when the site agent's audit commit lands, so the two stay identical the shipper at merge

Tests: node --test app/igneum-app/ui/*.test.mjs: 37 pass.

6. The second tip (20:0x UK): /api/live on the engine, the copy sweep, the project lead's goal rulings

What Where
GET /api/live on the engine: the observer's /api/live (the URL ota.rs already polls for the identity count) through curl, cached 2 s per window, this machine's blocks marked as the lane "you" by matching the block's miner id (the first 8 hex of its vote key hash) against every card's ids; state.you_blocks and state.source: "observer" added; {ok:false, error} when the observer is unreachable or the build has no observer address, which the UI meets with its own blocks strip. Four unit tests on the shaping from a fixture in the observer's shape (live::tests). The local node speaks gRPC and wRPC only and the engine carries no client for either (no TLS, no websocket stack), so a local-node source is OWED (a JSON endpoint on igneumd, or a wRPC client here); the reply shape stays the same when it comes. src/live.rs, src/server.rs, src/main.rs, ota::live_api_from made pub
The copy sweep: 150 strings, every one listed in docs/plans/miner-ui-4-copy.md ui/index.html, ui/app.js, the tests
the project lead's rulings: the per-card inherit option is "Default · Balanced" (grey, following the shared goal); "Default" wherever a per-card setting follows the shared one (the cap's "Back to Default"); the Cards strip is titled "Ember Tune"; every goal option carries its meaning as a tooltip (Efficiency "most hashes per watt, a little rate traded", Balanced "a little rate for most of the saving", Maximum "most hashes, watts second", Default "the goal set for all cards") ui/index.html, ui/app.js

The Rust tests did not run on this Mac (its cargo predates the lock file's version 4); the box runs them at the merge, as the rule says. UI tests: 37 pass. Screenshots u01, u03, u04, u06 reshot after the sweep.

7. Pinned to the next engine (ember-tune fd03f35, then b55bc1f), 20:5x UK

Numbering (the shipper, 21:3x UK): 0.3.16 is tonight's corrective cut, the same release as 0.3.15 (this branch's ui/ at 872f1c9) under a new number; the engine fields and the pinning below ship as 0.3.17 with miner-ui-4's tip e31acab.

Field Where the UI reads it
mining.watts_total, mining.pounds_per_day the Overview hero and Earnings use the engine's figures when present (View.fleetWatts, View.fleetPounds), else the UI's own sums as before
address.balance_wei (string or null), address.balance_age_s (-1 until read), address.balance_note, address.price_gbp_per_ign (null until a signed manifest carries a price) Earnings' first row: "12.3456 IGN · in the wallet · read 20 s ago" (View.balanceWords); a priced balance shows £ with "at the signed price"; an older engine without the field keeps the row as it was
card.tune_floor the Ember line's "(the lowest clock Ember will set)"
/api/live reply source ("node" when igneumd answers igneum_getRecentBlocks, "site" until then; pending: true before the first body) the chain scene's note (View.liveNote): "From your node. Your blocks are ringed." or "From Igneum’s observer. Your blocks are ringed."; "you" is matched in the UI from every card's vote-key ids whatever the source names the lane

At the 0.3.17 merge src/live.rs is ember-tune's (the fuller one, node first, site second); this branch's 0.3.15 file is superseded and its source word is "site" too, so nothing in the UI changes between the two.

8. N4 (the frozen tip), the capture route, and PC 2's light render (6 October 2026, 23:0x to 23:4x UK)

What Where
N4, the app half: sync_decision_v2 (the old rule AND the watch line's tip_age_s at or under 120 s AND peers >= 1; the cause word frozen / no peers / syncing / behind on node.sync_cause; the state word "behind" or "no peers", never "synced" on a frozen tip); is_stall_exit (code 45 or a "STALLED '" tail line; the first restarts the miner, the second since the node started restarts the node and re-dials its peers); node.tip_age_s, node.stall_exits. The known-failed case is the first test. UI: "Node behind · 1 peer · 133,000 blocks · last block 53 min ago" with the cause line, the pill "Node behind", the big button's "waiting: the node is behind the chain". The node lane's miner half is ca3-v4-0316. src/engine.rs, src/state.rs, ui/app.js, view.test.mjs
(b) a screenshot from the machine itself: POST /api/shot {name} writes SHOT <path> on the host protocol (beside URL, STATE, EXIT); the Mac host answers with WKWebView.takeSnapshot, the Windows host with WebView2 CapturePreview (PNG), to <data root>/shots/igneum-<name>-<unix>.png (live::shot_path, the name reduced to [a-z0-9-]); relay/playbooks/app-shot.ps1 asks and waits, a collect job's glob shots/*.png brings the file back. Read-only. The Swift parses (swiftc -parse); the C++ builds on the GitHub runner at the cut (BUILD-APP.bat already links shlwapi.lib). src/server.rs, src/live.rs, app/mac/IgneumMiner.swift, app/windows/host.cpp, relay/playbooks/app-shot.ps1
The light-theme showcase capture: Edge headless on PC 2 wrote nothing in three takes (154.0.4258.53, exit 0, no stderr, in the app's elevated session); PC 2's api/state and api/live were fetched by relay/playbooks/app-state-grab.ps1 into the data root and collected, and the ui-mock replayed them (IGNEUM_MOCK_STATE, IGNEUM_MOCK_LIVE) through the real 0.3.16 UI on the Mac: docs/plans/miner-ui-4/pc2-0.3.16/pc2-overview-light-1440.png, pc2-cards-light-1440.png, pc2-overview-dark-1440.png, captioned "rendered from PC 2's live state at 23:33 UK" (DESKTOP-KMCV30N, RTX 5090 at 103 MH/s and 317 W, node synced, 5 peers). Handed to the site lane. tools/ui-mock/server.mjs, relay/playbooks/app-state-grab.ps1
The fleet watts count mining and tuning cards only (PC 2's idle integrated card drew 48 W and was summed); the Ember off-state line no longer promises a start. ui/app.js

Tests: UI 39; the app crate on the box 152 passed (the N4 pair, the shot path, the four live tests among them).

9. 0.3.17 fold (7 October 2026, 23:20 UK)

  • Rebased onto a9eb58f1 (origin/release-0.3.15). ui/ comes wholly from this branch; Ember's finality-paused hunk (ember-tune 1357d280) is folded in as View.finalityWords(f): while finality is paused the node sub-line and the lock row say "paused" and nothing calls a lock final. Test in view.test.mjs (41 UI tests).
  • src/extnode.rs reads the node lane's igneum_getNodeInfo shape: params (every resolved consensus field, compared value by value against the manifest's keys), network (must be equal), powEngine ("stub" = refused on the port check, and a fault on the app's own node: node state stub, pill "Node fault", one error event). 168 box tests.
  • src/live.rs is Ember's merged module at the merge (nothing of this branch's lives there any more).
  • External node re-decided (the Mac, 7 October 2026 00:18 UK: the hand node left and the app sat on "node stopped" all night). While the app reads another node, or refused one, it checks the port every 5 s; gone for 60 s (extnode::TAKEOVER_WAIT_S) it starts its own node on the freed ports and says so in Activity; a node back inside the wait is kept. api/state node.mode own | external | none with node.mode_reason, shown under the Node details. Test an_external_node_that_goes_away_hands_the_ports_to_the_app_after_the_wait (169 box tests).