Merge miner-ui-3 921819a for 0.3.15: strips, in-place asks and the clock card on the brand tokens; plain-sentence strip and Activity lines; one header baseline (the Mine and Earnings screens as shipped in 0.3.14)
Every file under app/igneum-app/ui/ is that branch's side, per the merge rule. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
|
@ -61,7 +61,7 @@ input,textarea{font-variant-numeric:tabular-nums}
|
|||
.btn.tiny{min-height:26px;padding:2px 10px;font-size:var(--t-sm);border-radius:7px;font-family:var(--mono);font-weight:500}
|
||||
.btn.ghost{border-color:transparent;color:var(--ink-2)}
|
||||
.btn.ghost:hover{border-color:var(--line-2);color:var(--bone)}
|
||||
.btn.ghost.on{color:var(--molten);border-color:var(--molten-40);background:var(--molten-10)}
|
||||
.btn.ghost.on{color:var(--molten);border-color:var(--molten-40)}
|
||||
.btn.danger:hover{color:var(--ember);border-color:var(--ember)}
|
||||
:focus-visible{outline:2px solid var(--ember);outline-offset:3px;border-radius:6px}
|
||||
@media (prefers-reduced-motion:reduce){.btn{transition:none}}
|
||||
|
|
@ -77,7 +77,7 @@ body.mac .rail-brand{padding-top:30px}
|
|||
.nav svg{width:19px;height:19px;flex:0 0 19px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round;color:var(--ash);transition:color .15s ease}
|
||||
.nav:hover{background:var(--hover);color:var(--bone)}
|
||||
.nav:hover svg{color:var(--ink-2)}
|
||||
.nav.on{background:var(--ember-12);border-color:var(--ember-40);color:var(--bone);font-weight:600}
|
||||
.nav.on{background:var(--hover);border-color:var(--line);color:var(--bone);font-weight:600}
|
||||
.nav.on svg{color:var(--ember)}
|
||||
.nav.on::before{content:"";position:absolute;left:-13px;top:10px;bottom:10px;width:3px;border-radius:0 3px 3px 0;background:var(--ember)}
|
||||
.nav.small{min-height:36px;font-size:var(--t-base);color:var(--ash)}
|
||||
|
|
@ -99,13 +99,15 @@ body.has-rail .top{left:var(--rail)}
|
|||
.brand{display:flex;align-items:center;gap:10px;min-width:0}
|
||||
.brand .word{font-family:var(--head);font-weight:900;font-size:20px;letter-spacing:.06em}
|
||||
.brand .miner{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.22em;color:var(--ash);margin-left:var(--s-1);padding-top:3px}
|
||||
body.has-rail .top{align-items:baseline;padding-top:19px}
|
||||
.page-title{display:flex;align-items:baseline;gap:12px;min-width:0}
|
||||
.page-title h1{font-size:19px;font-weight:700;letter-spacing:0;white-space:nowrap}
|
||||
.page-sub{font-size:var(--t-base);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
||||
.top-right{display:flex;align-items:center;gap:var(--s-3);flex:0 0 auto;min-width:0}
|
||||
.pill{display:inline-flex;align-items:center;gap:var(--s-2);font-family:var(--mono);font-size:var(--t-sm);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line);border-radius:999px;padding:6px 12px 6px 10px;background:var(--graphite);white-space:nowrap;font-variant-numeric:tabular-nums;min-width:112px;justify-content:center}
|
||||
.pill{display:inline-flex;align-items:baseline;gap:var(--s-2);font-family:var(--sans);font-size:var(--t-base);font-weight:600;color:var(--ash);border:1px solid var(--line);border-radius:999px;padding:5px 12px 5px 10px;background:var(--graphite);white-space:nowrap;font-variant-numeric:tabular-nums;justify-content:center;line-height:1.3}
|
||||
.pill .dot{position:relative;top:-1px}
|
||||
.pill.on{color:var(--molten);border-color:var(--molten-40)}
|
||||
.pill.warn{color:var(--ember)}
|
||||
.pill.warn{color:var(--ember);border-color:var(--ember-40)}
|
||||
.dot{width:8px;height:8px;border-radius:50%;background:var(--ash);display:inline-block;flex:0 0 8px}
|
||||
.on .dot,.dot.live{background:var(--molten);animation:pulse 2s ease-in-out infinite}
|
||||
.warn .dot,.dot.bad{background:var(--ember);animation:none}
|
||||
|
|
@ -116,26 +118,30 @@ body.has-rail .top{left:var(--rail)}
|
|||
top moves once per change with a 150 ms transition (layoutStrip), never per poll. */
|
||||
.notices{position:fixed;top:var(--top);left:0;right:0;z-index:19}
|
||||
body.has-rail .notices{left:var(--rail)}
|
||||
.notice{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-2) var(--s-4);padding:8px calc(var(--gutter) - 6px) 8px var(--gutter);background:var(--molten-10);border-bottom:1px solid var(--molten-40);font-size:var(--t-base);line-height:1.4;color:var(--bone)}
|
||||
.notice.bad{background:var(--ember-12);border-bottom-color:var(--ember-40)}
|
||||
.notice.warn{background:var(--molten-10);border-bottom-color:var(--molten-40)}
|
||||
.notice.update-urgent{background:rgba(242,84,27,.55);border-bottom-color:var(--ember);color:#fff;font-weight:600}
|
||||
.notice{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-2) var(--s-3);padding:9px calc(var(--gutter) - 6px) 9px var(--gutter);background:var(--row);border-bottom:1px solid var(--line);box-shadow:inset 0 1px 0 var(--ember);font-size:var(--t-base);line-height:1.4;color:var(--ash)}
|
||||
.notice-dot{width:7px;height:7px;border-radius:50%;background:var(--ember);flex:0 0 7px;display:inline-block}
|
||||
.notice.bad .notice-text,.notice.update-urgent .notice-text{color:var(--bone)}
|
||||
.notice.update-urgent .notice-text{font-weight:600}
|
||||
.notice .notice-text b{color:var(--bone);font-weight:600}
|
||||
.notice-text{flex:1 1 320px;min-width:0}
|
||||
.notice-actions{display:flex;align-items:center;gap:var(--s-2);flex:0 0 auto}
|
||||
.notice-actions:empty{display:none}
|
||||
.notice-close{flex:0 0 auto;width:30px;height:30px;border-radius:8px;border:1px solid transparent;background:transparent;color:var(--ash);font-size:20px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0}
|
||||
.notice-close:hover{color:var(--bone);border-color:var(--line-2)}
|
||||
.notice.update-urgent .notice-close{color:#fff}
|
||||
.notice-detail{flex-basis:100%;font-size:var(--t-sm);color:var(--ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-3px}
|
||||
.notice .prog{flex-basis:100%;height:3px;background:rgba(127,127,127,.2);border-radius:2px;overflow:hidden;margin-top:-3px}
|
||||
.notice-more{flex:0 0 auto;width:30px;height:30px;border-radius:8px;border:1px solid transparent;background:transparent;color:var(--ash);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0}
|
||||
.notice-more:hover{color:var(--bone);border-color:var(--line-2)}
|
||||
.notice-more .chev{width:8px;height:8px;transform:rotate(45deg) translateY(-2px)}
|
||||
.notice-more.open .chev{transform:rotate(225deg) translateY(-2px)}
|
||||
.notice-detail{flex-basis:100%;font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2);white-space:normal;word-break:break-word;margin-top:-2px;user-select:text;-webkit-user-select:text}
|
||||
.notice .prog{flex-basis:100%;height:2px;background:var(--line);border-radius:2px;overflow:hidden;margin-top:-3px}
|
||||
.notice .prog i{display:block;height:100%;width:0;background:var(--ember);transition:width .5s linear}
|
||||
|
||||
/* a question in place of a dialog: the quit strip over the page, the inline asks inside a card */
|
||||
.ask-wrap{position:fixed;left:0;right:0;bottom:0;z-index:36;display:flex;justify-content:center;padding:0 var(--s-4) var(--s-4);pointer-events:none}
|
||||
body.has-rail .ask-wrap{left:var(--rail)}
|
||||
.ask{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;background:var(--graphite);border:1px solid var(--ember-40);border-radius:14px;padding:12px 16px;box-shadow:0 14px 40px var(--shadow);pointer-events:auto;max-width:720px;animation:rise .2s ease}
|
||||
.ask{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;background:var(--row);border:1px solid var(--line);border-radius:14px;padding:12px 16px;box-shadow:inset 0 1px 0 var(--ember),0 14px 40px var(--shadow);pointer-events:auto;max-width:720px;animation:rise .2s ease}
|
||||
.ask-text{flex:1 1 260px;font-size:var(--t-md);min-width:0}
|
||||
.ask.inline{box-shadow:none;background:var(--ember-12);margin-top:var(--s-3);animation:none}
|
||||
.ask.inline{box-shadow:inset 0 1px 0 var(--ember);background:var(--row);margin-top:var(--s-3);animation:none}
|
||||
|
||||
/* screens and pages */
|
||||
main{position:absolute;top:var(--top);bottom:var(--bottom);left:0;right:0;overflow:auto;padding:0 var(--gutter);overscroll-behavior:contain;transition:top .15s ease}
|
||||
|
|
@ -214,7 +220,7 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.toggle-big .ts{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.06em;opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
|
||||
.toggle-big.stop{background:var(--graphite);border-color:var(--line-2);color:var(--bone)}
|
||||
.toggle-big.stop:hover{border-color:var(--ash);background:var(--row)}
|
||||
.toggle-big.stop .ring{background:var(--ember-12);color:var(--ember)}
|
||||
.toggle-big.stop .ring{background:var(--obsidian);border:1px solid var(--line-2);color:var(--ember)}
|
||||
.toggle-big.stop .ts{color:var(--molten);opacity:1}
|
||||
.totals{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--s-3);background:var(--graphite);border:1px solid var(--line);border-radius:14px;padding:18px 20px;min-width:0}
|
||||
.tot{display:flex;flex-direction:column;min-width:0;padding-left:var(--s-4);border-left:1px solid var(--line)}
|
||||
|
|
@ -246,9 +252,10 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.feed>div:last-child{border-bottom:0}
|
||||
.feed>div>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.feed .t{color:var(--ash);flex:0 0 auto;font-size:var(--t-xs)}
|
||||
.feed .k{color:var(--molten);margin-right:6px}
|
||||
.feed .k.error,.feed .k.warn,.feed .k.block{color:var(--ember)}
|
||||
.feed .k.build,.feed .k.info{color:var(--ash)}
|
||||
.feed .k{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--molten);margin-right:10px;position:relative;top:-1px}
|
||||
.feed .k.error,.feed .k.warn,.feed .k.block{background:var(--ember)}
|
||||
.feed .k.build,.feed .k.info{background:var(--line-2)}
|
||||
.feed .k.proving{background:var(--nvidia)}
|
||||
|
||||
/* the card rows on Mine (the hero object): badge, name and state, the numbers, Tune, the switch, the chevron;
|
||||
the tune line under; the details under that */
|
||||
|
|
@ -349,8 +356,8 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)}
|
|||
#s-jobs-history td{padding:6px 6px 6px 0;border-top:1px solid var(--line);vertical-align:top}
|
||||
#s-jobs-history tr.failed td,#s-jobs-history tr.timeout td,#s-jobs-history tr.aborted td{color:var(--ember)}
|
||||
#s-jobs-history tr.running td{color:var(--molten)}
|
||||
.clock-card{border:1px solid var(--ember-40);background:var(--ember-12);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:var(--s-2)}
|
||||
.clock-card.warn{border-color:var(--molten-40);background:var(--molten-10)}
|
||||
.clock-card{border:1px solid var(--line);background:var(--row);box-shadow:inset 0 1px 0 var(--ember);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:var(--s-2)}
|
||||
.clock-card.warn{box-shadow:inset 0 1px 0 var(--molten)}
|
||||
.clock-msg{font-size:var(--t-md);color:var(--bone);line-height:1.45}
|
||||
.clock-card .note{margin-top:0}
|
||||
|
||||
|
|
@ -372,7 +379,7 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)}
|
|||
.options{display:flex;flex-direction:column;gap:var(--s-3);margin-top:6px}
|
||||
.option{display:flex;gap:var(--s-4);align-items:flex-start;background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:20px 22px;cursor:pointer;transition:border-color .15s ease,background .15s ease}
|
||||
.option:hover{border-color:var(--line-2)}
|
||||
.option.on{border-color:var(--ember);background:var(--ember-12)}
|
||||
.option.on{border-color:var(--ember);background:var(--row)}
|
||||
.option input[type=radio]{position:absolute;opacity:0;width:0;height:0}
|
||||
.option .radio{width:20px;height:20px;border-radius:50%;border:2px solid var(--line-2);flex:0 0 20px;margin-top:2px;position:relative}
|
||||
.option.on .radio{border-color:var(--ember)}
|
||||
|
|
@ -506,8 +513,8 @@ body.drawer-drag main{pointer-events:none}
|
|||
.log .src.app{color:var(--ember)}
|
||||
.log .src.watch{color:var(--ash)}
|
||||
.log .e,.log .e .src{color:var(--ember)}
|
||||
.log .ln.hit{background:rgba(255,179,92,.18);box-shadow:inset 3px 0 0 var(--molten);margin:0 calc(-1 * var(--gutter));padding:0 var(--gutter)}
|
||||
.log mark{background:rgba(255,179,92,.3);color:var(--bone);border-radius:2px;padding:0 1px}
|
||||
.log .ln.hit{background:var(--hover);box-shadow:inset 3px 0 0 var(--molten);margin:0 calc(-1 * var(--gutter));padding:0 var(--gutter)}
|
||||
.log mark{background:transparent;color:var(--bone);text-decoration:underline;text-decoration-color:var(--ember);text-underline-offset:2px}
|
||||
.log-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--ash);font-size:var(--t-sm);padding:0 var(--gutter);text-align:center}
|
||||
.log-jump{position:absolute;right:calc(var(--gutter) + 14px);bottom:14px;background:var(--graphite);border-color:var(--molten-40);color:var(--molten);box-shadow:0 8px 24px var(--shadow);z-index:2;gap:6px;padding:6px 12px}
|
||||
.log-jump:hover{border-color:var(--molten)}
|
||||
|
|
@ -590,6 +597,8 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)}
|
|||
.step{padding:32px 0}
|
||||
.money-row .v{font-size:var(--t-num)}
|
||||
.drawer-head{padding-left:var(--s-4);padding-right:var(--s-4)}
|
||||
.notice{padding-left:var(--s-4);padding-right:var(--s-2);gap:var(--s-2)}
|
||||
.notice-text{flex-basis:160px}
|
||||
.log,.log-ruler .t0,.log-ruler .t1{padding-left:var(--s-4);padding-right:var(--s-4)}
|
||||
}
|
||||
/* short windows (the 600 px floor): tighter paddings, a smaller canvas, so the drawer always has room */
|
||||
|
|
|
|||
|
|
@ -49,12 +49,27 @@ var Notices = (function () {
|
|||
}
|
||||
if (u.updated_from && s.uptime_s >= 0 && s.uptime_s < INSTALLED_S) {
|
||||
var now = s.version || ver;
|
||||
return notice('update-installed', 'update:installed:' + now, 'Updated to Igneum Miner ' + now + ' from ' + u.updated_from + '.');
|
||||
var at = s.now > 0 && s.uptime_s >= 0 ? ' at ' + hhmm(s.now - s.uptime_s) : '';
|
||||
return notice('update-installed', 'update:installed:' + now, 'Updated to ' + now + at + '.', { title: 'Igneum Miner ' + now + ', from ' + u.updated_from });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function jobTitle(j) { return j.title && j.title !== j.kind ? j.title : (j.kind === 'shard-benchmark' ? 'shard benchmark' : j.kind === 'build' ? 'build (mining continues)' : j.kind); }
|
||||
// the job in a user's words (miner-ui-3 polish, 6 October 2026): from its kind, else from its id's prefix
|
||||
function jobWord(kind, id) {
|
||||
var k = String(kind || ''), i = String(id || '');
|
||||
var t = k || i;
|
||||
if (/^update-now/.test(t) || /^update-now/.test(i)) return 'update check';
|
||||
if (/^restart/.test(t) || /^restart/.test(i)) return 'restart';
|
||||
if (/^collect/.test(t) || /^collect/.test(i)) return 'log collection';
|
||||
if (/shard-benchmark|^shard/.test(t) || /shard/.test(i)) return 'shard benchmark';
|
||||
if (/^build/.test(t) || /^build/.test(i)) return 'build';
|
||||
if (/ember|tune|sweep/.test(t) || /ember|tune|sweep/.test(i)) return 'tuning check';
|
||||
if (/script|playbook|run/.test(t)) return 'a script';
|
||||
return k || 'a task';
|
||||
}
|
||||
function hhmm(t) { var d = new Date(t * 1000); return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'); }
|
||||
function firstErrorLine(results, summary) {
|
||||
for (var i = 0; i < results.length; i++) if (/BUILD FAILED|error|failed|panic/i.test(results[i])) return firstLine(results[i]);
|
||||
return firstLine(summary);
|
||||
|
|
@ -65,18 +80,20 @@ var Notices = (function () {
|
|||
if (j.active) {
|
||||
var m = Math.max(0, Math.floor((now - j.started_at) / 60));
|
||||
var results = (j.results || []).filter(function (x) { return x.indexOf('RESULT') === 0; });
|
||||
return notice('job-running', 'job:run:' + j.id, 'Job: ' + jobTitle(j) + ' running, ' + m + ' min.' + (j.stage ? ' ' + endDot(cap(j.stage)) : ''), { detail: results.length ? results[results.length - 1] : '', title: j.message || '' });
|
||||
var tech = [jobTitle(j), results.length ? results[results.length - 1] : ''].filter(Boolean).join(' · ');
|
||||
return notice('job-running', 'job:run:' + j.id, 'A job from the team is running: ' + jobWord(j.kind, j.id) + ', ' + m + ' min.' + (j.stage ? ' ' + endDot(cap(j.stage)) : ''), { detail: tech, title: j.message || '' });
|
||||
}
|
||||
var l = j.last;
|
||||
if (!l || !l.id) return null;
|
||||
var mins = Math.max(0, Math.round((l.finished_at - l.started_at) / 60)), t = l.title || l.kind;
|
||||
var report = l.uploaded ? 'Report uploaded.' : 'Report not uploaded.';
|
||||
var report = l.uploaded ? '' : ' The report did not upload.', word = jobWord(l.kind, l.id), tech = t + (l.summary ? ': ' + l.summary : '');
|
||||
if (l.status === 'done') {
|
||||
if (l.finished_at && now - l.finished_at > JOB_DONE_S) return null;
|
||||
return notice('job-done', 'job:done:' + l.id, 'Job: ' + t + ' done after ' + mins + ' min. ' + report, { title: l.summary || '' });
|
||||
return notice('job-done', 'job:done:' + l.id, 'A job from the team ran: ' + word + ', ' + mins + ' min.' + report, { detail: tech, title: l.summary || '' });
|
||||
}
|
||||
var word = l.status === 'timeout' ? 'hit its time cap' : l.status === 'aborted' ? 'was stopped' : 'failed';
|
||||
return notice('job-failed', 'job:failed:' + l.id, 'Job: ' + t + ' ' + word + ' after ' + mins + ' min, exit ' + l.exit + '. ' + report, { tone: 'bad', detail: firstErrorLine(j.last_results || [], l.summary), title: l.summary || '' });
|
||||
var verb = l.status === 'timeout' ? 'ran out of time' : l.status === 'aborted' ? 'was stopped' : 'failed';
|
||||
var cause = firstErrorLine(j.last_results || [], l.summary);
|
||||
return notice('job-failed', 'job:failed:' + l.id, 'A job from the team ' + verb + ': ' + word + ', after ' + mins + ' min.' + report, { tone: 'bad', detail: [tech, cause !== l.summary ? cause : ''].filter(Boolean).join(' · ') + ' · exit ' + l.exit, title: l.summary || '' });
|
||||
}
|
||||
|
||||
// c = state.clock: the engine's words, Sync clock, the manual hint underneath
|
||||
|
|
@ -136,7 +153,7 @@ var Notices = (function () {
|
|||
}
|
||||
return best;
|
||||
}
|
||||
return { tuneNotice: tuneNotice, LEVEL: LEVEL, JOB_DONE_S: JOB_DONE_S, INSTALLED_S: INSTALLED_S, CARD_S: CARD_S, updateNotice: updateNotice, jobNotice: jobNotice, clockNotice: clockNotice, cardNotices: cardNotices, gather: gather, pick: pick, jobTitle: jobTitle, cap: cap, endDot: endDot, kindWord: kindWord };
|
||||
return { tuneNotice: tuneNotice, jobWord: jobWord, hhmm: hhmm, LEVEL: LEVEL, JOB_DONE_S: JOB_DONE_S, INSTALLED_S: INSTALLED_S, CARD_S: CARD_S, updateNotice: updateNotice, jobNotice: jobNotice, clockNotice: clockNotice, cardNotices: cardNotices, gather: gather, pick: pick, jobTitle: jobTitle, cap: cap, endDot: endDot, kindWord: kindWord };
|
||||
})();
|
||||
|
||||
/* ---------- the update card (pure; update-card.test.mjs loads this block) ----------
|
||||
|
|
@ -539,10 +556,111 @@ var View = (function () {
|
|||
if (text.length > max) { var cut = text.lastIndexOf(' ', max - 2); text = text.slice(0, cut > 40 ? cut : max - 2) + '…'; }
|
||||
return text;
|
||||
}
|
||||
// the engine's event line in a user's voice (src/engine.rs event strings, 6 October 2026): {text, tech}; tech is
|
||||
// the engine's own line when it differs, for the tooltip. Unknown lines keep their words with a JSON tail cut.
|
||||
var EVENT_MAP = [
|
||||
[/^updated to Igneum Miner (\S+) from (\S+)/, function (m) { return 'Updated to ' + m[1]; }],
|
||||
[/^Igneum Miner (\S+) is available: downloading/, function (m) { return 'Downloading ' + m[1]; }],
|
||||
[/^Igneum Miner (\S+) is ready/, function (m) { return m[1] + ' is ready to install'; }],
|
||||
[/^installing Igneum Miner (\S+)/, function (m) { return 'Installing ' + m[1]; }],
|
||||
[/^update to (\S+) failed/, function (m) { return 'The update to ' + m[1] + ' failed'; }],
|
||||
[/^the update to (\S+) did not take/, function (m) { return 'The update to ' + m[1] + ' did not take'; }],
|
||||
[/^Igneum Miner (\S+) did not stay up twice; restoring (\S+)/, function (m) { return m[1] + ' did not stay up; going back to ' + m[2]; }],
|
||||
[/^update downloaded/, function () { return 'Update downloaded'; }],
|
||||
[/^OTA: administrator approval not given/, function () { return 'The update waits for the next time someone is at this PC'; }],
|
||||
[/^job (\S+) \((.+)\) starts: /, function (m) { return 'A job from the team started: ' + Notices.jobWord(m[2], m[1]); }],
|
||||
[/^job (\S+) \((.+)\) (done|failed|timeout|aborted) after (\d+) min/, function (m) { return 'A job from the team ' + (m[3] === 'done' ? 'ran' : m[3] === 'timeout' ? 'ran out of time' : m[3] === 'aborted' ? 'was stopped' : 'failed') + ': ' + Notices.jobWord('', m[1]) + ', ' + m[4] + ' min'; }],
|
||||
[/^(\d+) remote jobs? received from Igneum/, function (m) { return m[1] + ' job' + (m[1] === '1' ? '' : 's') + ' from the team received'; }],
|
||||
[/^remote job: update check now/, function () { return 'The team asked for an update check'; }],
|
||||
[/^remote job: the app restarts/, function () { return 'The team asked the app to restart'; }],
|
||||
[/^remote job asked for a node restart, but the node is external/, function () { return 'The team asked for a node restart; this node is not the app’s, so nothing was done'; }],
|
||||
[/^remote jobs allowed/, function () { return 'Jobs from the team can run here'; }],
|
||||
[/^remote jobs switched off/, function () { return 'Jobs from the team are off'; }],
|
||||
[/^job finished; the miners restart/, function () { return 'The job finished; mining restarts'; }],
|
||||
[/^node synced: (\d+) blocks, (\d+) peer/, function (m) { return 'Node synced: ' + withCommas(+m[1]) + ' blocks, ' + m[2] + ' peer' + (m[2] === '1' ? '' : 's'); }],
|
||||
[/^a node already answers on/, function () { return 'Using the node already running on this machine'; }],
|
||||
[/^consensus parameters from the signed manifest/, function () { return 'Rule settings read from the team'; }],
|
||||
[/^this node build does not understand the consensus parameters/, function () { return 'This node is too old for the team’s rule settings; an app update carries the newer node'; }],
|
||||
[/^restarting the node with the new consensus parameters/, function () { return 'Restarting the node with the new rule settings'; }],
|
||||
[/^kernel tuning from the signed manifest/, function () { return 'Tuning starting points read from the team'; }],
|
||||
[/^block accepted by the node \((.+)\)/, function (m) { return 'Block found by ' + m[1]; }],
|
||||
[/^igneumd exited with code (\d+) after (\d+) s; restarting/, function (m) { return 'The node stopped (code ' + m[1] + '); restarting'; }],
|
||||
[/^igneumd exited at once \(code (\d+)\)/, function (m) { return 'The node stopped at once (code ' + m[1] + ')'; }],
|
||||
[/^igneumd could not start/, function () { return 'The node could not start'; }],
|
||||
[/^miner (\S+) exited with code (\d+); restarting in (\d+) s/, function (m) { return 'A worker stopped (code ' + m[2] + '); restarting in ' + m[3] + ' s'; }],
|
||||
[/^no reading from the node for (\d+) s/, function (m) { return 'No reading from the node for ' + m[1] + ' s; restarting it'; }],
|
||||
[/^no peer after 90 s/, function () { return 'No other node found after 90 s; still looking'; }],
|
||||
[/^no payout address/, function () { return 'No rewards address yet: finish the setup'; }],
|
||||
[/^mining paused; the node keeps running/, function () { return 'Mining paused; the node keeps running'; }],
|
||||
[/^mining resumed/, function () { return 'Mining resumed'; }],
|
||||
[/^GPU power cap in force: (.+)/, function (m) { return 'Power cap in force: ' + m[1]; }],
|
||||
[/^GPU power cap NOT applied/, function () { return 'Power cap not applied: the administrator prompt was refused (Retry on the card)'; }],
|
||||
[/^power control on:/, function () { return 'Power control on'; }],
|
||||
[/^rewards go to (\S+)/, function (m) { return 'Rewards go to ' + shortHex(m[1], 6, 4); }],
|
||||
[/^settings changed \((.+)\); the miners restart/, function (m) { return 'Settings changed (' + m[1] + '); mining restarts'; }],
|
||||
[/^(.+): tune queued; it starts once/, function (m) { return m[1] + ': tune queued'; }],
|
||||
[/^(.+): tuning started: /, function (m) { return m[1] + ': tuning started'; }],
|
||||
[/^(.+): best point (.+?) \((.+?)\); your setting stays pinned/, function (m) { return m[1] + ': best point ' + m[2] + ' (' + m[3] + '); your cap stays pinned'; }],
|
||||
[/^(.+): measured as it runs: (.+)/, function (m) { return m[1] + ': measured ' + m[2]; }],
|
||||
[/^(.+): no tune: (.+)/, function (m) { return m[1] + ': no tune (' + m[2] + ')'; }],
|
||||
[/^(.+): worker fault: (.+); the miner restarts the worker/, function (m) { return m[1] + ': worker fault; restarting it'; }],
|
||||
[/^(.+): the worker could not compile the next program/, function (m) { return m[1] + ': the next program compiles at the hour boundary instead'; }],
|
||||
[/^(.+) worker ready/, function (m) { return m[1] + ': worker ready'; }],
|
||||
[/^(.+) kernel race: (.+?) at ([\d.]+) MH\/s/, function (m) { return m[1] + ': program variant chosen, ' + m[3] + ' MH/s'; }],
|
||||
[/^(.+): switched on, (\d+) identit/, function (m) { return m[1] + ': on'; }],
|
||||
[/^(.+): switched off/, function (m) { return m[1] + ': off'; }],
|
||||
[/^(.+): (\d+) identit(y|ies); its worker restarts/, function (m) { return m[1] + ': ' + m[2] + ' identit' + m[3] + '; its worker restarts'; }],
|
||||
[/^next hourly program compiled and ready/, function () { return 'Next hourly program ready'; }],
|
||||
[/^the hourly program changed; the worker restarts/, function () { return 'New hourly program; the worker restarts on it'; }],
|
||||
[/^hourly program changed: /, function () { return 'New hourly program'; }],
|
||||
[/^program pack out of date, rebuilding/, function () { return 'Rebuilding the program pack'; }],
|
||||
[/^GPU worker built from source/, function () { return 'Worker built'; }],
|
||||
[/^block (\d+) shard (\d+) paid (.+) IGN/, function (m) { return 'Proof paid: ' + m[3] + ' IGN'; }],
|
||||
[/^block (\d+) shard (\d+) proven and submitted in (\d+) s/, function (m) { return 'Proof sent in ' + m[3] + ' s'; }],
|
||||
[/^segment .+ paid (.+) IGN to the aggregator/, function (m) { return 'Segment paid: ' + m[1] + ' IGN'; }],
|
||||
[/^segment (\S+)\.\.(\S+) aggregated and submitted in (\d+) s/, function (m) { return 'Segment proven in ' + m[3] + ' s'; }],
|
||||
[/^segment (\S+)\.\.(\S+): (\d+) shards proven, aggregated and submitted in (\d+) s/, function (m) { return 'Segment proven: ' + m[3] + ' pieces in ' + m[4] + ' s'; }],
|
||||
[/^segment (\S+)\.\.(\S+): (\d+) shards proven and submitted in (\d+) s; the segment record is held/, function (m) { return 'Segment proven: ' + m[3] + ' pieces in ' + m[4] + ' s; its record waits for the chain rule'; }],
|
||||
[/^segment (\S+)\.\.(\S+) record accepted on retry/, function () { return 'Segment record accepted'; }],
|
||||
[/^WSL2 prover setup started/, function () { return 'Prover setup started in its own window'; }],
|
||||
[/^Card removed: (.+); its worker stopped/, function (m) { return 'Card removed: ' + m[1]; }],
|
||||
[/^(.+): not usable \((.+?)\); its worker stopped/, function (m) { return m[1] + ': not usable (' + m[2] + ')'; }],
|
||||
[/^clock agrees with the network again/, function () { return 'Clock agrees with the network again'; }],
|
||||
[/^asking the system to set its clock/, function () { return 'Setting the clock from a time server'; }],
|
||||
[/^clock: (.+)/, function (m) { return 'Clock: ' + m[1]; }],
|
||||
[/^devnet only: the node trusts proof records/, function () { return 'Devnet trust mode: proof records are not verified (Settings)'; }],
|
||||
[/^a sweep is setting the caps right now/, function () { return 'A tune is setting the caps now; retry when it is done'; }],
|
||||
[/^updates are off for this engine/, function () { return 'Updates are off for this engine (a measurement run)'; }],
|
||||
[/^(.+); the node restarts$/, function (m) { return cap(m[1]) + '; the node restarts'; }],
|
||||
[/^(.+); the node is external, so the app cannot restart it/, function (m) { return cap(m[1]) + '; the node is not the app’s, so it is not restarted'; }]
|
||||
];
|
||||
function plainEvent(text) {
|
||||
text = String(text || '');
|
||||
for (var i = 0; i < EVENT_MAP.length; i++) { var m = EVENT_MAP[i][0].exec(text); if (m) { var out = EVENT_MAP[i][1](m); return { text: out, tech: out === text ? '' : text }; } }
|
||||
var cut = eventLine(text);
|
||||
return { text: cap(cut), tech: cut === text ? '' : text };
|
||||
}
|
||||
// the pill in the top bar: a verb a user understands, and its tone (on = molten, bad = ember, '' = grey)
|
||||
function pill(s) {
|
||||
if (s.quitting) return { text: 'Stopping', tone: '' };
|
||||
if (!s.setup_done) return { text: s.detecting ? 'Detecting cards' : 'Setting up', tone: '' };
|
||||
var m = s.mining || {}, n = s.node || {};
|
||||
if (tuningCard(m.cards)) return { text: 'Tuning', tone: 'on' };
|
||||
if (m.state === 'mining') return { text: 'Mining · ' + (m.hash_total >= 100 ? m.hash_total.toFixed(0) : (m.hash_total || 0).toFixed(1)) + ' MH/s', tone: 'on' };
|
||||
if (m.state === 'paused') return { text: 'Paused', tone: '' };
|
||||
if (n.state === 'syncing') return { text: 'Syncing the node', tone: '' };
|
||||
if (n.state === 'starting') return { text: 'Starting the node', tone: '' };
|
||||
if (n.state === 'restarting') return { text: 'Node restarting', tone: 'bad' };
|
||||
if (n.state === 'failed') return { text: 'Node failed', tone: 'bad' };
|
||||
if (n.state === 'stopped') return { text: 'Node stopped', tone: 'bad' };
|
||||
if (m.state === 'waiting') return { text: 'Starting the worker', tone: '' };
|
||||
if (m.state === 'idle') return { text: 'Not mining', tone: '' };
|
||||
return { text: cap(m.state || 'starting'), tone: '' };
|
||||
}
|
||||
// the confirmations' words (in place, never a dialog)
|
||||
function addressAsk(a) { return 'Pay ' + shortHex(a, 6, 4) + ' from the next block? The miner restarts.'; }
|
||||
function trustAsk(on) { return on ? 'Include proof records the node never checked? Devnet only; the node restarts.' : 'Verify proof records again? The node restarts.'; }
|
||||
return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, goalWords: goalWords, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, addressAsk: addressAsk, trustAsk: trustAsk };
|
||||
return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, goalWords: goalWords, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, addressAsk: addressAsk, trustAsk: trustAsk };
|
||||
})();
|
||||
/* ---------- Ember Tune: the card row's tuning line (pure; tune-line.test.mjs loads this block) ----------
|
||||
One line per card from the card state (src/state.rs, src/ember.rs): running (the phase and the step), tuned
|
||||
|
|
@ -1311,7 +1429,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
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, 8).map(function (e) { return '<div><span><span class="k ' + esc(e.kind) + '">' + esc(e.kind) + '</span>' + esc(View.eventLine(e.text)) + '</span><span class="t">' + clock(e.t) + '</span></div>'; }).join('') : '<div class="empty">No events yet. The node and the miner report here as they start.</div>';
|
||||
$('d-events').innerHTML = s.events.length ? s.events.slice(0, 8).map(function (e) { var pe = View.plainEvent(e.text); return '<div' + (pe.tech ? ' title="' + esc(pe.tech) + '"' : '') + '><span><i class="k ' + esc(e.kind) + '"></i>' + esc(pe.text) + '</span><span class="t">' + clock(e.t) + '</span></div>'; }).join('') : '<div class="empty">No events yet. The node and the miner report here as they start.</div>';
|
||||
}
|
||||
}
|
||||
// the Tuning card on Mine and the Tuning rows on Settings share the goal, the schedule and the Power control switch
|
||||
|
|
@ -1442,18 +1560,10 @@ if (typeof document !== 'undefined') (function () {
|
|||
else if (page === 'settings') renderSettings(s);
|
||||
}
|
||||
function renderPill(s) {
|
||||
var pill = $('pill'), text;
|
||||
var pill = $('pill'), pw = View.pill(s);
|
||||
pill.classList.remove('on', 'warn');
|
||||
if (s.quitting) text = 'stopping';
|
||||
else if (!s.setup_done) text = s.detecting ? 'detecting' : 'set up';
|
||||
else if (View.tuningCard(s.mining.cards)) { text = 'tuning'; pill.classList.add('on'); }
|
||||
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;
|
||||
if (pw.tone === 'on') pill.classList.add('on'); else if (pw.tone === 'bad') pill.classList.add('warn');
|
||||
$('pill-text').textContent = pw.text;
|
||||
var foot = (s.display_name ? '<b title="' + esc(s.display_name) + '">' + esc(s.display_name) + '</b><br>' : '') + (s.address.display ? 'pays ' + esc(s.address.display.slice(0, 6) + '…' + s.address.display.slice(-4)) : 'no address yet') + '<br>up ' + uptime(s.uptime_s);
|
||||
if (foot !== lastFoot) { lastFoot = foot; $('rail-status').innerHTML = foot; }
|
||||
var fv = 'v' + esc(s.version) + '<span class="chain"> · ' + esc(s.chain || s.network || '') + '</span>';
|
||||
|
|
@ -1492,7 +1602,8 @@ if (typeof document !== 'undefined') (function () {
|
|||
el.className = 'notice ' + n.kind + (n.tone ? ' ' + n.tone : '');
|
||||
el.dataset.key = n.key;
|
||||
$('notice-text').textContent = n.text; $('notice-text').title = n.title || '';
|
||||
$('notice-detail').textContent = n.detail; $('notice-detail').hidden = !n.detail; $('notice-detail').title = n.detail;
|
||||
$('notice-detail').textContent = n.detail; $('notice-detail').hidden = true; $('notice-detail').title = n.detail;
|
||||
$('notice-more').hidden = !n.detail; $('notice-more').setAttribute('aria-expanded', 'false'); $('notice-more').classList.remove('open');
|
||||
$('notice-actions').innerHTML = n.actions.map(function (a) { return '<button class="btn small ' + (a.primary ? 'primary' : 'ghost') + '" data-act="' + esc(a.act) + '"' + (a.disabled ? ' disabled' : '') + '>' + esc(a.label) + '</button>'; }).join('');
|
||||
$('notice-prog').hidden = !(n.progress >= 0);
|
||||
strip.hidden = false;
|
||||
|
|
@ -1514,6 +1625,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
var b = e.target.closest('[data-act]'); if (!b || b.disabled) return;
|
||||
var act = b.dataset.act, key = $('notice').dataset.key;
|
||||
if (act === 'close') { dismissed[key] = true; renderNotices(state || {}); }
|
||||
else if (act === 'more') { var open = $('notice-detail').hidden; $('notice-detail').hidden = !open; b.setAttribute('aria-expanded', open ? 'true' : 'false'); b.classList.toggle('open', open); layoutStrip(); }
|
||||
else if (act === 'install') { api('api/update/install', {}); toast('Installing at once'); }
|
||||
else if (act === 'retry') { api('api/update/install', {}); toast('Trying the update again'); }
|
||||
else if (act === 'open') api('api/update/open', {});
|
||||
|
|
@ -1659,7 +1771,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
}).catch(function () {
|
||||
failures++;
|
||||
if (failures >= 4) {
|
||||
$('pill').classList.add('warn', 'away'); $('pill-text').textContent = state && state.quitting ? 'stopped' : 'engine away';
|
||||
$('pill').classList.add('warn', 'away'); $('pill-text').textContent = state && state.quitting ? 'Stopped' : 'Engine not answering';
|
||||
if (phase === 'dashboard') {
|
||||
setText('node-line-text', 'Node: no answer'); setText('node-sub', 'the engine is not answering; the window reconnects by itself'); $('node-card').classList.add('bad');
|
||||
setText('btn-toggle-sub', 'the engine is not answering'); $('btn-toggle').disabled = true;
|
||||
|
|
|
|||
|
|
@ -48,8 +48,10 @@
|
|||
<!-- the status strip: one notice at a time (app.js, Notices) -->
|
||||
<div class="notices" id="notices" hidden>
|
||||
<div class="notice" id="notice" role="status" aria-live="polite">
|
||||
<i class="notice-dot" aria-hidden="true"></i>
|
||||
<span class="notice-text" id="notice-text"></span>
|
||||
<span class="notice-actions" id="notice-actions"></span>
|
||||
<button class="notice-more" id="notice-more" data-act="more" title="The technical line" aria-label="Show the technical line" aria-expanded="false" hidden><span class="chev"></span></button>
|
||||
<button class="notice-close" id="notice-close" data-act="close" title="Close" aria-label="Close">×</button>
|
||||
<span class="notice-detail mono" id="notice-detail" hidden></span>
|
||||
<span class="prog" id="notice-prog" hidden><i></i></span>
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ test('update wording: available, downloading with percent, installing, failed wi
|
|||
assert.equal(updateNotice(upd({ status: 'deferred' }), s()).text, 'Igneum Miner 0.3.6 is waiting for permission. It installs the next time someone is at this PC. Mining continues.');
|
||||
// updated: gone within 60 s of the new version starting
|
||||
const cur = upd({ status: 'current', available: false, ready: false, downloaded: false, updated_from: '0.3.4' });
|
||||
assert.equal(updateNotice(cur, s({ version: '0.3.6', uptime_s: 59 })).text, 'Updated to Igneum Miner 0.3.6 from 0.3.4.');
|
||||
assert.match(updateNotice(cur, s({ version: '0.3.6', uptime_s: 59, now: 1_800_000_000 })).text, /^Updated to 0\.3\.6 at \d\d:\d\d\.$/);
|
||||
assert.equal(updateNotice(cur, s({ version: '0.3.6', uptime_s: 60 })), null);
|
||||
assert.equal(updateNotice(upd({ status: 'current', available: false }), s()), null);
|
||||
// a build with no manifest reports an error the user cannot act on: no notice (the Settings note still says it)
|
||||
|
|
@ -106,22 +106,26 @@ test('update wording: available, downloading with percent, installing, failed wi
|
|||
|
||||
test('job wording: running with minutes and stage, done goes after 5 minutes, failed stays with the first error line', () => {
|
||||
const r = jobNotice(run(), NOW);
|
||||
assert.equal(r.text, 'Job: shard benchmark running, 6 min. Proving shard 3 of 8.');
|
||||
assert.equal(r.detail, 'RESULT shard=2 prove_s=39.8');
|
||||
assert.equal(jobNotice(run({ stage: '' }), NOW).text, 'Job: shard benchmark running, 6 min.');
|
||||
assert.equal(r.text, 'A job from the team is running: shard benchmark, 6 min. Proving shard 3 of 8.');
|
||||
assert.equal(r.detail, 'shard benchmark · RESULT shard=2 prove_s=39.8', 'the technical line goes behind the chevron');
|
||||
assert.equal(jobNotice(run({ stage: '' }), NOW).text, 'A job from the team is running: shard benchmark, 6 min.');
|
||||
const d = jobNotice(fin('done'), NOW);
|
||||
assert.equal(d.text, 'Job: build done after 32 min. Report uploaded.');
|
||||
assert.equal(d.text, 'A job from the team ran: build, 32 min.');
|
||||
assert.equal(d.key, 'job:done:job-6');
|
||||
assert.ok(jobNotice(fin('done'), NOW - 60 + N.JOB_DONE_S));
|
||||
assert.equal(jobNotice(fin('done'), NOW - 60 + N.JOB_DONE_S + 1), null);
|
||||
const f = jobNotice(fin('failed'), NOW);
|
||||
assert.equal(f.text, 'Job: build failed after 32 min, exit 1. Report not uploaded.');
|
||||
assert.equal(f.detail, 'BUILD FAILED: error[E0425]: cannot find value `foo`');
|
||||
assert.equal(f.text, 'A job from the team failed: build, after 32 min. The report did not upload.');
|
||||
assert.match(f.detail, /BUILD FAILED: error\[E0425\]: cannot find value `foo` · exit 1$/);
|
||||
assert.equal(f.tone, 'bad');
|
||||
assert.ok(jobNotice(fin('failed'), NOW + 86400 * 7), 'a failed job stays until closed');
|
||||
// no error line among the results: the summary is the cause
|
||||
assert.equal(jobNotice(fin('failed', { last_results: ['STAGE build'] }), NOW).detail, 'build exited with code 1');
|
||||
assert.equal(jobNotice(fin('timeout'), NOW).text, 'Job: build hit its time cap after 32 min, exit 1. Report not uploaded.');
|
||||
assert.match(jobNotice(fin('failed', { last_results: ['STAGE build'] }), NOW).detail, /^build: build exited with code 1 · exit 1$/);
|
||||
assert.equal(jobNotice(fin('timeout'), NOW).text, 'A job from the team ran out of time: build, after 32 min. The report did not upload.');
|
||||
assert.equal(N.jobWord('', 'update-now-0313-switch-d937c69d'), 'update check');
|
||||
assert.equal(N.jobWord('restart', 'restart-miners-0312'), 'restart');
|
||||
assert.equal(N.jobWord('collect', ''), 'log collection');
|
||||
assert.equal(N.jobWord('', 'ember-tune-pc1-5'), 'tuning check');
|
||||
assert.equal(jobNotice({ active: false, last: {} }, NOW), null);
|
||||
assert.equal(jobNotice(null, NOW), null);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -315,3 +315,34 @@ test('the node line, the activity line, the asks', () => {
|
|||
assert.match(V.trustAsk(false), /^Verify proof records again\?/);
|
||||
assert.equal(V.ago(30), 'just now'); assert.equal(V.ago(7200), '2 h ago'); assert.equal(V.ago(200000), '2 d ago');
|
||||
});
|
||||
|
||||
test('the activity feed and the pill speak in the user\'s voice; the engine line is kept as the tooltip', () => {
|
||||
const pe = V.plainEvent('job update-now-0313-switch-d937c69d (Exec restart: re-read the manifest (exec_restart_number 27276) and restart the node with it) done after 0 min, exit 0, report uploaded: update check and install asked');
|
||||
assert.equal(pe.text, 'A job from the team ran: update check, 0 min');
|
||||
assert.match(pe.tech, /^job update-now/);
|
||||
assert.equal(V.plainEvent('updated to Igneum Miner 0.3.14 from 0.3.13').text, 'Updated to 0.3.14');
|
||||
assert.equal(V.plainEvent('consensus parameters from the signed manifest: {"difficulty_v2_activation_daa":33000}').text, 'Rule settings read from the team');
|
||||
assert.equal(V.plainEvent('node synced: 151512 blocks, 6 peer(s)').text, 'Node synced: 151,512 blocks, 6 peers');
|
||||
assert.equal(V.plainEvent('block accepted by the node (Apple M5 Max)').text, 'Block found by Apple M5 Max');
|
||||
assert.equal(V.plainEvent('1 remote job received from Igneum').text, '1 job from the team received');
|
||||
assert.equal(V.plainEvent('remote job: update check now; a newer version installs at once').text, 'The team asked for an update check');
|
||||
assert.equal(V.plainEvent('a node already answers on 127.0.0.1:26610; using it (it is not stopped by this app)').text, 'Using the node already running on this machine');
|
||||
assert.equal(V.plainEvent('igneumd exited with code 101 after 300 s; restarting').text, 'The node stopped (code 101); restarting');
|
||||
assert.equal(V.plainEvent('miner apple:0 exited with code 1; restarting in 8 s').text, 'A worker stopped (code 1); restarting in 8 s');
|
||||
assert.equal(V.plainEvent('NVIDIA GeForce RTX 5090 kernel race: l128w16 at 122.3 MH/s (+4.1% over base, 6 variants, 40 s)').text, 'NVIDIA GeForce RTX 5090: program variant chosen, 122.3 MH/s');
|
||||
assert.equal(V.plainEvent('block 59199 shard 0 paid 0.0125 IGN').text, 'Proof paid: 0.0125 IGN');
|
||||
assert.equal(V.plainEvent('rewards go to 0xdd442fcbb964a3afdc90d49b408e8dd296fa86e8').text, 'Rewards go to 0xdd44…86e8');
|
||||
assert.equal(V.plainEvent('mining resumed').text, 'Mining resumed');
|
||||
assert.equal(V.plainEvent('something new the engine says').text, 'Something new the engine says');
|
||||
assert.equal(V.plainEvent('something new the engine says').tech, '');
|
||||
const base = { setup_done: true, quitting: false, mining: { state: 'mining', hash_total: 510.6, cards: [] }, node: { state: 'synced' } };
|
||||
assert.deepEqual(V.pill(base), { text: 'Mining · 511 MH/s', tone: 'on' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'mining', hash_total: 17.04, cards: [] } }), { text: 'Mining · 17.0 MH/s', tone: 'on' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'paused', hash_total: 0, cards: [] } }), { text: 'Paused', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'syncing' } }), { text: 'Syncing the node', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'restarting' } }), { text: 'Node restarting', tone: 'bad' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'mining', hash_total: 1, cards: [{ state: 'tuning' }] } }), { text: 'Tuning', tone: 'on' });
|
||||
assert.deepEqual(V.pill({ ...base, setup_done: false, detecting: true }), { text: 'Detecting cards', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, quitting: true }), { text: 'Stopping', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'idle', hash_total: 0, cards: [] } }), { text: 'Not mining', tone: '' });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -275,3 +275,35 @@ the quit ask, paused, syncing, the clock, the update card, the job strip, the lo
|
|||
| A Windows and a PC screenshot of the row with a real NVIDIA card (the telemetry cells, the cap slider, Retry) | the 0.3.14 cut, first thing to look at |
|
||||
| The row's state word while tuning carries the meta after it ("tuning: step 7 of 9 · measuring 441 W · 32 GB · ...") and is cut on a narrow row | a later pass: drop the meta while tuning |
|
||||
| The merge with `ember-tune`: `app.js` takes this branch's file (it already carries ember-tune's UI words and test); the engine files merge by themselves | the 0.3.14 shipper |
|
||||
|
||||
## 14. Polish round (6 October 2026, after the 0.3.14 cut)
|
||||
|
||||
the project lead, on the live Mac app: "I do think this could look better", at the job strip reading "Job: 0.3.14 is published:
|
||||
re-read the manifest and install now (...) done after 0 min. Report uploaded." Branch rebased on master 630b537.
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| No raw job, manifest or relay text on a user surface. The strip reads "A job from the team ran: update check, 0 min." / "A job from the team is running: shard benchmark, 6 min. Proving shard 3 of 8." / "A job from the team failed: build, after 32 min." / "Updated to 0.3.14 at 18:52."; the technical line (the job's title, summary, RESULT line, exit code) sits behind a chevron; the × stays. `Notices.jobWord` maps a kind or an id prefix to a user's word (update check, restart, log collection, shard benchmark, build, tuning check, a script). | app.js Notices |
|
||||
| The Activity feed speaks in the app's voice: `View.plainEvent` maps every engine event string (`src/engine.rs` et al., 80 patterns) to a sentence ("Block found by Apple M5 Max", "Rule settings read from the team", "The node stopped (code 101); restarting", "A job from the team ran: update check, 0 min"); the engine's own line is the row's tooltip; the kind word becomes a coloured dot. Unknown lines keep their words with a JSON tail cut. | app.js View, renderMine |
|
||||
| The header: title, grey subtitle and the pill share one baseline; the pill is a sentence with a coloured dot: "Mining · 511 MH/s" (molten), "Tuning" (molten), "Paused", "Syncing the node", "Starting the worker", "Not mining" (grey), "Node restarting", "Node failed", "Engine not answering" (ember), "Stopping", "Detecting cards", "Setting up". `View.pill`. | app.js, app.css |
|
||||
| Three Mine layouts were offered behind `?variant=a|b|c` (quieter, denser, hero number); the project lead picked C, then ruled the page stays as shipped (section 15). The switch is gone. | app.css |
|
||||
| 390 px: the strip's text, chevron and × on one line. Light and dark checked. | app.css |
|
||||
|
||||
Screenshots: `p04`/`p05` the plain job strip closed and open, `p06` the updated strip,
|
||||
`p07` the header paused, `p08` the activity feed, `p09`/`p12` light, `p10`/`p11` 390 px. Tests: 36 pass.
|
||||
|
||||
## 15. the project lead's ruling: the banner only (6 October 2026, evening)
|
||||
|
||||
the project lead: "lets not actually change the miner page just sort the banner". Variant C and the Earnings reshaping are
|
||||
reverted; the Mine and Earnings screens are exactly as shipped in 0.3.14. The three variant screenshots are gone with
|
||||
them. What stays from the polish round:
|
||||
|
||||
| Kept | Where |
|
||||
|---|---|
|
||||
| The strip's look: the row surface (one step lighter than the page), a 1 px ember hairline along the top, ash text, ember only on the state dot and the chevron; the update strip and the job strip share the one `.notice`; the asks and the clock card carry the same hairline. No filled brown or mustard field anywhere (the rail's active item, the pill, the ghost-on button, the option, the stop ring, the log hit and `mark` lost their tints; only `::selection` keeps a colour). | app.css |
|
||||
| The strip's words: "A job from the team ran: update check, 0 min." / "Updated to 0.3.14 at 18:52.", the technical line behind the chevron, the × kept | app.js Notices |
|
||||
| The Activity feed in the app's voice (`View.plainEvent`, the engine line as the tooltip, a kind dot) | app.js View, renderMine |
|
||||
| The header: title, subtitle and pill on one baseline; the pill a sentence with a coloured dot (`View.pill`) | app.js, app.css |
|
||||
|
||||
Screenshots: `r01-mine-strip-dark.png`, `r02-mine-strip-light.png` (Mine as shipped, the job strip showing),
|
||||
`r03-earnings-dark.png`. Tests: 36 pass.
|
||||
|
|
|
|||
BIN
docs/plans/miner-ui-3/p04-strip-job-done-plain.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
docs/plans/miner-ui-3/p05-strip-job-done-open.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
docs/plans/miner-ui-3/p06-strip-updated.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
docs/plans/miner-ui-3/p07-header-paused.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
docs/plans/miner-ui-3/p08-activity-plain.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
docs/plans/miner-ui-3/p09-light-mine.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
docs/plans/miner-ui-3/p10-phone-mine.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
docs/plans/miner-ui-3/p11-phone-earnings.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
docs/plans/miner-ui-3/p12-light-earnings.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
docs/plans/miner-ui-3/r01-mine-strip-dark.png
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
docs/plans/miner-ui-3/r02-mine-strip-light.png
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
docs/plans/miner-ui-3/r03-earnings-dark.png
Normal file
|
After Width: | Height: | Size: 197 KiB |