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>
101 lines
12 KiB
Markdown
101 lines
12 KiB
Markdown
# 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).
|