the project lead: can the drivers be packaged with the miner, for all cards, with the system knowing which to install if not
present. Not bundled: detected and installed on one click. A per-vendor table rides the signed manifest (drivers.json:
min_version, the version on offer, the vendor's URL, size, sha256 and its source page, the silent arguments, the
restart exit codes, the Authenticode signer, the Linux and HiveOS package), validated by the signer and the app alike
(src/drivertable.rs, shared), written to <app data>/drivers.json by ota.rs. Each card's driver version is read at
every detection (nvidia-smi's driver_version; Windows' DriverVersion for AMD and Intel) and compared; a missing or
old driver puts the offer on the card's row, on the dashboard and on the first-run list. The click downloads with
curl (resume), checks size, sha256 and the Authenticode subject, runs the installer through one elevated prompt
(platform::elevated_command, the PC 1 driver job's shape), reports restart required with a Restart now button, and
never restarts by itself; the miners keep mining. macOS: no step; Linux and HiveOS: the package line. Dry run through
IGNEUM_DRIVER_DRY_RUN or the table. Tests: the table, the versions, the offers per tier, the exit codes, the
Authenticode verdicts, the download against a mocked vendor server on 127.0.0.1, the UI's strip per state; the mock's
drivers scenarios; captures light and dark in docs/plans/driver-check-shots. publish-manifest.sh --drivers carries the
table. Also the doubled #[test] in detect.rs from the cherry-pick.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 3f0ef65786)
289 lines
33 KiB
JavaScript
289 lines
33 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), the key under the dust line
|
|
// 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.
|
|
// 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=cards|earnings|settings for the three lines
|
|
// 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) {
|
|
const base = { 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 };
|
|
const hash = '9f3a7c1e5d2b4a6f8e0c1d3b5a7f9e2c4d6b8a0f1e3c5d7b9a2f4e6c8d0b1a3c21e';
|
|
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 } };
|
|
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 } };
|
|
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 === '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; }
|
|
s.uptime_s = recState.uptime_s + Math.floor(t - t0);
|
|
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') 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 === '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') {
|
|
// 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: '581.57', wanted: '581.57', min_version: '570.00', size: 1012345678, url: 'https://us.download.nvidia.com/Windows/581.57/581.57-desktop-win10-win11-64bit-international-dch-whql.exe', hash_source: 'https://www.nvidia.com/en-us/drivers/', text: 'NVIDIA driver 581.57: 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.6733', wanted: '32.0.101.9034', min_version: '32.0.101.9034', 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.6733 is older than the 32.0.101.9034 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: '581.57', driver_os: '581.57', 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-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 === '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));
|