igneum/tools/ui-mock/server.mjs
igneum-labs 86d4fd1fef app-ia-26 items 6 and 7: Settings in 4.4's order, Updates holds the Interface row, the copy and the units
The founder's audit (docs/design/app-audit-2026-10-08.md, 4.4, items 6 and 7). Settings reads, in this order: Wallet
(the address, Change address, Show my key, Open the wallet), Node (the network cards, one line of state, the facts
behind Details), Updates (the version and its update, "Install updates by itself", the Interface row "Interface 1.0.0,
built in" with one sentence of help and the built-in switch; the Interface card is gone, T-R21 and A19), Power
control, Electricity, Heat mode (the switch label is five words; the paragraph is the help, A20), Proving (the shard
card and the switches from the old Prove page), This machine, Logs, Advanced, The dev fee last. Item 7: no
text-transform on a unit label (.tot .k, A8); the key sheet reads "Settings can show it again"; the setup's card note
drops "The card row has a slider." (the slider is on Tune); no em dash anywhere in the window. The mock's ladder
carries the engine's earned windows on every scenario (the audit lane's ask: figures, not "reading", when the founder
looks), and tools/ui-mock/shots-audit-26.txt is the audit's shot list for item 8.

Tests known-failed first on build-2 (once.test.mjs: the eleven sections in order with T-R21, A19 and A20; T-A8 with the
em-dash grep), then green; 125 UI tests green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 14:28:54 +00:00

402 lines
57 KiB
JavaScript

#!/usr/bin/env node
// A stand-in for the Igneum Miner engine, for working on app/igneum-app/ui without touching a running miner.
// Serves the UI files at /t/mock/ (the same path shape as the engine) and replays the recorded API responses in
// recorded/ with every timestamp shifted to now. POSTs are logged and answered {ok:true}; a few (pause, resume,
// settings) are applied to the in-memory state so the dashboard reacts. Nothing here reaches the real engine.
//
// node tools/ui-mock/server.mjs [port] default 4310
// open http://127.0.0.1:4310/t/mock/?scenario=live
//
// Scenarios (set with ?scenario= on the page URL, or GET /mock/scenario?set=<name>):
// live the recorded state as it was (Apple M5 Max mining, node synced)
// syncing node syncing, 41,000 headers, blocks climbing at a steady rate (the ETA)
// nodedown node failed and restarting, miner waiting
// nocards no GPU found (cards screen with ?screen=cards, and the dashboard)
// integrated one integrated card only, off by default
// clock clock skew that blocks mining
// paused miner paused
// biglog 12,000 log lines with errors and program swaps (virtualised list)
// fresh setup not done (welcome, cards, address screens)
// job a remote job running
// firstwait miner-ui-5: mining, no block yet (the Poisson count-up), the ladder's first rung
// firstblock miner-ui-5: the first block a minute ago (the block card, not yet seen by a window), the key under the dust line
// secondblock first-block-21: block 2 two seconds ago, ten minutes after the first; the ordinary accepted pulse (the
// block on the strip, the activity line, "2 blocks this run"), no card; ladder.first_block_shown true
// ladder miner-ui-5: every rung walked (vote, signature, full window, rank 3, streak, a paid shard)
// drivers driver-check: the 5090 fine, the 9070 XT and the Arc with an Install offer (also drivers-running,
// drivers-reboot, drivers-error, drivers-mac)
// GET /api/ladder answers the chain facts in src/chainfacts.rs's shape (the devnet's params, this key ranked);
// POST /api/card accepts the block card's PNG and answers a path.
// euro currency-21: region Germany, 38.69 c/kWh, every money line in euro (add &lang=de-DE for German placement)
// rig gpu-logos: one card per vendor (RTX 5090, RX 9070 XT, Arc B580, a UHD 770 iGPU off), every mark on one page
// tiers tiers-25: the 5090 and the 5080 with their three tiers measured, Balanced in force (tiers-first: the install's
// first minutes, nothing measured; tiers-mac: the Apple M5 Max with no lever, one stock entry; tiers-flip: the class v5
// crossing, the 5090 re-measuring with its v4 tiers out of date and the knee note, the 5080 already on v5)
// tiers-table tiers-table-26: the install's first minute with the three tiers from the team's table (the 5090 measured, the
// 5070 Ti estimated), nothing measured on these cards yet
// earn-held the Earnings tab under a job holding the card (the projection paused with the job's name); earn-still: the chain
// standing still for 15 min (Network paused); both with the earned 24 hours from the ladder
// knob knob-24: the 5090 locked at 1,300 MHz with the knee at 1,200 on its curve, the 4070 in the clock search
// (knob-search: both cards searching)
// heat Ember Heat (mission item 7): a 5090 and a 4070 in heat mode holding 20 °C (heating, 60% duty), the
// region United Kingdom at the Ofgem price; ?page=mine|tune|earnings|settings for the four pages (app-ia-26)
// The log grows by a few lines a second in every scenario, so follow and scroll-to-bottom can be exercised.
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const uiDir = path.resolve(here, '../../app/igneum-app/ui');
const coin = path.resolve(here, '../../brand/igneum-coin-1024.png');
const port = parseInt(process.argv[2] || '4310', 10);
const PREFIX = '/t/mock';
// IGNEUM_MOCK_STATE=<file> replays a state captured from a real machine (relay/playbooks/app-state-grab.ps1) instead of
// the recorded one; IGNEUM_MOCK_LIVE=<file> serves that machine's /api/live reply as it was (timestamps shifted to now)
const recState = JSON.parse(fs.readFileSync(process.env.IGNEUM_MOCK_STATE || path.join(here, 'recorded/state.json'), 'utf8'));
const recLive = process.env.IGNEUM_MOCK_LIVE ? JSON.parse(fs.readFileSync(process.env.IGNEUM_MOCK_LIVE, 'utf8')) : null;
const recLog = JSON.parse(fs.readFileSync(path.join(here, 'recorded/log.json'), 'utf8')).lines;
const t0 = Date.now() / 1000; // server start
const shift = t0 - recState.now; // recorded clock -> live clock
let scenario = 'live';
let paused = false;
let seq = 0;
let log = [];
const posts = [];
function clone(v) { return JSON.parse(JSON.stringify(v)); }
function now() { return Date.now() / 1000; }
function seedLog() {
seq = 0; log = [];
for (const l of recLog) log.push({ seq: ++seq, t: l.t + shift, src: l.src, err: l.err, text: l.text });
if (scenario === 'biglog') {
const base = log[log.length - 1].t - 3600 * 2;
const srcs = ['node', 'node', 'node', 'miner1', 'watch', 'app'];
for (let i = 0; i < 12000; i++) {
const src = srcs[i % srcs.length];
const t = base + i * 0.6;
let text, err = false;
if (i % 1500 === 1499) { src === 'app'; text = '[build] hourly program changed: new program ' + i; }
else if (i % 977 === 500) { err = true; text = 'worker: kernel launch failed (code 702) on identity mac-d937c69d-1; restarting in 5 s'; }
else if (src === 'node') text = '2026-10-04 18:00:00.000+01:00 [INFO ] PoW accepted ' + (i * 2654435761 >>> 0).toString(16).padStart(8, '0') + 'c954de57e97dc514cd8a92d by igneum-lottery-v2-bound (daa ' + (30000 + i) + ', nonce 0x' + (i * 97).toString(16) + ')';
else if (src === 'miner1') text = (t).toFixed(3) + ' ACCEPTED block nonce=0x' + (i * 31).toString(16) + ' (gpu worker, cpu re-check ok, identity mac-d937c69d-1)';
else if (src === 'watch') text = t.toFixed(3) + ' node1 blocks=' + (30000 + i) + ' headers=' + (30000 + i) + ' daa=' + (30000 + i) + ' tips=1 peers=4 synced=true';
else text = 'status: accepted ' + (5000 + i / 6 | 0) + ' blocks, 17.0 MH/s, mining | node ' + (30000 + i) + ' blocks, 4 peers, synced';
log.push({ seq: ++seq, t, src: i % 1500 === 1499 ? 'app' : src, err, text });
}
log.sort((a, b) => a.t - b.t); log.forEach((l, i) => { l.seq = i + 1; }); seq = log.length;
}
}
// a trickle of new lines so the drawer has something to follow
let tick = 0;
function growLog() {
const t = now(); tick++;
const h = (t * 1000 | 0).toString(16);
log.push({ seq: ++seq, t, src: 'node', err: false, text: new Date(t * 1000).toISOString().replace('T', ' ').slice(0, 23) + ' [INFO ] PoW accepted ' + h + 'c954de57e97dc514cd8a92d by igneum-lottery-v2-bound (daa ' + (39571 + tick) + ')' });
if (tick % 4 === 0) log.push({ seq: ++seq, t, src: 'miner1', err: false, text: t.toFixed(3) + ' ACCEPTED block nonce=0x' + h + ' (gpu worker, cpu re-check ok, identity mac-d937c69d-1)' });
if (tick % 10 === 0) log.push({ seq: ++seq, t, src: 'watch', err: false, text: t.toFixed(3) + ' node1 blocks=' + (39571 + tick) + ' headers=' + (39571 + tick) + ' tips=1 peers=4 synced=true' });
if (tick % 45 === 0) log.push({ seq: ++seq, t, src: 'app', err: true, text: '[error] mock: a sample error line so "last error" has somewhere to jump' });
if (tick % 120 === 60) log.push({ seq: ++seq, t, src: 'app', err: false, text: '[build] hourly program changed: new program (mock swap)' });
while (log.length > 40000) log.shift();
}
setInterval(growLog, 1000);
// /api/live for the embedded DAG scene (site/live-dag.js): a synthetic chain at one block a second, four miner lanes
// ("you" is this machine), parents into the last two seconds, the selected chain through "you" and "miner-2", blue
// and red colouring, proofs filling two blocks in three after 20 s, a checkpoint band every 30 s locked after 10 s.
const LIVE_T0 = Math.floor(Date.now() / 1000) - 400;
function liveNow(windowS) {
const t = Math.floor(now()), from = t - Math.min(300, Math.max(30, windowS)) - 25, blocks = [];
const lanes = ['you', 'miner-2', 'miner-3', 'miner-4'];
let prev = [];
for (let s = from; s <= t - 2; s++) {
const seed = ((s % 100000) * 7919 + ((s >> 3) % 97) * 131) % 1000;
const miner = lanes[seed % 4], hash = 'b' + s.toString(16).padStart(10, '0');
const chain = miner === 'you' || miner === 'miner-2' ? (seed % 7 !== 3) : (seed % 5 === 0);
const color = seed % 11 === 0 ? 'red' : (s < t - 4 ? 'blue' : 'pending');
const proven = color === 'blue' && s < t - 20 && seed % 3 !== 0;
const shards = proven ? [{ state: 'paid' }] : color === 'blue' && s < t - 8 ? [{ state: 'proving' }] : [];
const parents = prev.slice(-2).map(b => b.hash);
const locked = (s - LIVE_T0) % 30 === 0 && s < t - 10;
const b = { hash, ts: s * 1000, blue_score: 194000 + (s - LIVE_T0), daa: 198000 + (s - LIVE_T0), parents, chain, color, miner, locked, final: s < t - 40, shards, proven };
blocks.push(b); prev.push(b); if (prev.length > 3) prev.shift();
}
const checkpoints = [];
for (let s = from; s <= t - 2; s++) if ((s - LIVE_T0) % 30 === 0) checkpoints.push({ index: Math.floor((s - LIVE_T0) / 30), hash: 'c' + s.toString(16), blue_score: 194000 + (s - LIVE_T0), daa: 198000 + (s - LIVE_T0), state: s < t - 10 ? 'locked' : 'pending', fraction_total: s < t - 10 ? 0.71 : 0.44 });
return { ok: true, now: t, state: { stale: false, age_s: 1, height: 198000 + (t - LIVE_T0) }, blocks, finality: { checkpoints } };
}
// miner-ui-5: this machine's ladder record (state.ladder, src/ladder.rs) per scenario
function ladderNow(t) {
// app-ia-26: the engine's earned windows (earned-windows-26) on every scenario, the recorded machine's day (253 blocks, 3 shards), week and lifetime (8,054 blocks) at 4.88 IGN
const earnedBase = { h24: { wei: '1234640000000000000000', ign: 1234.64, blocks: 253, shards: 3, priced: true }, d7: { wei: '8930400000000000000000', ign: 8930.4, blocks: 1830, shards: 19, priced: true }, all: { wei: '39303520000000000000000', ign: 39303.52, blocks: 8054, shards: 61, priced: true }, estimated: false };
const base = { earned: earnedBase, first_block_at: 0, first_block_card: '', first_block_hash: '', first_block_daa: 0, days_mined_30: 0, blocks_30d: 0, blocks_today: 0, days: [], cards_first: {}, blocks: [], votes_signed: 0, last_vote_index: 0, last_vote_at: 0, last_signed_locked: 0, signed_locked_count: 0, streak_s: 0, shards: [], milestone: null, first_synced_at: t0 - 2800, first_mining_at: t0 - 2700, first_block_shown: false };
const hash = '9f3a7c1e5d2b4a6f8e0c1d3b5a7f9e2c4d6b8a0f1e3c5d7b9a2f4e6c8d0b1a3c21e';
const hash2 = '4c1d9e7a2b5f8c0e3d6a9b1c4e7f0a2d5b8c1e4f7a0b3d6c9e2f5a8b1d4c7e0f3a2';
if (scenario === 'secondblock') return { ...base, first_block_at: t - 602, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 266454, days_mined_30: 1, blocks_30d: 2, blocks_today: 2, cards_first: { [recState.mining.cards[0].key]: t - 602 }, blocks: [{ hash: hash2, daa: 267054, nonce: '3a9c0d11e2f4b5a6', at: t - 2, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 2 }, { hash, daa: 266454, nonce: '267dd27200a91c80', at: t - 602, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 1 }], votes_signed: 20, last_vote_index: 8516, last_vote_at: t - 8, milestone: null, first_block_shown: true };
if (scenario === 'firstblock') return { ...base, first_block_at: t - 70, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 266454, days_mined_30: 1, blocks_30d: 1, blocks_today: 1, blocks: [{ hash, daa: 266454, nonce: '267dd27200a91c80', at: t - 70, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 1 }], votes_signed: 2, last_vote_index: 8496, last_vote_at: t - 20, milestone: { kind: 'first', count: 1, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, hash, daa: 266454, at: t - 70 }, first_block_shown: false };
if (scenario === 'earn-held' || scenario === 'earn-still') return { ...base, first_block_at: t - 86000, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 259200, days_mined_30: 2, blocks_30d: 3, blocks_today: 2, blocks_24h: 3, earned: { h24: { wei: '14640000000000000000', ign: 14.64, blocks: 3, shards: 0, priced: true }, d7: { wei: '89200000000000000000', ign: 89.2, blocks: 18, shards: 1, priced: true }, all: { wei: '393070000000000000000', ign: 393.07, blocks: 80, shards: 6, priced: true }, estimated: false }, blocks: [{ hash: hash2, daa: 267054, nonce: '3a9c0d11e2f4b5a6', at: t - 2 * 3600 - 900, card: 'nvidia:0:NVIDIA GeForce RTX 5090', card_name: 'NVIDIA GeForce RTX 5090', count: 3 }, { hash, daa: 266454, nonce: '267dd27200a91c80', at: t - 5 * 3600, card: 'nvidia:0:NVIDIA GeForce RTX 5090', card_name: 'NVIDIA GeForce RTX 5090', count: 2 }, { hash: 'b' + hash.slice(1), daa: 259200, nonce: '1', at: t - 86000, card: 'nvidia:0:NVIDIA GeForce RTX 5090', card_name: 'NVIDIA GeForce RTX 5090', count: 1 }], votes_signed: 40, last_vote_index: 8495, last_vote_at: t - 12, first_block_shown: true };
if (scenario === 'ladder') return { ...base, first_block_at: t - 7300, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 259200, days_mined_30: 1, blocks_30d: 61, blocks_today: 61, votes_signed: 240, last_vote_index: 8495, last_vote_at: t - 12, last_signed_locked: 8495, signed_locked_count: 238, streak_s: 7100, shards: [{ block: 160390, shard: 3, wei: '1150000000000000000', at: t - 400 }], milestone: { kind: 'hundred', count: 100, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, hash, daa: 266400, at: t - 50 }, first_block_shown: true };
return base;
}
// miner-ui-5: GET /api/ladder, in src/chainfacts.rs's shape: the devnet's finality params (api/live, 7 Oct 2026),
// four keys relayed through the observer, this machine's key 8fafda27 ranked per scenario
function ladderReply(t) {
const mine = scenario === 'firstblock' ? { id: '8fafda27', blocks: 1, voter: false, participation: 0, stripped_until_daa: 0, revealed: false, rank: 4 }
: scenario === 'ladder' ? { id: '8fafda27', blocks: 61, voter: true, participation: 1, stripped_until_daa: 0, revealed: true, rank: 3 }
: scenario === 'secondblock' ? { id: '8fafda27', blocks: 2, voter: false, participation: 0, stripped_until_daa: 0, revealed: false, rank: 4 }
: scenario === 'firstwait' ? null : { id: '8fafda27', blocks: 251, voter: true, participation: 1, stripped_until_daa: 0, revealed: true, rank: 2 };
return { ok: true, source: 'observer', read_at: t, cached_age_s: 0, rpc: { weights: 'getFinalityWeights', checkpoints: 'getFinalityCheckpoints', stats: '/api/stats', relay: 'igneum.network/api/live (the observer\'s copy of the node\'s reply)' },
params: { dust: 5, weight_window: 7200, presence_window: 20, checkpoint_interval: 30, min_daa: 7200 }, checkpoint_index: 8495, daa_score: 266454, latest_locked_index: 8495, next_index: 8496, finality_active: true,
total_weight: 6000, active_weight: 5900.5, voters: 14, keys_listed: 4, keys_cap: 64,
network: { hashrate_hps: 764944722, miner_ign_per_block: 4.88044084, ign_per_block: 6.10055105, ramp_factor: 0.192519, daa: 266454, blocks_per_day_measured: 92280, bps: 1, stale: false },
mine: mine ? [mine] : [], best: mine };
}
function stateNow() {
const s = clone(recState);
const t = now();
s.now = t;
s.ladder = ladderNow(t);
s.installed_at = t0 - 3000;
if (scenario === 'firstwait') { s.mining.accepted_total = 0; s.mining.accepted_session = 0; s.mining.cards[0].accepted = 0; s.mining.found = []; s.finality = { ...s.finality, last_lock: 0, votes: 0 }; }
if (scenario === 'firstblock') { s.mining.accepted_total = 1; s.mining.accepted_session = 1; s.mining.cards[0].accepted = 1; }
if (scenario === 'secondblock') { s.mining.accepted_total = 2; s.mining.accepted_session = 2; s.mining.cards[0].accepted = 2; }
s.uptime_s = recState.uptime_s + Math.floor(t - t0);
s.started_at = t0 - recState.uptime_s; // first-block-21: the run's start on the wall clock (every card here is this run's)
s.mining.found = recState.mining.found.map(x => x + shift).filter(x => t - x < 3600);
s.events = recState.events.map(e => ({ ...e, t: e.t + shift }));
s.finality.last_lock_at += shift; s.finality.age_s = t - s.finality.last_lock_at;
s.update.checked_at += shift; s.clock.checked_at += shift; s.jobs.checked_at += shift;
s.program.eta_s = Math.max(0, Math.round(3600 - ((t - t0) % 3600)));
s.node.blocks = recState.node.blocks + Math.floor(t - t0); s.node.headers = s.node.blocks; s.node.daa = s.node.blocks;
s.node.last_reading_age_s = (t - t0) % 10;
// a captured state (IGNEUM_MOCK_STATE) is replayed as it was: the machine's own name, cards, rate and numbers
if (process.env.IGNEUM_MOCK_STATE) { s.node.blocks = recState.node.blocks; s.node.headers = recState.node.headers; s.node.daa = recState.node.daa; return s; }
// a live-looking hash rate
const wob = Math.sin((t - t0) / 7) * 1.4;
s.mining.hash_total = +(16.9 + wob).toFixed(2); s.mining.cards[0].hash_now = s.mining.hash_total;
// a fresh block every ~9 s on average (none before the first block)
const extra = []; if (scenario !== 'firstwait' && scenario !== 'firstblock' && scenario !== 'secondblock') for (let k = t0; k < t; k += 9.3) extra.push(k + 0.4);
if (scenario === 'firstwait') { s.mining.found = []; s.mining.accepted_total = 0; s.mining.accepted_session = 0; s.mining.cards[0].accepted = 0; s.events = s.events.filter(e => e.kind !== 'block'); }
if (scenario === 'firstblock') { s.mining.found = [t - 70]; s.events = [{ kind: 'block', t: t - 70, text: 'first block: found by ' + s.mining.cards[0].name + ' (block card)' }, { kind: 'block', t: t - 70, text: 'block accepted by the node (' + s.mining.cards[0].name + ')' }].concat(s.events.filter(e => e.kind !== 'block')).slice(0, 40); }
if (scenario === 'secondblock') { s.mining.found = [t - 602, t - 2]; s.events = [{ kind: 'block', t: t - 2, text: 'block accepted by the node (' + s.mining.cards[0].name + ')' }, { kind: 'block', t: t - 602, text: 'first block: found by ' + s.mining.cards[0].name + ' (block card)' }, { kind: 'block', t: t - 602, text: 'block accepted by the node (' + s.mining.cards[0].name + ')' }].concat(s.events.filter(e => e.kind !== 'block')).slice(0, 40); }
if (scenario === 'ladder') { s.settings.prove = true; s.proving = { ...s.proving, enabled: true, available: true, status: 'idle', paid: 1, paid_wei: '1150000000000000000', last_paid: 'block 160390 shard 3' }; s.events = [{ kind: 'proving', t: t - 400, text: 'block 160390 shard 3 paid 1.15 IGN' }, { kind: 'block', t: t - 50, text: 'block 100 of this machine: found by ' + s.mining.cards[0].name + ' (block card)' }].concat(s.events).slice(0, 40); }
s.mining.found = s.mining.found.concat(extra).filter(x => t - x < 3600).sort((a, b) => a - b);
s.mining.accepted_session += extra.length; s.mining.accepted_total += extra.length; s.mining.cards[0].accepted += extra.length;
if (extra.length) s.events = [{ kind: 'block', t: extra[extra.length - 1], text: 'block accepted by the node (Apple M5 Max)' }].concat(s.events).slice(0, 40);
// ui-ota: the interface state (Settings > Interface); ?ui=ota shows an over-the-air bundle active
s.ui = { embedded_version: '1.0.0', active_version: '1.0.0', source: 'embedded', installed_at: 0, confirmed: true, published_version: '', busy: false, error: '', bad: [], builtin: false, note: '' };
s.settings = { ...s.settings, ui_builtin: false };
s.display_name = 'studio-mac-pro-with-a-long-name';
s.host = s.display_name;
if (paused || scenario === 'paused') { s.mining.paused = true; s.mining.state = 'paused'; s.mining.cards[0].state = 'off'; s.mining.hash_total = 0; s.mining.cards[0].hash_now = 0; }
if (scenario === 'syncing') {
const rate = 38; // blocks per second
s.node.state = 'syncing'; s.node.synced = false; s.node.headers = 412000; s.node.blocks = Math.min(412000, 30000 + Math.floor((t - t0) * rate)); s.node.daa = s.node.blocks;
s.node.message = 'downloading blocks'; s.node.peers = 2; s.node.tips = 0;
s.mining.state = 'waiting'; s.mining.hash_total = 0; s.mining.cards[0].state = 'waiting'; s.mining.cards[0].hash_now = 0; s.mining.found = []; s.program.eta_s = -1; s.program.message = '';
s.finality = { age_s: 0, last_lock: 0, last_lock_at: 0, message: 'Finality starts after the node is synced.', votes: 0 };
}
if (scenario === 'nodedown') {
s.node.state = 'restarting'; s.node.synced = false; s.node.restart_in_s = 7 - Math.floor(t - t0) % 8; s.node.restarts = 3; s.node.peers = 0; s.node.message = 'the node exited (code 101)';
s.mining.state = 'waiting'; s.mining.hash_total = 0; s.mining.cards[0].state = 'waiting'; s.mining.cards[0].hash_now = 0; s.mining.found = []; s.program.eta_s = -1;
s.events = [{ kind: 'error', t: t - 12, text: 'node exited with code 101; restarting in 8 s' }].concat(s.events).slice(0, 40);
}
if (scenario === 'nocards') { s.mining.cards = []; s.mining.state = 'idle'; s.mining.hash_total = 0; s.mining.found = []; s.detect_message = 'No Metal, CUDA or OpenCL device answered.'; s.detecting = false; }
if (scenario === 'integrated') {
s.mining.cards = [{ ...recState.mining.cards[0], key: 'intel::Intel Iris Xe', name: 'Intel Iris Xe Graphics', kind: 'integrated', vendor: 'other', worker: 'OpenCL', detail: '96 EUs, shared memory', vram_mb: 0, enabled: false, state: 'off', hash_now: 0, hash_avg: 0, accepted: 0, ids: [], reason: 'integrated graphics: off by default, around 0.3 MH/s and it slows the desktop', identities: 1, prepared: false }];
s.mining.state = 'idle'; s.mining.hash_total = 0; s.mining.found = [];
}
if (scenario === 'clock') { s.clock = { ...s.clock, severity: 'block', skew_s: -412, message: 'This machine\'s clock is 6 min 52 s behind the network. Blocks it finds are rejected until the clock is right.', hint: 'System Settings > General > Date & Time: turn on "Set time and date automatically"' }; s.mining.state = 'waiting'; s.mining.hash_total = 0; }
if (scenario === 'fresh') { s.setup_done = false; s.phase = 'welcome'; s.address = { display: '', key_saved: false, source: '', value: '', wallet_file: '' }; }
if (scenario === 'job') { s.jobs = { ...s.jobs, active: true, id: 'job-2026-10-04-17', kind: 'shard-benchmark', title: 'shard benchmark', stage: 'proving shard 3 of 8', started_at: t - 400, results: ['RESULT shard=1 prove_s=41.2', 'RESULT shard=2 prove_s=39.8'], message: '' }; }
if (scenario === 'heat' || scenario === 'heat-rest') {
const resting = scenario === 'heat-rest';
s.mining.cards = [{ ...recState.mining.cards[0], key: 'nvidia:0:NVIDIA GeForce RTX 5090', name: 'NVIDIA GeForce RTX 5090', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '21760 cores', vram_mb: 32768, device: '0', identities: 8, ids: ['8fafda27', '1b2c3d4e', '5f6a7b8c'], power_default_w: 575, power_pct: 80, power_applied: true, power_limit_w: 460, power_w: resting ? 22 : 290, temp_gpu: resting ? 31 : 64, temp_mem: resting ? 36 : 78, telemetry_at: t, hash_now: resting ? 0 : 122.3, hash_avg: 121.8, state: resting ? 'resting' : 'mining', message: resting ? 'resting: the room is 20.1 °C, holding 20.0 °C (heat mode)' : '', tune_line: 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)', tune_source: 'full', tune_control: true, tune_clock_mhz: 2470, sweep_pct: 80, sweep_at: t - 7200, sweep_watts: 290, sweep_mhs: 122.3, sweep_eff: 0.422, tune_before_watts: 374, tune_before_mhs: 122.5, tune_curve: [{ clock_mhz: 0, power_pct: 100, watts: 374, mhs: 122.5, eff: 0.328 }, { clock_mhz: 0, power_pct: 90, watts: 340, mhs: 122.4, eff: 0.36 }, { clock_mhz: 0, power_pct: 80, watts: 302, mhs: 122.3, eff: 0.405 }, { clock_mhz: 2470, power_pct: 80, watts: 290, mhs: 122.3, eff: 0.422 }] },
{ ...recState.mining.cards[0], key: 'nvidia:1:NVIDIA GeForce RTX 4070', name: 'NVIDIA GeForce RTX 4070', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '5888 cores', vram_mb: 12288, device: '1', identities: 2, ids: ['9a8b7c6d'], power_default_w: 200, power_pct: 80, power_applied: true, power_limit_w: 160, power_w: resting ? 12 : 158, temp_gpu: resting ? 29 : 58, temp_mem: 0, telemetry_at: t, hash_now: resting ? 0 : 61.4, hash_avg: 60.9, state: resting ? 'resting' : 'mining', message: resting ? 'resting: the room is 20.1 °C, holding 20.0 °C (heat mode)' : '', tune_line: 'Tuned: 61.4 MH/s at 158 W (0.389 MH/W)', tune_source: 'confirm', tune_control: true, sweep_pct: 80, sweep_at: t - 3600, sweep_watts: 158, sweep_mhs: 61.4, sweep_eff: 0.389 }];
s.mining.hash_total = resting ? 0 : 183.7; s.mining.state = resting ? 'waiting' : 'mining'; s.display_name = 'study-pc';
s.settings = { ...s.settings, sweep: true, power_control: true, tune_goal: 'balanced', power_price_pence: 26.32, tune_climb: false, tune_period_s: 604800, region: 'gb', heat_on: true, heat_set_c: 20, heat_schedule: '06:00 20, 22:00 16', heat_tz_min: 60, heat_room_c: 19.6, heat_room_at: t - 2400, heat_offset_c: 7.5 };
s.heat = { on: true, phase: resting ? 'resting' : 'heating', set_c: 20, room_c: resting ? 20.1 : 19.6, room_source: 'typed', room_error_c: 0, room_age_s: 2400, duty: 0.6, duty_hour: 0.58, heat_w: resting ? 0 : 448, full_w: 448, heat_avg_w: 269, until_s: resting ? 230 : 312, note: resting ? 'Holding 20.0 °C: room 20.1 °C from your reading 40 min ago. resting, heats again in 4 min. Heat 60% of the time.' : 'Holding 20.0 °C: room 19.6 °C from your reading 40 min ago. heating, 5 min to go. Heat 60% of the time.', period_s: 600, offset_c: 7.5, offset_learned: true };
s.events = [{ kind: 'info', t: t - 312, text: 'heat mode: heating again' }, { kind: 'info', t: t - 2400, text: 'heat mode on: holding 20.0 °C (schedule 06:00 20, 22:00 16); the cards heat for a share of every 10 minutes and rest for the rest' }].concat(s.events).slice(0, 40);
}
if (scenario === 'intel') {
// PC 1 after the B580 swap (7 October 2026, docs/plans/intel-arc.md): the 5090 and the first Intel card; the Arc's
// numbers are the plan's expectation, not a measurement, until the PC 1 job's row lands
s.mining.cards = [{ ...recState.mining.cards[0], key: 'nvidia:NVIDIA GeForce RTX 5090', name: 'NVIDIA GeForce RTX 5090', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '21760 cores', vram_mb: 32768, device: '0', identities: 8, ids: ['8fafda27', '1b2c3d4e', '5f6a7b8c'], power_default_w: 575, power_pct: 80, power_applied: true, power_limit_w: 460, power_w: 227, temp_gpu: 61, temp_mem: 70, hash_now: 127.7, hash_avg: 127.1, eff_mhw: 0.563, sweep_supported: true, sweep_state: 'idle', sweep_note: '', tune_line: 'Tuned: 127.7 MH/s at 227 W (0.563 MH/W)', tune_source: 'full', tune_control: true, tune_clock_mhz: 1854, sweep_pct: 100, sweep_watts: 227, sweep_at: t - 7200 },
{ ...recState.mining.cards[0], key: 'intel:Intel(R) Arc(TM) B580 Graphics', name: 'Intel(R) Arc(TM) B580 Graphics', code: 'Intel(R) Arc(TM) B580 Graphics', platform: 'Intel(R) OpenCL Graphics (OpenCL 3.0 ), driver 32.0.101.9034', kind: 'discrete', vendor: 'intel', worker: 'OpenCL', detail: '160 compute units', vram_mb: 12208, device: '1', bus: '05:00.0', identities: 8, ids: ['c0ffee01', 'c0ffee02'], enabled: true, state: 'mining', hash_now: 15.2, hash_avg: 15.0, accepted: 3, rejected: 0, power_default_w: 0, power_pct: 0, power_applied: false, power_w: 0, temp_gpu: 0, temp_mem: 0, eff_mhw: 0, sweep_supported: false, sweep_state: 'unsupported', sweep_note: 'not available: Intel Arc exposes no power cap or reading through OpenCL (the driver\'s IGCL counters come next)', tune_line: '', tune_source: '', tune_control: false, driver: '32.0.101.9034' }];
s.mining.hash_total = 142.9; s.display_name = 'pc-1';
}
if (scenario === 'drivers' || scenario === 'drivers-running' || scenario === 'drivers-reboot' || scenario === 'drivers-error' || scenario === 'drivers-mac' || scenario === 'drivers-egpu' || scenario === 'drivers-egpu-running') {
// driver-check (7 October 2026): PC 1 after the Arc went in, with the manifest's table: the 5090 fine, the
// 9070 XT's driver older than the worker needs, the Arc on Windows' inbox driver, the install's states
var tbl = 'https://www.intel.com/content/www/us/en/download/785597/';
var nvOffer = { vendor: 'nvidia', status: 'fine', installed: '617.42', wanted: '617.42', min_version: '570.00', size: 990853168, url: 'https://us.download.nvidia.com/Windows/617.42/617.42-desktop-win10-win11-64bit-international-dch-whql.exe', hash_source: 'https://www.nvidia.com/en-us/drivers/', text: 'NVIDIA driver 617.42: fine.', installable: false };
var amdOffer = { vendor: 'amd', status: 'old', installed: '32.0.21042.62', wanted: '26.9.2', min_version: '32.0.32000.0', size: 912345678, url: 'https://drivers.amd.com/drivers/whql-amd-software-adrenalin-edition-26.9.2-win10-win11-sep2026.exe', hash_source: 'https://www.amd.com/en/support/download/drivers.html', text: 'Install the AMD driver 26.9.2 (870 MB). Driver 32.0.21042.62 is older than the 32.0.32000.0 the worker needs.', installable: true };
var arcOffer = { vendor: 'intel', status: 'old', installed: '32.0.101.6600', wanted: '32.0.101.9034', min_version: '32.0.101.6733', size: 932631144, url: 'https://downloadmirror.intel.com/929959/gfx_win_101.9034.exe', hash_source: tbl, text: 'Install the Intel Arc driver 32.0.101.9034 (889 MB). Driver 32.0.101.6600 is older than the 32.0.101.6733 the worker needs.', installable: true };
s.mining.cards = [{ ...recState.mining.cards[0], key: 'nvidia:NVIDIA GeForce RTX 5090', name: 'NVIDIA GeForce RTX 5090', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '21760 cores', vram_mb: 32768, device: '0', identities: 2, ids: ['ea53bd41', '7b8ef6fd'], power_default_w: 575, power_pct: 80, power_applied: true, power_limit_w: 460, power_w: 291, temp_gpu: 63, temp_mem: 72, hash_now: 122.4, hash_avg: 121.1, accepted: 102, driver: '617.42', driver_os: '617.42', driver_offer: nvOffer, sweep_supported: true, sweep_state: 'idle', sweep_note: '' },
{ ...recState.mining.cards[0], key: 'amd:gfx1201', name: 'AMD Radeon RX 9070 XT', code: 'gfx1201', kind: 'discrete', vendor: 'amd', worker: 'OpenCL', detail: '32 compute units', vram_mb: 16304, device: '1', identities: 2, ids: ['a405c3e6', 'e15fe94b'], enabled: true, state: 'mining', hash_now: 18.9, hash_avg: 17.8, accepted: 20, power_w: 198, temp_gpu: 61, driver: '3683.0 (PAL,LC)', driver_os: '32.0.21042.62', driver_offer: amdOffer, sweep_supported: true, sweep_state: 'idle', sweep_note: '' },
{ ...recState.mining.cards[0], key: 'intel:Intel(R) Arc(TM) B580 Graphics', name: 'Intel(R) Arc(TM) B580 Graphics', code: 'Intel(R) Arc(TM) B580 Graphics', kind: 'discrete', vendor: 'intel', worker: 'OpenCL', detail: '160 compute units', vram_mb: 11930, device: '2', identities: 8, ids: [], enabled: false, state: 'off', hash_now: 0, hash_avg: 0, accepted: 0, power_w: 0, temp_gpu: 0, reason: 'the driver is older than the worker needs', driver: '', driver_os: '32.0.101.6733', driver_offer: arcOffer, sweep_supported: false, sweep_state: 'unsupported', sweep_note: 'not available: Intel Arc exposes no power cap or reading through OpenCL (the driver\'s IGCL counters come next)' }];
s.mining.hash_total = 141.3; s.display_name = 'pc-1';
s.drivers = { status: 'idle', vendor: '', version: '', progress: 0, message: '', error: '', reboot_required: false, dry_run: false, started_at: 0, finished_at: 0, table_updated: '2026-10-07', platform: 'windows' };
if (scenario === 'drivers-running') s.drivers = { ...s.drivers, status: 'downloading', vendor: 'intel', version: '32.0.101.9034', progress: 0.43 + ((Date.now() / 1000) % 60) / 110, message: 'downloading 32.0.101.9034 (889 MB) from downloadmirror.intel.com', started_at: t - 40 };
if (scenario === 'drivers-reboot') s.drivers = { ...s.drivers, status: 'reboot', vendor: 'intel', version: '32.0.101.9034', progress: 1, message: 'INTEL driver 32.0.101.9034 installed. Restart Windows to finish.', reboot_required: true, started_at: t - 300, finished_at: t - 20 };
if (scenario === 'drivers-error') s.drivers = { ...s.drivers, status: 'error', vendor: 'amd', version: '26.9.2', progress: 1, message: 'download: sha256 mismatch: the file is not the one the table names (page https://www.amd.com/en/support/download/drivers.html)', error: 'download: sha256 mismatch: the file is not the one the table names (page https://www.amd.com/en/support/download/drivers.html)', started_at: t - 120, finished_at: t - 5 };
if (scenario === 'drivers-egpu' || scenario === 'drivers-egpu-running') {
// PC 2, 7 October 2026: the Arc B580 in the Razer Core X V2 (the eGPU kind), the 5060 Ti inside the case; the offer
// on the eGPU row warns that its worker stops and the display reset can take the machine; running: the worker off
var arcRow = s.mining.cards.find(c => c.vendor === 'intel');
s.mining.cards = s.mining.cards.filter(c => c.vendor === 'intel' || c.vendor === 'nvidia').map(c => c.vendor === 'intel' ? { ...c, kind: 'external', enabled: true, state: 'mining', hash_now: 11.0, hash_avg: 10.98, power_w: 118, temp_gpu: 61, identities: 2, ids: ['7c2e', 'a91f'] } : { ...c, key: 'nvidia:0:NVIDIA GeForce RTX 5060 Ti', name: 'NVIDIA GeForce RTX 5060 Ti', detail: '4608 cores', vram_mb: 16384, hash_now: 40.2, hash_avg: 40.1, power_w: 152, power_default_w: 180, power_limit_w: 152 });
s.display_name = 'pc-2'; s.mining.hash_total = 51.2;
if (scenario === 'drivers-egpu-running') { s.mining.cards = s.mining.cards.map(c => c.vendor === 'intel' ? { ...c, enabled: false, state: 'off', hash_now: 0, power_w: 0 } : c); s.mining.hash_total = 40.2; s.drivers = { ...s.drivers, status: 'installing', vendor: 'intel', version: '32.0.101.9034', progress: 0.72, message: 'installing INTEL 32.0.101.9034: Windows asks for permission once', reboot_required: false, started_at: t - 95, finished_at: 0 }; }
void arcRow;
}
if (scenario === 'drivers-mac') { s.mining.cards = [{ ...recState.mining.cards[0], driver_offer: { vendor: 'apple', status: 'none', installed: '', wanted: '', min_version: '', size: 0, url: '', hash_source: '', text: 'Apple silicon: no driver step, macOS carries it.', installable: false } }]; s.mining.hash_total = recState.mining.hash_total; s.display_name = recState.display_name; s.drivers.platform = 'macos'; }
}
if (scenario === 'euro') {
// currency-21: a German machine, the Eurostat tariff typed, no override: every money line in euro
s.settings = { ...s.settings, region: 'de', power_price_pence: 38.69, currency: '' };
s.mining.cards = s.mining.cards.map((c, i) => i === 0 ? { ...c, power_w: 92, power_default_w: 0, telemetry_at: t } : c);
}
if (scenario === 'rig') {
// gpu-logos (7 October 2026): a mixed rig, one card per vendor, every mark on one page; the Intel numbers are the
// intel scenario's expectation, the others the heat and nvidia scenarios' readings
s.mining.cards = [{ ...recState.mining.cards[0], key: 'nvidia:0:NVIDIA GeForce RTX 5090', name: 'NVIDIA GeForce RTX 5090', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '21760 cores', vram_mb: 32768, device: '0', identities: 8, ids: ['8fafda27', '1b2c3d4e', '5f6a7b8c'], power_default_w: 575, power_pct: 80, power_applied: true, power_limit_w: 460, power_w: 291, temp_gpu: 63, temp_mem: 76, telemetry_at: t, hash_now: 122.3, hash_avg: 121.8, accepted: 102, state: 'mining', tune_line: 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)', tune_source: 'full', tune_control: true, sweep_pct: 80, sweep_supported: true, sweep_state: 'idle' },
{ ...recState.mining.cards[0], key: 'amd:gfx1201', name: 'AMD Radeon RX 9070 XT', code: 'gfx1201', kind: 'discrete', vendor: 'amd', worker: 'OpenCL', detail: '64 compute units', platform: 'AMD Accelerated Parallel Processing', vram_mb: 16384, device: '1', bus: '0000:03:00.0', identities: 4, ids: ['a1b2c3d4'], power_default_w: 304, power_pct: 100, power_applied: false, power_w: 198, temp_gpu: 61, temp_mem: 0, telemetry_at: t, hash_now: 18.9, hash_avg: 18.7, accepted: 20, state: 'mining', sweep_supported: true, sweep_state: 'idle', sweep_note: 'measure only: no cap through OpenCL' },
{ ...recState.mining.cards[0], key: 'intel:Intel(R) Arc(TM) B580 Graphics', name: 'Intel(R) Arc(TM) B580 Graphics', code: 'Intel(R) Arc(TM) B580 Graphics', platform: 'Intel(R) OpenCL Graphics (OpenCL 3.0 ), driver 32.0.101.9034', kind: 'discrete', vendor: 'intel', worker: 'OpenCL', detail: '160 compute units', vram_mb: 12208, device: '2', bus: '05:00.0', identities: 4, ids: ['c0ffee01'], enabled: true, state: 'mining', hash_now: 15.2, hash_avg: 15.0, accepted: 3, rejected: 0, power_default_w: 0, power_pct: 0, power_applied: false, power_w: 0, temp_gpu: 0, temp_mem: 0, eff_mhw: 0, sweep_supported: false, sweep_state: 'unsupported', sweep_note: 'not available: Intel Arc exposes no power cap or reading through OpenCL (the driver\'s IGCL counters come next)' },
{ ...recState.mining.cards[0], key: 'intel::Intel UHD 770', name: 'Intel UHD Graphics 770', kind: 'integrated', vendor: 'other', worker: 'OpenCL', detail: '32 EUs', vram_mb: 0, enabled: false, state: 'off', hash_now: 0, hash_avg: 0, accepted: 0, ids: [], reason: 'integrated graphics: off by default', identities: 1 }];
s.mining.hash_total = 156.4; s.display_name = 'rig-01';
s.settings = { ...s.settings, sweep: true, power_control: true, tune_goal: 'balanced', power_price_pence: 26.32, region: 'gb' };
}
if (scenario === 'tiers-table') {
// tiers-table-26 (8 October 2026): the install's first minute, the three tiers seeded from the team's table (app/igneum-app/tiers/
// class-v5-tiers.json: the 5090 measured, the 5070 Ti estimated), nothing measured on these cards yet, Balanced in force from install
const table = JSON.parse(fs.readFileSync(path.join(here, '../../app/igneum-app/tiers/class-v5-tiers.json'), 'utf8'));
const rowsOf = (name) => { const n = name.toUpperCase(); let best = null, len = 0; for (const c of table.cards) for (const m of c.match) if (n.includes(m.toUpperCase()) && m.length > len) { best = c; len = m.length; } return best ? best.tiers.map((r) => ({ ...r, table: true, label: r.label || best.label })) : []; };
const mk = (key, name, detail, vram, dev, ids, pdef, w, hash, temp) => ({ ...recState.mining.cards[0], key, name, kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail, vram_mb: vram, device: dev, identities: ids.length, ids, power_default_w: pdef, power_pct: 100, power_applied: true, power_limit_w: pdef, power_w: w, temp_gpu: temp, temp_mem: 0, telemetry_at: t, hash_now: hash, hash_avg: hash, accepted: 0, state: 'mining', sweep_supported: true, tune_control: true, sweep_pct: 0, sweep_state: 'idle', sweep_note: '', tune_line: '', sweep_at: 0, tiers: rowsOf(name), tier: 'balanced', tier_note: '', tier_at: 0, tiers_class: 'v5', program_class: 'v5', tiers_table: true, tiers_remeasure_at: 0, knee_loss_pct: 0, lock_at: 0, lock_mhz: 0, lock_note: '' });
s.mining.cards = [mk('nvidia:0:NVIDIA GeForce RTX 5090', 'NVIDIA GeForce RTX 5090', '21760 cores', 32768, '0', ['8fafda27', '1b2c3d4e'], 575, 471, 136.1, 67), mk('nvidia:1:NVIDIA GeForce RTX 5070 Ti', 'NVIDIA GeForce RTX 5070 Ti', '8960 cores', 16384, '1', ['9a8b7c6d'], 300, 221, 78.2, 63)];
s.mining.hash_total = 214.3; s.mining.state = 'mining'; s.display_name = 'study-pc'; s.uptime_s = 80;
s.settings = { ...s.settings, sweep: true, power_control: true, tune_tier: 'balanced', tune_goal: 'balanced', power_price_pence: 28, region: 'gb' };
}
if (scenario === 'tiers' || scenario === 'tiers-first' || scenario === 'tiers-mac' || scenario === 'tiers-flip') {
// tiers-25 (8 October 2026): the three tiers from each card's own search (the project lead's definition: best MH/W row, best MH/W row within
// 1% of the top rate, the top-rate row). The rows are the shape of last night's 5090 and 5080 grids as main quoted them
// (5090 knee 1,200 MHz / 305 W, 5080 knee 1,100 MHz / 147 W), not a measurement. tiers-first: the install's first minutes,
// nothing measured yet, Ember Tune on at Balanced. tiers-mac: the recorded Apple M5 Max with no lever (one stock entry).
const first = scenario === 'tiers-first', flip = scenario === 'tiers-flip';
const t5090 = [{ id: 'efficiency', clock_mhz: 1200, power_pct: 80, w: 305, mhs: 121.9, mhw: 0.4, source: 'measured' }, { id: 'balanced', clock_mhz: 1950, power_pct: 90, w: 372, mhs: 126.8, mhw: 0.341, source: 'measured' }, { id: 'max', clock_mhz: 0, power_pct: 100, w: 450, mhs: 128.4, mhw: 0.285, source: 'stock' }];
const t5080 = [{ id: 'efficiency', clock_mhz: 1100, power_pct: 80, w: 147, mhs: 71.2, mhw: 0.484, source: 'measured' }, { id: 'balanced', clock_mhz: 1800, power_pct: 90, w: 250, mhs: 76.1, mhw: 0.304, source: 'measured' }, { id: 'max', clock_mhz: 0, power_pct: 100, w: 360, mhs: 78.0, mhw: 0.217, source: 'stock' }];
const curve5090 = [{ clock_mhz: 0, power_pct: 100, watts: 450, mhs: 128.4, eff: 0.285 }, { clock_mhz: 0, power_pct: 90, watts: 405, mhs: 128.1, eff: 0.316 }, { clock_mhz: 0, power_pct: 80, watts: 360, mhs: 127.2, eff: 0.353 }, { clock_mhz: 2472, power_pct: 90, watts: 398, mhs: 128.0, eff: 0.322 }, { clock_mhz: 1950, power_pct: 90, watts: 372, mhs: 126.8, eff: 0.341 }, { clock_mhz: 1545, power_pct: 80, watts: 332, mhs: 124.6, eff: 0.375 }, { clock_mhz: 1200, power_pct: 80, watts: 305, mhs: 121.9, eff: 0.4 }, { clock_mhz: 1100, power_pct: 80, watts: 298, mhs: 114.9, eff: 0.386 }];
if (scenario === 'tiers-mac') {
s.mining.cards = [{ ...recState.mining.cards[0], tiers: [{ id: 'max', clock_mhz: 0, power_pct: 100, w: 38, mhs: 26.7, mhw: 0.703, source: 'stock' }], tier: 'max', tier_note: 'no lever: Apple silicon sets its own clocks and power', tier_at: t - 600, tune_line: 'Measured: 26.7 MH/s at 38 W (0.703 MH/W)', tune_source: 'baseline', tune_control: false, sweep_at: t - 600, sweep_note: 'measure only on Apple silicon: the system sets the clocks and the power; no control exposed', sweep_state: 'idle', lock_note: 'no lever', lock_at: 0, lock_mhz: 0 }];
s.settings = { ...s.settings, sweep: true, tune_tier: 'balanced', tune_goal: 'balanced' };
} else {
s.mining.cards = [{ ...recState.mining.cards[0], key: 'nvidia:0:NVIDIA GeForce RTX 5090', name: 'NVIDIA GeForce RTX 5090', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '21760 cores', vram_mb: 32768, device: '0', identities: 8, ids: ['8fafda27', '1b2c3d4e', '5f6a7b8c'], power_default_w: 450, power_pct: first ? 100 : 90, power_applied: true, power_limit_w: first ? 450 : 405, power_w: first ? 448 : 372, temp_gpu: first ? 66 : 59, temp_mem: first ? 78 : 68, gclk_mhz: first ? 2790 : 1950, clock_cap_mhz: first ? 0 : 1950, telemetry_at: t, hash_now: first ? 128.3 : 126.8, hash_avg: first ? 128.1 : 126.5, accepted: first ? 0 : 102, state: 'mining', sweep_supported: true, tune_control: true, sweep_pct: first ? 0 : 90,
...(first ? { sweep_state: 'idle', sweep_note: '', tune_line: '', sweep_at: 0, tiers: [], tier: '', tier_note: '', tier_at: 0, lock_at: 0, lock_mhz: 0, lock_note: '' }
: { sweep_state: 'idle', sweep_note: '', tune_line: 'Tuned: 126.8 MH/s at 372 W (0.341 MH/W)', tune_source: 'full', tune_clock_mhz: 1950, sweep_at: t - 7200, sweep_watts: 372, sweep_mhs: 126.8, sweep_eff: 0.341, tune_before_watts: 450, tune_before_mhs: 128.4, tune_curve: curve5090, tiers: t5090, tier: 'balanced', tier_note: '', tier_at: t - 7200, lock_mhz: 1950, lock_mhs: 126.8, lock_w: 372, lock_mhw: 0.341, unlocked_mhs: 128.1, unlocked_w: 405, lock_at: t - 7200, lock_note: 'rate fell 5.8 percent at 1100 MHz' }) },
{ ...recState.mining.cards[0], key: 'nvidia:1:NVIDIA GeForce RTX 5080', name: 'NVIDIA GeForce RTX 5080', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '10752 cores', vram_mb: 16384, device: '1', identities: 8, ids: ['9a8b7c6d', '0c1d2e3f'], power_default_w: 360, power_pct: first ? 100 : 90, power_applied: true, power_limit_w: first ? 360 : 324, power_w: first ? 358 : 250, temp_gpu: first ? 64 : 57, temp_mem: 0, gclk_mhz: first ? 2700 : 1800, clock_cap_mhz: first ? 0 : 1800, telemetry_at: t, hash_now: first ? 77.9 : 76.1, hash_avg: first ? 77.8 : 76.0, accepted: first ? 0 : 61, state: 'mining', sweep_supported: true, tune_control: true, sweep_pct: first ? 0 : 90,
...(first ? { sweep_state: 'idle', sweep_note: '', tune_line: '', sweep_at: 0, tiers: [], tier: '', tier_note: '', tier_at: 0, lock_at: 0, lock_mhz: 0, lock_note: '' }
: { sweep_state: 'idle', sweep_note: '', tune_line: 'Tuned: 76.1 MH/s at 250 W (0.304 MH/W)', tune_source: 'full', tune_clock_mhz: 1800, sweep_at: t - 6900, sweep_watts: 250, sweep_mhs: 76.1, sweep_eff: 0.304, tune_before_watts: 360, tune_before_mhs: 78.0, tiers: t5080, tier: 'balanced', tier_note: '', tier_at: t - 6900, lock_mhz: 1800, lock_mhs: 76.1, lock_w: 250, lock_mhw: 0.304, unlocked_mhs: 77.8, unlocked_w: 324, lock_at: t - 6900, lock_note: 'rate fell 6.1 percent at 1000 MHz' }) }];
if (flip) {
// the class v5 crossing three minutes ago: the 5090's v4 tiers are out of date and its re-run started, with the knee loss
// read at 5.2 percent at the same lock; the 5080 was re-measured already under v5
s.mining.cards[0] = { ...s.mining.cards[0], tiers_class: 'v4', program_class: 'v5', tiers_remeasure_at: t - 180, knee_loss_pct: 5.2, state: 'tuning', sweep_state: 'running', tune_plan: 'full', tune_step: 2, tune_steps: 13, tune_eta_s: 540, lock_step: 0, lock_steps: 0, sweep_note: 'tuning: holding 90% · 38 s (step 2 of 13)' };
s.mining.cards[1] = { ...s.mining.cards[1], tiers_class: 'v5', program_class: 'v5', tiers_remeasure_at: 0, knee_loss_pct: 1.1 };
s.events = [{ kind: 'info', t: t - 180, text: 'Ember Tune: class v5 arrived at DAA 68,400; re-measuring NVIDIA GeForce RTX 5090 (its class v4 points are out of date)' }].concat(s.events).slice(0, 40);
} else { s.mining.cards[0].tiers_class = s.mining.cards[0].program_class = first ? '' : 'v4'; s.mining.cards[1].tiers_class = s.mining.cards[1].program_class = first ? '' : 'v4'; }
s.mining.hash_total = first ? 206.2 : 202.9; s.mining.state = 'mining'; s.display_name = 'study-pc'; s.uptime_s = first ? 95 : recState.uptime_s;
s.settings = { ...s.settings, sweep: true, power_control: true, tune_tier: 'balanced', tune_goal: 'balanced', power_price_pence: first ? 0 : 28, region: 'gb' };
if (!first) s.events = [{ kind: 'info', t: t - 6900, text: 'Ember Tune set NVIDIA GeForce RTX 5080 to Balanced: 76.1 MH/s at 250 W, 110 W under stock' }, { kind: 'info', t: t - 7200, text: 'Ember Tune set NVIDIA GeForce RTX 5090 to Balanced: 126.8 MH/s at 372 W, 78 W under stock' }].concat(s.events).slice(0, 40);
}
}
if (scenario === 'earn-held' || scenario === 'earn-still') {
// the Earnings tab's two truths (8 October 2026): earn-held = PC 1's shape, a floor-lane job holding the card since 12:40 while the
// projection would read 100k+ IGN a day; earn-still = the chain standing still (no block for 15 min). Both with 3 blocks earned in
// the last 24 hours from the ladder's coinbase reads
const heldJob = scenario === 'earn-held';
const c0 = { ...recState.mining.cards[0], key: 'nvidia:0:NVIDIA GeForce RTX 5090', name: 'NVIDIA GeForce RTX 5090', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '21760 cores', vram_mb: 32768, device: '0', identities: 8, ids: ['8fafda27', '1b2c3d4e', '5f6a7b8c'], power_default_w: 575, power_pct: 80, power_applied: true, power_limit_w: 460, power_w: heldJob ? 410 : 291, temp_gpu: 63, temp_mem: 76, telemetry_at: t, hash_now: heldJob ? 0 : 122.3, hash_avg: 121.8, accepted: 3, state: heldJob ? 'held' : 'mining', message: heldJob ? 'paused for a job from the team' : '', sweep_supported: true, sweep_state: 'idle', sweep_note: '', tune_line: 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)', tune_source: 'full', tune_control: true, tune_clock_mhz: 2470, sweep_pct: 80, sweep_at: t - 7200 };
s.mining.cards = [c0]; s.mining.state = heldJob ? 'held' : 'mining'; s.mining.hash_total = heldJob ? 0 : 122.3; s.mining.accepted_session = 0; s.display_name = 'pc-1'; s.uptime_s = 2 * 3600 + 300;
if (heldJob) s.jobs = { ...s.jobs, active: true, id: 'job-20261008-1240', kind: 'run', title: 'class v6 floor lane 2 (the 5090 grid)', stage: 'row 7 of 24', started_at: t - 2 * 3600 - 60, message: '' };
else s.node = { ...s.node, tip_age_s: 900 };
s.mining.found = [];
}
if (scenario === 'knob' || scenario === 'knob-search') {
// the core-clock knob (knob-24, 7 October 2026): the 5090 locked at 1,300 MHz after the ladder stopped at the knee
// (1,200 MHz, the rate 5.2% under the cap row), read against the unlocked row at the same 80% cap; the 4070 in the
// clock search (knob-search: both cards searching). The rows are the shape of the PC 1 passes, not a measurement.
const search = scenario === 'knob-search';
const ladder = [{ clock_mhz: 0, power_pct: 100, watts: 374, mhs: 122.5, eff: 0.328 }, { clock_mhz: 0, power_pct: 90, watts: 340, mhs: 122.4, eff: 0.36 }, { clock_mhz: 0, power_pct: 80, watts: 302, mhs: 122.3, eff: 0.405 }, { clock_mhz: 0, power_pct: 70, watts: 264, mhs: 118.1, eff: 0.447 },
{ clock_mhz: 2472, power_pct: 80, watts: 290, mhs: 122.3, eff: 0.422 }, { clock_mhz: 1854, power_pct: 80, watts: 227, mhs: 121.6, eff: 0.536 }, { clock_mhz: 1545, power_pct: 80, watts: 206, mhs: 121.1, eff: 0.588 }, { clock_mhz: 1300, power_pct: 80, watts: 190, mhs: 120.7, eff: 0.635 }, { clock_mhz: 1200, power_pct: 80, watts: 182, mhs: 116.0, eff: 0.637 }];
const locked = { lock_mhz: 1300, lock_mhs: 120.7, lock_w: 190, lock_mhw: 0.635, unlocked_mhs: 122.3, unlocked_w: 302, lock_step: 0, lock_steps: 0, lock_at: t - 7200, lock_note: 'rate fell 5.2 percent at 1200 MHz' };
const searching = { state: 'tuning', sweep_state: 'running', tune_plan: 'full', tune_step: 8, tune_steps: 13, tune_eta_s: 390, lock_step: 4, lock_steps: 9, sweep_note: 'tuning: holding 1545 MHz · 80% · 41 s (step 8 of 13)', lock_mhz: 0, lock_mhs: 0, lock_w: 0, lock_mhw: 0, unlocked_mhs: 0, unlocked_w: 0, lock_at: 0, lock_note: '' };
s.mining.cards = [{ ...recState.mining.cards[0], key: 'nvidia:0:NVIDIA GeForce RTX 5090', name: 'NVIDIA GeForce RTX 5090', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '21760 cores', vram_mb: 32768, device: '0', identities: 8, ids: ['8fafda27', '1b2c3d4e', '5f6a7b8c'], power_default_w: 575, power_pct: 80, power_applied: true, power_limit_w: 460, power_w: search ? 206 : 190, temp_gpu: 58, temp_mem: 66, gclk_mhz: search ? 1545 : 1300, clock_cap_mhz: search ? 1545 : 1300, telemetry_at: t, hash_now: search ? 121.1 : 120.7, hash_avg: 120.4, accepted: 102, sweep_supported: true, sweep_pct: 80, tune_control: true,
...(search ? searching : { state: 'mining', sweep_state: 'idle', sweep_note: '', tune_line: 'Tuned: 120.7 MH/s at 190 W (0.635 MH/W)', tune_source: 'full', tune_clock_mhz: 1300, sweep_at: t - 7200, sweep_watts: 190, sweep_mhs: 120.7, sweep_eff: 0.635, tune_before_watts: 374, tune_before_mhs: 122.5, tune_curve: ladder, ...locked }) },
{ ...recState.mining.cards[0], key: 'nvidia:1:NVIDIA GeForce RTX 4070', name: 'NVIDIA GeForce RTX 4070', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '5888 cores', vram_mb: 12288, device: '1', identities: 2, ids: ['9a8b7c6d'], power_default_w: 200, power_pct: 80, power_applied: true, power_limit_w: 160, power_w: 149, temp_gpu: 57, temp_mem: 0, gclk_mhz: 2100, clock_cap_mhz: 2100, telemetry_at: t, hash_now: 60.2, hash_avg: 60.0, sweep_supported: true, sweep_pct: 80, tune_control: true, ...searching, tune_step: 7, tune_steps: 12, tune_eta_s: 330, lock_step: 3, lock_steps: 8, sweep_note: 'tuning: holding 2100 MHz · 80% · 22 s (step 7 of 12)' }];
s.mining.hash_total = search ? 181.3 : 180.9; s.mining.state = 'mining'; s.display_name = 'study-pc';
s.settings = { ...s.settings, sweep: true, power_control: true, tune_goal: 'efficiency', power_price_pence: 26.32, region: 'gb' };
}
if (scenario === 'nvidia') {
s.mining.cards = [{ ...recState.mining.cards[0], key: 'nvidia:0:NVIDIA GeForce RTX 5090', name: 'NVIDIA GeForce RTX 5090', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '21760 cores', vram_mb: 32768, device: '0', identities: 8, ids: ['8fafda27', '1b2c3d4e', '5f6a7b8c'], power_default_w: 575, power_pct: 80, power_applied: true, power_limit_w: 460, power_w: 441, temp_gpu: 71, temp_mem: 92, telemetry_at: t, hash_now: 412.5, hash_avg: 405.2 },
{ ...recState.mining.cards[0], key: 'nvidia:1:NVIDIA GeForce RTX 4070', name: 'NVIDIA GeForce RTX 4070', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '5888 cores', vram_mb: 12288, device: '1', identities: 2, ids: ['9a8b7c6d'], power_default_w: 200, power_pct: 80, power_applied: false, power_note: 'nvidia-smi: wanted 160 W, card reports 200 W', power_limit_w: 200, power_w: 188, temp_gpu: 64, temp_mem: 0, telemetry_at: t, hash_now: 98.1, hash_avg: 97.4, state: 'restarting', restart_in_s: 4 },
{ ...recState.mining.cards[0], key: 'intel::Intel UHD 770', name: 'Intel UHD Graphics 770', kind: 'integrated', vendor: 'other', worker: 'OpenCL', detail: '32 EUs', vram_mb: 0, enabled: false, state: 'off', hash_now: 0, hash_avg: 0, accepted: 0, ids: [], reason: 'integrated graphics: off by default', identities: 1 }];
s.mining.hash_total = 510.6; s.display_name = 'rig-01';
}
return s;
}
const types = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff2': 'font/woff2' };
function send(res, code, type, body, cache) {
res.writeHead(code, { 'Content-Type': type, 'Cache-Control': cache ? 'public, max-age=86400' : 'no-store', 'Content-Length': Buffer.byteLength(body) });
res.end(body);
}
function json(res, v) { send(res, 200, 'application/json; charset=utf-8', JSON.stringify(v)); }
seedLog();
http.createServer((req, res) => {
const u = new URL(req.url, 'http://127.0.0.1');
if (u.pathname === '/mock/scenario') {
if (u.searchParams.get('set')) { scenario = u.searchParams.get('set'); paused = false; seedLog(); }
return json(res, { scenario, lines: log.length, posts: posts.slice(-20) });
}
if (u.pathname === '/mock/posts') return json(res, posts);
if (!u.pathname.startsWith(PREFIX)) return send(res, 404, 'text/plain', 'not found');
let rest = u.pathname.slice(PREFIX.length) || '/';
if (rest === '/' || rest === '/index.html') {
if (u.searchParams.get('scenario') && u.searchParams.get('scenario') !== scenario) { scenario = u.searchParams.get('scenario'); paused = false; seedLog(); }
return send(res, 200, types['.html'], fs.readFileSync(path.join(uiDir, 'index.html')));
}
if (rest === '/api/state' && req.method === 'GET') return json(res, stateNow());
if (rest === '/api/live' && req.method === 'GET') {
if (recLive && recLive.blocks) {
// replayed with its newest block 3 s behind now, so the module's window shows it as it was captured
const newest = Math.max(...recLive.blocks.map(b => b.ts || 0)), dt = Date.now() - 3000 - newest;
const d = JSON.parse(JSON.stringify(recLive)); d.blocks.forEach(b => { b.ts = (b.ts || newest) + dt; if (!b.miner) b.miner = 'unknown'; }); d.now = Math.floor(Date.now() / 1000); return json(res, d);
}
return json(res, liveNow(parseInt(u.searchParams.get('window') || '120', 10)));
}
if (rest === '/api/ladder' && req.method === 'GET') return json(res, ladderReply(now()));
if (rest === '/api/log' && req.method === 'GET') {
const after = parseInt(u.searchParams.get('after') || '0', 10), limit = Math.min(20000, parseInt(u.searchParams.get('limit') || '400', 10));
let out = log.filter(l => l.seq > after); if (out.length > limit) out = out.slice(out.length - limit);
return json(res, { lines: out });
}
if (req.method === 'POST') {
let body = ''; req.on('data', c => { body += c; }); req.on('end', () => {
posts.push({ t: now(), path: rest, body: body.slice(0, 300) }); console.log('POST', rest, body.slice(0, 200));
if (rest === '/api/pause') paused = true; if (rest === '/api/resume') paused = false;
if (rest === '/api/key/reveal') return json(res, { ok: true, private_key: '0x' + 'ab'.repeat(32) });
if (rest === '/api/card') { let bytes = 0; try { bytes = Buffer.from((JSON.parse(body).png || '').split(',')[1] || '', 'base64').length; } catch (e) { } return json(res, { ok: true, path: '/Users/you/Library/Application Support/Igneum/cards/igneum-block-block-1-' + Math.floor(now()) + '.png', bytes }); }
if (rest === '/api/setup') return json(res, { ok: true, address: '0xdd442fcbb964a3afdc90d49b408e8dd296fa86e8', display: '0xdd442fCbb964A3aFDc90D49B408e8DD296FA86E8', private_key: '0x' + 'cd'.repeat(32), wallet_file: '/Users/you/Library/Application Support/Igneum/app/wallet.json' });
json(res, { ok: true });
});
return;
}
if (rest === '/coin.png') return send(res, 200, types['.png'], fs.readFileSync(coin), true);
const file = path.normalize(path.join(uiDir, rest));
if (!file.startsWith(uiDir) || !fs.existsSync(file) || fs.statSync(file).isDirectory()) return send(res, 404, 'text/plain', 'not found');
send(res, 200, types[path.extname(file)] || 'application/octet-stream', fs.readFileSync(file), rest.startsWith('/fonts/'));
}).listen(port, '127.0.0.1', () => console.log('ui-mock on http://127.0.0.1:' + port + PREFIX + '/ scenario=' + scenario));