ui-mock replays a captured machine's state and chain feed (IGNEUM_MOCK_STATE, IGNEUM_MOCK_LIVE); the fleet watts count mining and tuning cards only; PC 2's 0.3.16 state rendered in light and dark

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-06 22:38:14 +00:00
parent 6dd3c3686b
commit 24adda1025
6 changed files with 18 additions and 4 deletions

View file

@ -344,8 +344,9 @@ var View = (function () {
// MH per watt: the engine's live figure, else rate over draw
function effOf(cd) { if (cd.eff_mhw > 0) return cd.eff_mhw; return cd.power_w > 0 && cd.hash_now > 0 ? cd.hash_now / cd.power_w : 0; }
function effText(e) { return e > 0 ? (e >= 10 ? e.toFixed(1) : e.toFixed(2)) + ' MH/W' : ''; }
// the watts drawn now by the present cards
function wattsTotal(cards) { return (cards || []).filter(present).reduce(function (a, c) { return a + (c.power_w > 0 ? c.power_w : 0); }, 0); }
// the watts drawn now by the cards that are mining or tuning (an idle card's standby draw is not the fleet's cost;
// the 0.3.17 engine's mining.watts_total counts the same way)
function wattsTotal(cards) { return (cards || []).filter(function (c) { return present(c) && (c.state === 'mining' || c.state === 'tuning'); }).reduce(function (a, c) { return a + (c.power_w > 0 ? c.power_w : 0); }, 0); }
// why a row has no power or temperature cell: one sentence, once, instead of three n/a
function noReading(cd) {
if (cd.power_w > 0 || cd.temp_gpu > 0) return '';

View file

@ -233,6 +233,7 @@ test('money: £ a day from watts at a price, nothing without one, the cells leav
assert.equal(apple.noReading, 'No power or temperature reading on Apple silicon');
assert.equal(V.cells(card({ kind: 'integrated', vendor: 'other', power_w: 0, temp_gpu: 0, eff_mhw: 0 }), 28).noReading, 'No power or temperature reading on an integrated GPU');
assert.equal(V.wattsTotal([card(), card({ key: 'b', power_w: 100 }), card({ key: 'c', power_w: 50, removed_at: 5 })]), 510.2);
assert.equal(V.wattsTotal([card(), card({ key: 'd', power_w: 48, state: 'off', enabled: false })]), 410.2, 'an idle card’s standby draw is not counted');
assert.equal(V.effText(V.effOf(card({ eff_mhw: 0.422 }))), '0.42 MH/W');
assert.equal(V.effText(V.effOf(card({ eff_mhw: 0, power_w: 0 }))), '');
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

View file

@ -31,7 +31,10 @@ const coin = path.resolve(here, '../../brand/igneum-coin-1024.png');
const port = parseInt(process.argv[2] || '4310', 10);
const PREFIX = '/t/mock';
const recState = JSON.parse(fs.readFileSync(path.join(here, 'recorded/state.json'), 'utf8'));
// 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
@ -116,6 +119,8 @@ function stateNow() {
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;
@ -180,7 +185,14 @@ http.createServer((req, res) => {
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') return json(res, liveNow(parseInt(u.searchParams.get('window') || '120', 10)));
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/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);