igneum/app/igneum-app/ui/app.js
igneum-labs 9f0c9036ea Build job: a PC builds the node and the app engine for Linux and Windows inside WSL2, mining untouched
New job kind `build` (jobs.rs, jobbuild.rs, jobrun.rs run_build): free-space check on both sides (20 GB), the
build-inputs zip by sha256, setup inside the distro as root (mingw-w64 posix, clang for bindgen, protoc, zstd, the
Windows rust target; idempotent), sources extracted with the target dir persisting under /root/igneum-build,
cargo build --release native and for x86_64-pc-windows-gnu, cargo test for the manifest's packages, binaries
zstd-compressed and sent to the relay (fn=upload, Blob PUT, fn=drop; 50 MB each) with sha256 in RESULT lines,
STAGE lines with UTC times, a 40-minute default budget and per-stage caps, the Linux side killed on a cap. The
app's runner stays serial (one Active at a time), so a build never overlaps a shard job; nothing stops the miners.
From this version an unknown job kind is skipped by the app (parse_lenient) instead of rejecting the whole file;
the signer stays strict.

Mac side: packaging/windows/push-build-inputs.sh packs a fork worktree, app/igneum-app, brand/icons and
proto-cuda with a manifest (branch, commit, dirty, builds, tests) and the sha256; publish-jobs.sh add --kind build;
tools/build-job.mjs packs, publishes, watches, fetches, checks both sha256 per file and the PE header of every exe
(plus verify-exe.py on igneum-app.exe), and places the binaries where push-inputs.sh, make-payload.sh and the
cloud-devnet scripts look. relay.mjs drop <file> --body carries the body.

Tested on the Mac: 33 app tests (6 new) and the signer's 21; cargo check for x86_64-pc-windows-gnu; the packer
(7.9 MB zip, no target dirs); the publisher against a scratch folder with the rebuilt signer, the old signer
refusing the kind, a bad job refused at signing; the fetch path against the live relay with a real exe (sha256
and PE pass, a wrong sha256 refused; test items deleted). Not run on a PC: the job itself. docs/plans/build-job.md
has the first job for PC 1 and the rollout order (0.3.4 must be on the PCs before a build job is published).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 20:17:55 +00:00

560 lines
44 KiB
JavaScript

/* Igneum Miner dashboard. Talks to the engine on the same origin: api/state every second, api/log while the
drawer is open. Screens: welcome, cards, address (plus the one-time key sheet), dashboard. ?screen=<name> forces
a screen (screenshots). Everything is relative to the token path the page was opened on. */
(function () {
'use strict';
var $ = function (id) { return document.getElementById(id); };
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var dpr = Math.min(2, window.devicePixelRatio || 1);
var state = null, phase = 'welcome', forced = null, cardsSeen = false, keyShownThisRun = false;
var params = new URLSearchParams(location.search);
if (params.get('screen')) forced = params.get('screen');
var forcedUpdate = params.get('update'); // a sample update state for screenshots: available|downloading|ready|waiting|applying|urgent|manual|error|updated
if (params.get('host') === 'mac') document.body.classList.add('mac');
// ---------- helpers ----------
function api(path, body) {
var opt = body === undefined ? { cache: 'no-store' } : { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) };
return fetch(path, opt).then(function (r) { return r.json(); });
}
function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
function compact(n) { if (n === null || n === undefined || isNaN(n)) return '0'; var a = Math.abs(n); if (a >= 1e12) return (n / 1e12).toFixed(2) + 'T'; if (a >= 1e9) return (n / 1e9).toFixed(2) + 'G'; if (a >= 1e6) return (n / 1e6).toFixed(2) + 'M'; if (a >= 1e4) return (n / 1e3).toFixed(1) + 'k'; return String(Math.round(n)); }
function withCommas(n) { return String(Math.round(n || 0)).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }
function rel(s) { s = Math.max(0, Math.round(s)); if (s < 60) return s + ' s ago'; var m = Math.floor(s / 60); if (m < 60) return m + ' min ago'; var h = Math.floor(m / 60); if (h < 24) return h + ' h ' + (m % 60) + ' min ago'; return Math.floor(h / 24) + ' d ago'; }
function hms(s) { s = Math.max(0, Math.round(s)); var h = Math.floor(s / 3600), m = Math.floor(s % 3600 / 60), x = s % 60; return (h ? h + ':' : '') + (h ? String(m).padStart(2, '0') : m) + ':' + String(x).padStart(2, '0'); }
function uptime(s) { var h = Math.floor(s / 3600), m = Math.floor(s % 3600 / 60); return h ? h + ' h ' + m + ' min' : m + ' min'; }
function clock(t) { var d = new Date(t * 1000); return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0') + ':' + String(d.getSeconds()).padStart(2, '0'); }
var toastT = null;
function toast(text) { var t = $('toast'); t.textContent = text; t.hidden = false; clearTimeout(toastT); toastT = setTimeout(function () { t.hidden = true; }, 2200); }
function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function () { toast('Copied'); }, function () { fallbackCopy(text); }); }
else fallbackCopy(text);
}
function fallbackCopy(text) { var ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); toast('Copied'); } catch (e) { toast('Select and copy it by hand'); } document.body.removeChild(ta); }
document.addEventListener('click', function (e) {
var b = e.target.closest('[data-copy]'); if (!b) return;
copyText($(b.getAttribute('data-copy')).textContent);
});
// ---------- screens ----------
function show(p) {
phase = p;
document.body.setAttribute('data-phase', p);
var dash = p === 'dashboard';
$('bottom').hidden = !dash;
$('btn-settings').hidden = !dash;
document.body.classList.toggle('has-bottom', dash);
if (!dash) document.body.classList.remove('drawer-open');
if (p === 'cards' && !cardsSeen) { cardsSeen = true; api('api/detect', {}); }
api('api/phase', { phase: p });
}
$('btn-begin').addEventListener('click', function () { show('cards'); });
$('btn-cards-next').addEventListener('click', function () {
var choices = readCardRows($('cards-list'));
if (choices.length) api('api/cards', { cards: choices }).then(function () { show('address'); }); else show('address');
});
$('btn-cards-retry').addEventListener('click', function () { cardsSeen = false; $('cards-detecting').hidden = false; api('api/detect', {}); });
$('btn-address-back').addEventListener('click', function () { show('cards'); });
// address options
var opts = document.querySelectorAll('.option');
function pickOption(el) { opts.forEach(function (o) { o.classList.toggle('on', o === el); }); el.querySelector('input').checked = true; if (el.id === 'opt-paste') $('addr-input').focus(); }
opts.forEach(function (o) { o.addEventListener('click', function (e) { if (e.target.tagName !== 'INPUT' || e.target.type === 'radio') pickOption(o); }); });
$('addr-input').addEventListener('focus', function () { pickOption($('opt-paste')); });
function validAddr(a) { return /^0x[0-9a-fA-F]{40}$/.test((a || '').trim()); }
$('btn-start').addEventListener('click', function () {
var mode = document.querySelector('input[name=mode]:checked').value;
var body = { mode: mode };
if (mode === 'paste') {
var a = $('addr-input').value.trim();
if (!validAddr(a)) { $('addr-err').hidden = false; return; }
$('addr-err').hidden = true; body.address = a;
}
$('btn-start').disabled = true;
api('api/setup', body).then(function (r) {
$('btn-start').disabled = false;
if (!r.ok) { $('addr-err').textContent = r.error || 'could not save'; $('addr-err').hidden = false; return; }
if (mode === 'generate' && r.private_key) { showKeySheet(r); }
else { api('api/start', {}).then(function () { show('dashboard'); }); }
}).catch(function () { $('btn-start').disabled = false; toast('The engine did not answer'); });
});
function showKeySheet(r) {
$('key-address').textContent = r.display || r.address;
$('key-private').textContent = r.private_key;
$('key-file').textContent = r.wallet_file || '';
$('key-ack').checked = false; $('btn-key-done').disabled = true;
$('sheet-key').hidden = false; keyShownThisRun = true;
}
$('key-ack').addEventListener('change', function () { $('btn-key-done').disabled = !this.checked; });
$('btn-key-done').addEventListener('click', function () {
api('api/key/saved', {}).then(function () { return api('api/start', {}); }).then(function () { $('sheet-key').hidden = true; $('key-private').textContent = ''; show('dashboard'); });
});
// ---------- settings ----------
$('btn-settings').addEventListener('click', function () { fillSettings(); $('settings').hidden = false; });
$('btn-settings-close').addEventListener('click', function () { $('settings').hidden = true; $('s-key-box').hidden = true; $('s-key').textContent = ''; });
$('settings').addEventListener('click', function (e) { if (e.target === $('settings')) $('btn-settings-close').click(); });
function fillSettings() {
if (!state) return;
$('s-address').textContent = state.address.display || 'not set';
renderCardRows($('s-cards'), state.mining.cards);
state.mining.cards.forEach(function (cd) {
if (cd.vendor !== 'nvidia' || !(cd.power_default_w > 0)) return;
var row = $('s-cards').querySelector('.gpu-row[data-key="' + cd.key.replace(/"/g, '\\"') + '"] .info');
if (!row) return;
var want = Math.round(cd.power_default_w * (cd.power_pct || 80) / 100);
var d = document.createElement('div'); d.className = cd.power_applied ? 'msg ok' : 'msg hot';
d.innerHTML = cd.power_applied ? 'power cap: ' + want + ' W applied' : 'power cap NOT applied (needs the administrator prompt) <button class="btn tiny" data-power-retry="1">Retry</button>';
row.appendChild(d);
});
$('s-name').value = state.display_name || '';
$('s-mid').textContent = (state.machine_id || '').slice(0, 8);
$('s-vote').checked = state.settings.vote;
$('s-login').checked = state.settings.start_at_login;
$('s-version').textContent = 'Igneum Miner ' + state.version;
$('s-node-dir').textContent = 'chain data: ' + state.node_dir;
$('s-log-dir').textContent = 'logs: ' + state.log_dir;
$('s-reveal').hidden = state.address.source !== 'generated';
$('s-live').hidden = !state.live_page;
var u = state.update;
$('s-auto-update').checked = !!state.settings.auto_update;
fillJobsSettings(state.jobs || {});
$('s-install').hidden = !((u.ready || u.downloaded || u.status === 'error') && !u.applying);
$('s-update-note').textContent = settingsUpdateNote(u);
}
$('s-address-save').addEventListener('click', function () {
var a = $('s-address-input').value.trim();
if (!validAddr(a)) { $('s-address-err').textContent = 'An address is 0x followed by 40 hex characters.'; $('s-address-err').hidden = false; return; }
api('api/settings', { address: a }).then(function (r) { if (r.ok) { $('s-address-err').hidden = true; $('s-address-input').value = ''; toast('Rewards address changed'); } else { $('s-address-err').textContent = r.error; $('s-address-err').hidden = false; } });
});
$('s-reveal').addEventListener('click', function () {
api('api/key/reveal', {}).then(function (r) { if (r.ok) { $('s-key').textContent = r.private_key; $('s-key-box').hidden = false; } else toast(r.error || 'no key'); });
});
$('s-name-save').addEventListener('click', function () {
api('api/settings', { display_name: $('s-name').value }).then(function (r) { if (r.ok) toast('Renamed'); });
});
$('s-cards-save').addEventListener('click', function () {
var choices = readCardRows($('s-cards'));
api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast('Applied; only the changed workers restart'); });
});
$('s-vote').addEventListener('change', function () { api('api/settings', { vote: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied; the miner restarts' : 'Applied'); }); });
$('s-login').addEventListener('change', function () { var on = this.checked; api('api/settings', { start_at_login: on }).then(function (r) { if (!r.ok) { toast(r.error || 'could not change'); $('s-login').checked = !on; } }); });
$('s-update').addEventListener('click', function () { api('api/update/check', {}); $('s-update-note').textContent = 'Checking.'; setTimeout(fillSettings, 4000); });
$('s-install').addEventListener('click', function () { api('api/update/install', {}); toast('Installing at once'); });
$('s-auto-update').addEventListener('change', function () { api('api/update/auto', { on: this.checked }); });
$('s-live').addEventListener('click', function () { if (state && state.live_page) api('api/open', { url: state.live_page }); });
$('update-install').addEventListener('click', function () { api('api/update/install', {}); toast('Installing at once'); });
$('update-open').addEventListener('click', function () { api('api/update/open', {}); });
$('update-later').addEventListener('click', function () { $('update-banner').hidden = true; $('update-banner').dataset.dismissed = (state && state.update) ? (state.update.status + ':' + state.update.version) : '1'; layoutBanners(); });
$('job-hide').addEventListener('click', function () { $('job-banner').hidden = true; $('job-banner').dataset.dismissed = jobKey(state && state.jobs); layoutBanners(); });
$('s-prove').addEventListener('change', function () { api('api/prove', { on: this.checked }).then(function (r) { if (r.ok) toast(r.ok && $('s-prove').checked ? 'Proving on; the first shard arrives within a minute' : 'Proving off'); }); });
$('pv-setup').addEventListener('click', function () { api('api/prove/setup', {}).then(function (r) { toast(r.ok ? 'Setup started in its own window' : (r.error || 'could not start')); }); });
$('s-jobs-allow').addEventListener('change', function () { api('api/jobs/allow', { on: this.checked }); setTimeout(fillSettings, 800); });
$('s-jobs-check').addEventListener('click', function () { api('api/jobs/check', {}); $('s-jobs-note').textContent = 'Checking.'; setTimeout(fillSettings, 4000); });
// ---------- bottom bar ----------
$('btn-pause').addEventListener('click', function () {
if (!state) return;
api(state.mining.paused ? 'api/resume' : 'api/pause', {});
});
$('btn-logs').addEventListener('click', function () { document.body.classList.toggle('drawer-open'); if (document.body.classList.contains('drawer-open')) pollLog(); });
$('btn-quit').addEventListener('click', function () {
if (!confirm('Quit Igneum Miner? The miner stops first, then the node.')) return;
api('api/quit', {}); $('pill-text').textContent = 'stopping'; $('btn-quit').disabled = true;
});
// ---------- log drawer ----------
var logAfter = 0, logFilter = '', logLines = [];
document.querySelectorAll('.chip').forEach(function (c) { c.addEventListener('click', function () { document.querySelectorAll('.chip').forEach(function (x) { x.classList.toggle('on', x === c); }); logFilter = c.getAttribute('data-src'); renderLog(); }); });
function renderLog() {
var el = $('log'), follow = $('log-follow').checked;
var lines = logLines.filter(function (l) { return !logFilter || l.src.indexOf(logFilter) === 0; }).slice(-600);
el.innerHTML = lines.map(function (l) { return '<span class="src ' + esc(l.src) + '">' + clock(l.t) + ' ' + esc(l.src.padEnd(6)) + '</span> ' + (l.err ? '<span class="e">' : '') + esc(l.text) + (l.err ? '</span>' : ''); }).join('\n');
if (follow) el.scrollTop = el.scrollHeight;
}
function pollLog() {
if (!document.body.classList.contains('drawer-open')) return;
api('api/log?after=' + logAfter + '&limit=400').then(function (r) {
if (r.lines && r.lines.length) { logLines = logLines.concat(r.lines).slice(-3000); logAfter = r.lines[r.lines.length - 1].seq; renderLog(); }
}).catch(function () { });
}
setInterval(pollLog, 1500);
// ---------- the blocks strip (your blocks over the last 10 minutes) ----------
var c = $('dag'), x = c.getContext('2d'), W = 0, H = 0, seen = {}, born = {};
function size() { W = c.clientWidth; H = c.clientHeight; if (!W || !H) return; c.width = W * dpr; c.height = H * dpr; x.setTransform(dpr, 0, 0, dpr, 0, 0); }
window.addEventListener('resize', function () { size(); });
function rrect(px, py, w, h, r) { x.beginPath(); x.moveTo(px + r, py); x.lineTo(px + w - r, py); x.quadraticCurveTo(px + w, py, px + w, py + r); x.lineTo(px + w, py + h - r); x.quadraticCurveTo(px + w, py + h, px + w - r, py + h); x.lineTo(px + r, py + h); x.quadraticCurveTo(px, py + h, px, py + h - r); x.lineTo(px, py + r); x.quadraticCurveTo(px, py, px + r, py); x.closePath(); }
function draw(now) {
if (!W || !H) size();
if (!W) return;
x.clearRect(0, 0, W, H);
var span = 600, S = 18, pad = 10, right = W - 26, left = pad;
var pxs = (right - left) / span;
var nowU = state ? (state.now + (performance.now() - stateAt) / 1000) : Date.now() / 1000;
// minute lines
x.font = '500 10px IBM Plex Mono, monospace'; x.textAlign = 'center';
for (var m = 0; m <= 10; m++) {
var t = nowU - m * 60, px = right - (nowU - t) * pxs;
x.strokeStyle = m === 0 ? 'rgba(255,179,92,0.5)' : 'rgba(42,42,48,0.9)'; x.lineWidth = 1; x.setLineDash(m === 0 ? [] : [3, 6]);
x.beginPath(); x.moveTo(px, 14); x.lineTo(px, H - 18); x.stroke(); x.setLineDash([]);
if (px > 30) { x.fillStyle = 'rgba(154,154,158,0.8)'; x.fillText(m === 0 ? 'now' : '-' + m + ' min', px, H - 5); }
}
var found = state ? state.mining.found : [];
var rail = H * 0.5;
x.strokeStyle = 'rgba(58,58,66,0.9)'; x.lineWidth = 1; x.beginPath(); x.moveTo(left, rail); x.lineTo(right, rail); x.stroke();
var shown = 0;
found.forEach(function (t, i) {
var age = nowU - t; if (age < 0 || age > span) return; shown++;
var key = String(t);
if (!born[key]) born[key] = now;
var fl = Math.max(0, 1 - (now - born[key]) / 1200);
var px = right - age * pxs, py = rail + ((i % 3) - 1) * 22;
if (fl > 0) { var g = x.createRadialGradient(px, py, 0, px, py, S * 2.2); g.addColorStop(0, 'rgba(255,179,92,' + (0.6 * fl) + ')'); g.addColorStop(1, 'rgba(255,179,92,0)'); x.fillStyle = g; x.beginPath(); x.arc(px, py, S * 2.2, 0, Math.PI * 2); x.fill(); }
x.fillStyle = '#F2541B'; rrect(px - S / 2, py - S / 2, S, S, 5); x.fill();
if (fl > 0) { x.strokeStyle = 'rgba(255,179,92,' + fl + ')'; x.lineWidth = 2; rrect(px - S / 2 - 2, py - S / 2 - 2, S + 4, S + 4, 6); x.stroke(); }
});
if (!shown) {
x.fillStyle = 'rgba(154,154,158,0.9)'; x.font = '500 12px IBM Plex Mono, monospace'; x.textAlign = 'center';
var msg = !state ? 'waiting for the engine' : state.mining.state === 'mining' ? 'mining: your blocks appear here as the node accepts them' : state.mining.state === 'paused' ? 'paused' : state.node.synced ? 'waiting for the worker' : 'waiting for the node to sync';
x.fillText(msg, W / 2, rail - 14);
}
}
var stateAt = performance.now();
function frame(now) { if (phase === 'dashboard') draw(now); if (!reduce) requestAnimationFrame(frame); else setTimeout(function () { frame(performance.now()); }, 1000); }
requestAnimationFrame(frame);
// ---------- render ----------
// ---------- GPU rows: one per card, with a switch and an identities stepper ----------
function badgeHtml(cd) {
if (cd.vendor === 'apple') return '<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg>';
return cd.vendor === 'nvidia' ? 'NV' : cd.vendor === 'amd' ? 'AMD' : 'GPU';
}
function kindWord(k) { return k === 'apple' ? 'Apple silicon' : k === 'discrete' ? 'Discrete' : k === 'integrated' ? 'Integrated' : k === 'external' ? 'External' : 'Unknown'; }
function vramText(cd) { if (!cd.vram_mb) return ''; var gb = cd.vram_mb / 1024; return (gb >= 10 ? Math.round(gb) : gb.toFixed(gb < 4 ? 1 : 0)) + ' GB' + (cd.kind === 'apple' ? ' unified' : ''); }
function renderCardRows(container, cards) {
container.querySelectorAll('.gpu-row').forEach(function (n) { n.remove(); });
cards.forEach(function (cd) {
var d = document.createElement('div');
d.className = 'gpu-row' + (cd.enabled ? '' : ' off');
d.dataset.key = cd.key;
var meta = '<span>worker <b>' + esc(cd.worker) + '</b></span>' + (vramText(cd) ? '<span>' + esc(vramText(cd)) + '</span>' : '') + (cd.detail && cd.kind !== 'apple' ? '<span>' + esc(cd.detail) + '</span>' : '') + (cd.detail && cd.kind === 'apple' ? '<span>' + esc(cd.detail.split(',')[0]) + '</span>' : '') + (cd.device ? '<span>device <b>' + esc(cd.device) + '</b></span>' : '');
d.innerHTML = '<div class="badge ' + esc(cd.vendor) + '">' + badgeHtml(cd) + '</div>' +
'<div class="info"><div class="name">' + esc(cd.name) + ' <span class="kind ' + esc(cd.kind) + '">' + kindWord(cd.kind) + '</span></div><div class="meta">' + meta + '</div>' +
(cd.reason ? '<div class="reason">' + esc(cd.reason) + '</div>' : '') + (cd.message ? '<div class="msg">' + esc(cd.message) + '</div>' : '') + '</div>' +
(cd.vendor === 'nvidia' && cd.power_default_w > 0 ? '<div class="power"><span class="k">power cap</span><input type="range" min="60" max="100" step="5" value="' + (cd.power_pct || 80) + '"><span class="pv mono">' + (cd.power_pct || 80) + '% · ' + Math.round(cd.power_default_w * (cd.power_pct || 80) / 100) + ' W</span></div>' : '') +
'<div class="ids"><span class="k">identities</span><button type="button" data-d="-1" aria-label="fewer">&minus;</button><input type="number" min="1" max="64" value="' + cd.identities + '"><button type="button" data-d="1" aria-label="more">+</button></div>' +
'<label class="switch" title="' + (cd.enabled ? 'mining on this card' : 'not mining on this card') + '"><input type="checkbox"' + (cd.enabled ? ' checked' : '') + (cd.kind === 'unknown' ? ' disabled' : '') + '><span class="track"></span></label>';
container.appendChild(d);
});
}
function readCardRows(container) {
var out = [];
container.querySelectorAll('.gpu-row').forEach(function (r) {
var n = parseInt(r.querySelector('.ids input').value, 10); if (!(n >= 1)) n = 1; if (n > 64) n = 64;
var pr = r.querySelector('.power input'), item = { key: r.dataset.key, enabled: r.querySelector('.switch input').checked, identities: n };
if (pr) item.power_pct = parseInt(pr.value, 10);
out.push(item);
});
return out;
}
document.addEventListener('click', function (e) {
var b = e.target.closest('.ids button'); if (!b) return;
var inp = b.parentNode.querySelector('input'), v = parseInt(inp.value, 10) || 1;
v = Math.max(1, Math.min(64, v + parseInt(b.dataset.d, 10))); inp.value = v;
});
document.addEventListener('change', function (e) {
if (e.target.matches('.gpu-row .switch input')) { var r = e.target.closest('.gpu-row'); r.classList.toggle('off', !e.target.checked); }
});
document.addEventListener('input', function (e) {
if (!e.target.matches('.power input')) return;
var row = e.target.closest('.gpu-row'), cd = state && state.mining.cards.filter(function (c) { return c.key === row.dataset.key; })[0];
var pct = parseInt(e.target.value, 10);
row.querySelector('.pv').textContent = pct + '%' + (cd && cd.power_default_w ? ' · ' + Math.round(cd.power_default_w * pct / 100) + ' W' : '');
});
var cardsRendered = '';
function renderCards(s) {
var list = $('cards-list'), det = $('cards-detecting');
if (s.detecting) { det.hidden = false; $('detect-text').textContent = 'asking the graphics cards to report in'; $('btn-cards-next').disabled = true; $('cards-sub').textContent = 'Asking the graphics cards to report in.'; cardsRendered = ''; return; }
det.hidden = true;
var cards = s.mining.cards;
var sig = cards.map(function (c) { return c.key; }).join('|');
if (sig !== cardsRendered) { cardsRendered = sig; renderCardRows(list, cards); }
if (!cards.length) {
$('cards-sub').textContent = 'No GPU this app can drive was found.';
$('cards-note').textContent = (s.detect_message || '') + ' The node can still run on its own; mining needs a supported card.';
$('cards-note').hidden = false; $('cards-help').hidden = true; $('btn-cards-retry').hidden = false; $('btn-cards-next').disabled = false; $('btn-cards-next').textContent = 'Continue without mining';
return;
}
var on = cards.filter(function (c) { return c.enabled; }).length;
$('cards-sub').textContent = cards.length === 1 ? 'One card found. Switch it on to mine with it.' : cards.length + ' GPUs found. Switch on the ones you want to mine with; each gets its own worker.';
$('cards-note').hidden = !s.detect_message; $('cards-note').textContent = s.detect_message || '';
$('cards-help').hidden = false;
var nv = cards.filter(function (c) { return c.vendor === 'nvidia' && c.power_default_w > 0; }).length;
$('cards-power').hidden = !nv;
$('btn-cards-retry').hidden = true; $('btn-cards-next').disabled = false; $('btn-cards-next').textContent = on ? 'Continue' : 'Continue';
}
// power draw, the cap and the temperatures (NVIDIA); memory over 90 C amber, over 95 C red
function telemetryHtml(cd) {
if (cd.vendor !== 'nvidia' || !(cd.telemetry_at > 0 || cd.power_default_w > 0)) return '';
var memCls = cd.temp_mem > 95 ? 'hot' : cd.temp_mem > 90 ? 'warm' : '';
var h = '<div class="m tele"><span>draw <b>' + (cd.power_w ? Math.round(cd.power_w) + ' W' : 'n/a') + '</b>' + (cd.power_limit_w ? ' <span class="dim">/ cap ' + Math.round(cd.power_limit_w) + ' W</span>' : '') + '</span>' +
'<span>GPU <b>' + (cd.temp_gpu ? Math.round(cd.temp_gpu) + ' °C' : 'n/a') + '</b></span>' +
'<span class="' + memCls + '">memory <b>' + (cd.temp_mem ? Math.round(cd.temp_mem) + ' °C' : 'n/a') + '</b></span></div>';
if (memCls) h += '<div class="msg ' + memCls + '">memory ' + Math.round(cd.temp_mem) + ' °C: card throttling or at risk</div>';
if (cd.power_default_w > 0) {
var want = Math.round(cd.power_default_w * (cd.power_pct || 80) / 100);
if (cd.power_applied) h += '<div class="msg ok">power cap: ' + want + ' W applied</div>';
else h += '<div class="msg hot">power cap NOT applied (needs the administrator prompt)' + (cd.power_note && cd.power_note.indexOf('wanted') > 0 ? ' · ' + esc(cd.power_note.replace(/^.*: card reports/, 'card reports')) : '') + ' <button class="btn tiny" data-power-retry="1">Retry</button></div>';
}
return h;
}
document.addEventListener('click', function (e) {
var b = e.target.closest('[data-power-retry]'); if (!b) return;
api('api/power/apply', {}).then(function () { toast('Administrator prompt: allow it to set the cap'); });
});
var lastEventsKey = '';
function renderDashboard(s) {
var m = s.mining, n = s.node, p = s.program, f = s.finality;
$('d-hash').textContent = m.hash_total >= 100 ? m.hash_total.toFixed(0) : m.hash_total.toFixed(1);
var mining = m.cards.filter(function (c) { return c.state === 'mining'; }).length;
$('d-hash-sub').textContent = m.state === 'paused' ? 'paused' : mining ? (mining + ' of ' + m.cards.length + ' card' + (m.cards.length === 1 ? '' : 's') + ' mining') : m.state === 'waiting' ? (n.synced ? 'worker starting' : 'waiting for the node') : m.state === 'idle' ? 'no GPU worker' : m.state;
$('d-blocks').textContent = withCommas(m.accepted_total);
$('d-blocks-sub').textContent = m.accepted_session + ' this run' + (m.found.length ? ' · ' + m.found.length + ' last hour' : '') + (m.rejected_session ? ' · ' + m.rejected_session + ' rejected' : '');
var nodeWord = n.state === 'synced' ? 'synced' : n.state === 'syncing' ? 'syncing' : n.state;
$('d-node').textContent = nodeWord;
// the live numbers always show, so a stuck label is visible as stuck
var live = 'height ' + withCommas(n.blocks) + (n.headers > n.blocks ? ' of ' + withCommas(n.headers) : '') + ' · daa ' + withCommas(n.daa) + ' · ' + n.peers + ' peer' + (n.peers === 1 ? '' : 's');
$('d-node-sub').textContent = (s.clock && s.clock.severity === 'block') ? 'clock ' + Math.round(Math.abs(s.clock.skew_s)) + ' s ' + (s.clock.skew_s < 0 ? 'behind' : 'ahead') + ': fix it to mine' : n.state === 'restarting' ? ('restart in ' + n.restart_in_s + ' s · ' + live) : (n.state === 'syncing' && n.message ? n.message + ' · ' : '') + live;
$('d-node-sub').title = n.last_reading_age_s >= 0 ? 'last reading ' + Math.round(n.last_reading_age_s) + ' s ago' : '';
var cell = $('d-node-cell'); cell.classList.toggle('ok', n.state === 'synced'); cell.classList.toggle('bad', n.state === 'failed' || n.state === 'restarting' || n.state === 'stopped' || (s.clock && s.clock.severity === 'block'));
if (n.daa > 0 && p.eta_s >= 0) { $('d-eta').textContent = hms(p.eta_s); $('d-eta-sub').textContent = p.message + (p.epoch_index ? ' (epoch ' + p.epoch_index + ')' : ''); }
else { $('d-eta').textContent = '--:--'; $('d-eta-sub').textContent = 'waiting for the node'; }
var nowU = s.now;
$('d-found-10').textContent = m.found.filter(function (t) { return nowU - t <= 600; }).length;
$('d-found-60').textContent = m.found.length;
$('d-chain-blocks').textContent = compact(n.blocks);
// per-card tiles (enabled cards), the rest in one muted line
var on = m.cards.filter(function (c) { return c.enabled; }), off = m.cards.filter(function (c) { return !c.enabled; });
$('d-cards-eyebrow').textContent = on.length + ' worker' + (on.length === 1 ? '' : 's') + (m.cards.length > on.length ? ' of ' + m.cards.length + ' GPUs' : '');
var tb = $('d-cards');
if (!on.length) tb.innerHTML = '<div class="empty">' + (m.cards.length ? 'No card switched on. Open settings to pick one.' : 'No GPU worker. The node runs on its own.') + '</div>';
else tb.innerHTML = on.map(function (cd) {
var cls = cd.state === 'mining' ? 'mining' : (cd.state === 'failed' || cd.state === 'restarting') ? 'bad' : '';
var word = cd.state === 'restarting' ? ('restart in ' + cd.restart_in_s + ' s') : cd.state;
var extra = cd.ids.length ? '<span>id <b>' + esc(cd.ids.join(' ')) + '</b></span>' : '';
return '<div class="gpu-tile' + (cd.state === 'mining' ? '' : ' idle') + '"><div class="n">' + esc(cd.name) + ' <span class="kind ' + esc(cd.kind) + '">' + kindWord(cd.kind) + '</span></div>' +
'<div class="h">' + cd.hash_now.toFixed(1) + '<span class="unit">MH/s</span></div>' +
'<div class="m"><span>blocks <b>' + cd.accepted + '</b>' + (cd.rejected ? ' <span class="dim">/ ' + cd.rejected + ' rejected</span>' : '') + '</span><span>avg <b>' + cd.hash_avg.toFixed(1) + '</b></span><span>identities <b>' + cd.identities + '</b></span>' + extra + '</div>' +
telemetryHtml(cd) +
'<span class="st ' + cls + '"><i></i>' + esc(word) + (cd.prepared ? ' · next program ready' : '') + '</span>' + (cd.message ? '<div class="msg">' + esc(cd.message) + '</div>' : '') + '</div>';
}).join('');
$('d-cards-off').hidden = !off.length;
$('d-cards-off').textContent = off.length ? 'off: ' + off.map(function (c) { return c.name + ' (' + kindWord(c.kind).toLowerCase() + ')'; }).join(', ') + '. Settings switches them on.' : '';
// node card
$('d-node-net').textContent = s.chain + (n.version ? ' · ' + n.version.replace(/^igneumd\s*/, '') : '');
$('n-blocks').textContent = withCommas(n.blocks); $('n-headers').textContent = withCommas(n.headers); $('n-peers').textContent = n.peers;
$('n-daa').textContent = withCommas(n.daa); $('n-diff').textContent = compact(n.difficulty); $('n-tips').textContent = n.tips;
var consensus = n.consensus_switch_daa > 0 ? ' Consensus switch at DAA ' + withCommas(n.consensus_switch_daa) + (n.daa > 0 && n.consensus_switch_daa > n.daa ? ' (' + withCommas(n.consensus_switch_daa - n.daa) + ' blocks away).' : '.') + (n.override_restart_wait ? ' ' + n.override_restart_wait + '.' : '') : '';
$('n-note').textContent = (n.state === 'synced' ? ('Reading every 10 s' + (n.last_reading_age_s >= 0 ? ', last ' + Math.round(n.last_reading_age_s) + ' s ago' : '') + '.') : (n.message ? n.message + '.' : '')) + consensus;
// finality
if (f.last_lock > 0) { $('f-lock').textContent = '#' + withCommas(f.last_lock); $('f-age').textContent = rel(f.age_s); $('f-note').textContent = 'Checkpoints lock at 2/3 of the 30-day weight. Votes are sent by this miner.'; }
else { $('f-lock').textContent = 'none yet'; $('f-age').textContent = 'n/a'; $('f-note').textContent = f.message || 'Locks appear once the miner votes on checkpoints.'; }
// proving
var pv = s.proving || {};
var pvWord = { off: 'off', setup: 'needs setup', waiting: 'waiting', idle: 'idle', proving: 'proving', submitted: 'submitted' }[pv.status] || (pv.status || 'off');
$('pv-state').textContent = pvWord + (pv.status === 'proving' && pv.current ? ' ' + pv.current : '') + (pv.backend && pv.enabled && pv.available ? ' (' + pv.backend.toUpperCase() + ')' : '');
$('pv-assigned').textContent = String(pv.assigned || 0);
$('pv-submitted').textContent = String(pv.submitted || 0);
$('pv-paid').textContent = String(pv.paid || 0) + (pv.paid_wei ? ' (' + (Number(pv.paid_wei) / 1e18).toFixed(4) + ' IGN)' : '');
$('pv-note').textContent = pv.enabled ? (pv.message || '') : 'Off. Settings switches it on: this machine proves the shards the chain assigns to its keys.';
$('pv-setup-row').hidden = !(pv.enabled && !pv.available && pv.setup_hint);
$('f-votes').textContent = withCommas(f.votes);
// events
var key = s.events.length ? s.events[0].t + ':' + s.events.length : '';
if (key !== lastEventsKey) {
lastEventsKey = key;
$('d-events').innerHTML = s.events.length ? s.events.slice(0, 14).map(function (e) { return '<div><span><span class="k ' + esc(e.kind) + '">' + esc(e.kind) + '</span>' + esc(e.text) + '</span><span class="t">' + clock(e.t) + '</span></div>'; }).join('') : '<div class="empty">No events yet.</div>';
}
// bottom
$('btn-pause').textContent = m.paused ? 'Resume' : 'Pause';
$('foot-status').textContent = (s.quitting ? 'stopping: miners first, then the node' : (s.display_name ? s.display_name + ' · ' : '') + (m.state === 'mining' ? 'mining' : m.state) + ' · node ' + nodeWord + ' · up ' + uptime(s.uptime_s) + (s.address.display ? ' · rewards to ' + s.address.display.slice(0, 8) + '…' + s.address.display.slice(-4) : ''));
$('foot-version').textContent = 'v' + s.version;
}
function renderPill(s) {
var pill = $('pill'), text;
pill.classList.remove('on', 'warn');
if (s.quitting) text = 'stopping';
else if (!s.setup_done) text = s.detecting ? 'detecting' : 'set up';
else if (s.mining.state === 'mining') { text = s.mining.hash_total.toFixed(1) + ' MH/s'; pill.classList.add('on'); }
else if (s.mining.state === 'paused') text = 'paused';
else if (s.node.state === 'syncing' || s.node.state === 'starting') text = 'node ' + s.node.state;
else if (s.node.state === 'failed' || s.node.state === 'restarting') { text = 'node ' + s.node.state; pill.classList.add('warn'); }
else if (s.mining.state === 'idle' && s.node.state === 'synced') { text = 'node synced'; pill.classList.add('on'); }
else text = s.mining.state;
$('pill-text').textContent = text;
}
function renderClock(s) {
var c = s.clock || { severity: 'none' }, bad = c.severity === 'block', warn = c.severity === 'warn';
var banner = $('clock-banner');
banner.hidden = !(bad || warn) || phase === 'dashboard';
banner.classList.toggle('warn', warn);
$('clock-banner-text').textContent = c.message || '';
$('clock-banner-hint').textContent = c.hint || '';
$('clock-sync').disabled = !!c.syncing; $('clock-sync').textContent = c.syncing ? 'Syncing' : 'Sync clock';
var card = $('n-clock');
card.hidden = !(bad || warn);
card.classList.toggle('warn', warn);
$('n-clock-msg').textContent = c.message || '';
$('n-clock-hint').textContent = c.hint ? 'By hand: ' + c.hint : '';
$('n-clock-result').textContent = c.sync_result || '';
$('n-clock-sync').disabled = !!c.syncing; $('n-clock-sync').textContent = c.syncing ? 'Syncing' : 'Sync clock';
$('btn-start').disabled = bad;
$('start-blocked').hidden = !bad;
$('start-blocked').textContent = bad ? c.message + ' Use "Sync clock" above.' : '';
$('btn-key-done').disabled = bad || !$('key-ack').checked;
layoutBanners();
}
// fixed banners push the content down by their height
function layoutBanners() {
var h = 0;
['clock-banner', 'update-banner', 'job-banner'].forEach(function (id) { var b = $(id); if (!b.hidden) { b.style.top = (60 + h) + 'px'; h += b.offsetHeight; } });
$('main').style.top = (60 + h) + 'px';
}
window.addEventListener('resize', layoutBanners);
$('clock-sync').addEventListener('click', function () { api('api/clock/sync', {}); });
$('n-clock-sync').addEventListener('click', function () { api('api/clock/sync', {}); });
// ---------- over-the-air updates (src/ota.rs): one banner, the settings note ----------
function cap(t) { return t ? t.charAt(0).toUpperCase() + t.slice(1) : ''; }
function updateLine(u) {
var v = 'Igneum Miner ' + u.version;
if (u.urgent && u.urgent_text) return { text: u.urgent_text + (u.status === 'downloading' ? ' Downloading.' : ''), urgent: true, prog: u.status === 'downloading' };
switch (u.status) {
case 'available': return { text: v + ' is available. Downloading it.' };
case 'downloading': return { text: 'Downloading ' + v + (u.size ? ' (' + Math.round(u.size / 1e6) + ' MB)' : '') + ': ' + Math.round((u.progress || 0) * 100) + '%', prog: true };
case 'staging': return { text: v + ' downloaded and verified. Preparing it.' };
case 'ready': return { text: v + ' is ready. ' + (u.wait ? cap(u.wait) + '.' : 'It installs at the next safe moment.'), install: true };
case 'applying': return { text: 'Installing ' + v + ': ' + (u.wait ? u.wait + '.' : 'the miners stop, then the node, then the app opens again.') };
case 'deferred': return { text: v + ' is downloaded. Windows asked for permission and nobody answered; it installs the next time someone is at this PC. Mining continues.', install: true };
case 'manual': return { text: v + ' is downloaded. ' + cap(u.wait || 'open the download and drag the app over the old one.'), open: true };
case 'error': return { text: 'Update: ' + (u.error || 'failed') + '.', install: !!(u.ready || u.downloaded) };
default: return null;
}
}
function settingsUpdateNote(u) {
var l = updateLine(u), parts = [];
if (u.updated_from) parts.push('Updated from ' + u.updated_from + '.');
if (u.rolled_back) parts.push('Rolled back: ' + u.rolled_back + '.');
if (l && !(u.rolled_back && u.status === 'error')) parts.push(l.text);
else if (u.status === 'checking') parts.push('Checking.');
else if (u.status === 'current') parts.push('This is the latest version' + (u.checked_at ? ' (checked ' + rel(state.now - u.checked_at) + ')' : '') + '.');
else if (u.error) parts.push(u.error);
if (u.activation_height && !u.urgent) parts.push('Consensus upgrade at height ' + withCommas(u.activation_height) + '.');
return parts.join(' ');
}
function renderUpdate(s) {
var u = s.update, b = $('update-banner'), l = updateLine(u);
var key = u.status + ':' + u.version;
var show = !!l && (u.urgent || u.applying || b.dataset.dismissed !== key);
if (show) {
$('update-text').textContent = l.text;
b.classList.toggle('urgent', !!l.urgent);
$('update-install').hidden = !l.install || u.applying;
$('update-open').hidden = !l.open;
$('update-later').hidden = !!l.urgent || !!u.applying;
$('update-prog').hidden = !l.prog;
$('update-prog').firstElementChild.style.width = Math.round((u.progress || 0) * 100) + '%';
}
if (b.hidden === show) { b.hidden = !show; layoutBanners(); }
}
function sampleUpdate(kind) {
var u = { available: true, version: '0.3.1', notes: 'difficulty v2, OTA updates', status: 'ready', downloaded: true, ready: true, applying: false, progress: 1, size: 20588331, auto: true, wait: 'installs at the next safe moment', urgent: false, urgent_text: '', activation_height: 0, error: '', updated_from: '', rolled_back: '' };
if (kind === 'available') { u.status = 'available'; u.downloaded = false; u.ready = false; u.progress = 0; }
if (kind === 'downloading') { u.status = 'downloading'; u.downloaded = false; u.ready = false; u.progress = 0.43; }
if (kind === 'waiting') { u.wait = 'hourly program boundary in 97 s; installing after it'; }
if (kind === 'applying') { u.status = 'applying'; u.applying = true; }
if (kind === 'deferred') { u.status = 'deferred'; u.wait = 'waits for the next time someone is at this PC (Windows asks for permission); mining continues'; }
if (kind === 'urgent') { u.urgent = true; u.activation_height = 120000; u.urgent_text = 'Consensus upgrade at height 120000 (difficulty v2): the node is 1,240 blocks away. Installing 0.3.1 now.'; }
if (kind === 'manual') { u.status = 'manual'; u.ready = false; u.wait = '/Applications is not writable; open the downloaded disk image and drag the app over the old one'; }
if (kind === 'error') { u.status = 'error'; u.error = 'sha256 mismatch: the file is not what the manifest signed'; u.downloaded = false; u.ready = false; }
if (kind === 'updated') { u.status = 'current'; u.available = false; u.ready = false; u.downloaded = false; u.updated_from = '0.3.0'; u.version = '0.3.1'; }
return u;
}
// ---------- remote jobs (src/jobrun.rs): one strip while a job runs and after it, the settings block ----------
function jobKey(j) { if (!j) return ''; return j.active ? ('run:' + j.id) : (j.last && j.last.id ? ('done:' + j.last.id + ':' + j.last.status) : ''); }
function jobTitle(j) { return j.title && j.title !== j.kind ? j.title : (j.kind === 'shard-benchmark' ? 'shard benchmark' : j.kind === 'build' ? 'build (node and app, mining continues)' : j.kind); }
function jobLine(j, now) {
if (j.active) {
var m = Math.max(0, Math.floor((now - j.started_at) / 60));
return { text: 'Job: ' + jobTitle(j) + ' running, ' + m + ' min' + (j.stage ? '. ' + cap(j.stage) + '.' : '.') + (j.message ? ' ' + j.message : '') + (j.account ? ' ' + j.account : ''), results: j.results || [] };
}
var l = j.last;
if (!l || !l.id) return null;
// the last outcome stays up for 30 minutes
if (l.finished_at && now - l.finished_at > 1800) return null;
var d = Math.max(0, Math.round((l.finished_at - l.started_at) / 60));
return { text: 'Job: ' + (l.title || l.kind) + ' ' + l.status + ' after ' + d + ' min, exit ' + l.exit + (l.uploaded ? ', report uploaded' : ', report not uploaded') + (l.summary ? '. ' + l.summary : ''), results: j.last_results || [], failed: l.status !== 'done' };
}
function renderJobs(s) {
var j = s.jobs || {}, b = $('job-banner'), l = jobLine(j, s.now || 0);
var key = jobKey(j);
var show = !!l && b.dataset.dismissed !== key && phase === 'dashboard';
if (show) {
$('job-text').textContent = l.text;
b.classList.toggle('failed', !!l.failed);
var r = (l.results || []).filter(function (x) { return x.indexOf('RESULT') === 0; }).slice(-6);
$('job-results').textContent = r.join('\n');
$('job-results').hidden = !r.length;
}
if (b.hidden === show) { b.hidden = !show; layoutBanners(); }
else if (show) layoutBanners();
}
function fillJobsSettings(j) {
$('s-jobs-allow').checked = !!j.allowed;
$('s-prove').checked = !!(state.settings && state.settings.prove);
$('s-jobs-key').textContent = j.key_fingerprint ? 'signing key sha256:' + j.key_fingerprint : '';
var parts = [];
if (j.account) parts.push(j.account);
if (!j.allowed) parts.push('Off: nothing runs here until it is switched on.');
else if (!j.url_set) parts.push('No jobs address in this build.');
else if (j.error) parts.push(j.error + '.');
else if (j.active) parts.push('Running ' + jobTitle(j) + ' (' + j.id + ').');
else parts.push('Nothing running' + (j.checked_at ? '; checked ' + rel((state.now || 0) - j.checked_at) : '') + (j.queued ? '; ' + j.queued + ' queued' : '') + '.');
$('s-jobs-note').textContent = parts.join(' ');
var h = j.history || [];
$('s-jobs-history').innerHTML = h.length ? '<table><thead><tr><th>job</th><th>kind</th><th>started</th><th>exit</th><th>report</th></tr></thead><tbody>' + h.map(function (r) {
return '<tr class="' + esc(r.status) + '"><td class="mono" title="' + esc(r.summary || '') + '">' + esc(r.id) + '</td><td>' + esc(r.kind) + '</td><td>' + (r.started_at ? clock(r.started_at) : '') + '</td><td>' + esc(r.status) + (r.status !== 'running' ? ' (' + r.exit + ')' : '') + '</td><td>' + (r.uploaded ? 'uploaded' : (r.status === 'running' ? 'pending' : 'not uploaded')) + '</td></tr>';
}).join('') + '</tbody></table>' : '<p class="note">No job has run on this machine yet.</p>';
}
function render(s) {
state = s; stateAt = performance.now();
if (forcedUpdate) { s.update = sampleUpdate(forcedUpdate); if (forcedUpdate === 'updated') s.version = '0.3.1'; }
renderPill(s);
renderClock(s);
renderUpdate(s);
renderJobs(s);
if (phase === 'cards') renderCards(s);
if (phase === 'dashboard') renderDashboard(s);
if (s.quitting && !$('btn-quit').disabled) { $('btn-quit').disabled = true; }
}
var failures = 0, first = true;
function poll() {
api('api/state').then(function (s) {
failures = 0;
if (first) {
first = false;
var start = forced || (s.setup_done ? 'dashboard' : 'welcome');
if (start === 'settings') { show('dashboard'); setTimeout(function () { fillSettings(); $('settings').hidden = false; }, 300); }
else if (start === 'key') { show('address'); showKeySheet({ address: s.address.value || '0x0000000000000000000000000000000000000000', display: s.address.display || '0x0000000000000000000000000000000000000000', private_key: '0x' + '0'.repeat(64), wallet_file: s.address.wallet_file || (s.node_dir.replace(/[^\/\\]+$/, '') + 'app/wallet.json') }); }
else show(start);
if (start === 'cards') renderCards(s);
}
render(s);
}).catch(function () {
failures++;
if (failures >= 4) { $('pill').classList.add('warn'); $('pill-text').textContent = state && state.quitting ? 'stopped' : 'engine away'; }
});
}
poll(); setInterval(poll, 1000);
size();
})();