diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index 7e9e1218a..92089252f 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -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 ''; diff --git a/app/igneum-app/ui/view.test.mjs b/app/igneum-app/ui/view.test.mjs index b85b30bf7..801d53098 100644 --- a/app/igneum-app/ui/view.test.mjs +++ b/app/igneum-app/ui/view.test.mjs @@ -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 }))), ''); }); diff --git a/docs/plans/miner-ui-4/pc2-0.3.16/pc2-cards-light-1440.png b/docs/plans/miner-ui-4/pc2-0.3.16/pc2-cards-light-1440.png new file mode 100644 index 000000000..1bf031c3b Binary files /dev/null and b/docs/plans/miner-ui-4/pc2-0.3.16/pc2-cards-light-1440.png differ diff --git a/docs/plans/miner-ui-4/pc2-0.3.16/pc2-overview-dark-1440.png b/docs/plans/miner-ui-4/pc2-0.3.16/pc2-overview-dark-1440.png new file mode 100644 index 000000000..6851936c5 Binary files /dev/null and b/docs/plans/miner-ui-4/pc2-0.3.16/pc2-overview-dark-1440.png differ diff --git a/docs/plans/miner-ui-4/pc2-0.3.16/pc2-overview-light-1440.png b/docs/plans/miner-ui-4/pc2-0.3.16/pc2-overview-light-1440.png new file mode 100644 index 000000000..024de8c30 Binary files /dev/null and b/docs/plans/miner-ui-4/pc2-0.3.16/pc2-overview-light-1440.png differ diff --git a/tools/ui-mock/server.mjs b/tools/ui-mock/server.mjs index 80f233fc5..b4644e960 100644 --- a/tools/ui-mock/server.mjs +++ b/tools/ui-mock/server.mjs @@ -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= replays a state captured from a real machine (relay/playbooks/app-state-grab.ps1) instead of +// the recorded one; IGNEUM_MOCK_LIVE= 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);