diff --git a/app/igneum-app/ui/app.css b/app/igneum-app/ui/app.css index bde54a016..1474113f1 100644 --- a/app/igneum-app/ui/app.css +++ b/app/igneum-app/ui/app.css @@ -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.on{color:var(--molten);border-color:var(--molten-40)} -.pill.warn{color:var(--ember)} +.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);background:var(--molten-10)} +.pill.warn{color:var(--ember);border-color:var(--ember-40);background:var(--ember-12)} .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} @@ -126,7 +128,11 @@ body.has-rail .notices{left:var(--rail)} .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-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:3px;background:rgba(127,127,127,.2);border-radius:2px;overflow:hidden;margin-top:-3px} .notice .prog i{display:block;height:100%;width:0;background:var(--ember);transition:width .5s linear} @@ -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 */ @@ -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 */ @@ -605,3 +614,59 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)} .drawer-head{padding-bottom:6px} .toggle-big{min-height:96px} } + +/* variant A, quieter: fewer borders, type carries the hierarchy */ +body[data-variant="a"] .card{border-color:transparent;padding:28px} +body[data-variant="a"] .card h3{font-size:20px} +body[data-variant="a"] .gpu-list{gap:0} +body[data-variant="a"] .gpu-line{border:0;border-radius:0;background:transparent;border-bottom:1px solid var(--line)} +body[data-variant="a"] .gpu-line:last-child{border-bottom:0} +body[data-variant="a"] .gl-main{padding:16px 0} +body[data-variant="a"] .gl-tune,body[data-variant="a"] .gl-details{border-top:0;padding-left:60px;padding-right:0;padding-top:0} +body[data-variant="a"] .gl-details{padding-bottom:16px} +body[data-variant="a"] .totals{border-color:transparent} +body[data-variant="a"] .toggle-big.stop{border-color:transparent} +body[data-variant="a"] .tot{border-left:0} +body[data-variant="a"] .num .v{font-size:22px} +body[data-variant="a"] .seg{border-color:transparent} +body[data-variant="a"] #dag{border:0} +/* variant B, denser: one line per card, numbers right-aligned in fixed tabular columns, a slim total bar */ +body[data-variant="b"] .hero-row{grid-template-columns:auto 1fr;gap:var(--s-3)} +body[data-variant="b"] .toggle-big{min-height:64px;flex-direction:row;align-items:center;gap:14px;padding:12px 18px} +body[data-variant="b"] .toggle-big .ring{margin:0} +body[data-variant="b"] .toggle-big .ts{display:none} +body[data-variant="b"] .totals{padding:10px 20px;align-items:center} +body[data-variant="b"] .tot .v{font-size:22px} +body[data-variant="b"] .tot .k{display:inline;margin:0 0 0 6px} +body[data-variant="b"] .tot .s{margin-top:0} +body[data-variant="b"] .card{padding:16px 20px} +body[data-variant="b"] .card-head{margin-bottom:8px} +body[data-variant="b"] .gpu-list{gap:0;border:1px solid var(--line);border-radius:var(--row-r);overflow:hidden} +body[data-variant="b"] .gpu-line{border:0;border-radius:0;border-top:1px solid var(--line)} +body[data-variant="b"] .gpu-line:first-child{border-top:0} +body[data-variant="b"] .gl-main{padding:8px 12px;gap:var(--s-3);grid-template-columns:32px minmax(140px,1fr) auto auto} +body[data-variant="b"] .badge{width:32px;height:32px;flex-basis:32px;border-radius:8px;font-size:9px} +body[data-variant="b"] .gpu-line .name{font-size:var(--t-base)} +body[data-variant="b"] .gpu-line .msg{display:none} +body[data-variant="b"] .nums{gap:0;flex-wrap:nowrap} +body[data-variant="b"] .num{min-width:92px;padding-left:8px} +body[data-variant="b"] .num .v{font-size:16px;font-family:var(--mono);font-weight:500} +body[data-variant="b"] .num .k{font-size:9px} +body[data-variant="b"] .gl-tune{padding:0 12px 6px 56px;border-top:0;font-size:var(--t-xs)} +body[data-variant="b"] .gl-tune .bar{height:2px} +body[data-variant="b"] .gl-details{padding-left:56px} +body[data-variant="b"] #dag{height:72px} +body[data-variant="b"] .feed>div{padding:4px 0} +/* variant C, the hero number: the fleet total big, the button a bar under it, the cards below */ +body[data-variant="c"] .hero-row{grid-template-columns:1fr} +body[data-variant="c"] .totals{order:1;grid-template-columns:2fr 1fr 1fr 1fr;align-items:end;padding:28px 28px 24px;border-color:transparent;background:linear-gradient(180deg,var(--graphite),var(--obsidian))} +body[data-variant="c"] .tot:first-child .v{font-size:84px;line-height:.95;letter-spacing:-.03em} +body[data-variant="c"] .tot:first-child .k{font-size:var(--t-sm);margin-top:10px} +body[data-variant="c"] .tot:first-child .s{font-size:var(--t-md)} +body[data-variant="c"] .tot .v{font-size:30px} +body[data-variant="c"] .tot{border-left:0} +body[data-variant="c"] .toggle-big{order:2;min-height:64px;flex-direction:row;align-items:center;gap:14px;padding:12px 20px} +body[data-variant="c"] .toggle-big .ring{margin:0} +body[data-variant="c"] .toggle-big .ts{margin-left:auto} +body[data-variant="c"] .gl-main{padding:10px 14px} +body[data-variant="c"] .num .v{font-size:18px} diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index 015497462..8b7319523 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -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 @@ -589,6 +707,7 @@ if (typeof document !== 'undefined') (function () { var forcedUpdate = params.get('update'); // a sample update state for screenshots: available|downloading|ready|waiting|applying|urgent|manual|error|updated var forcedJob = params.get('job'); // a sample remote job for screenshots: running|done|failed if (params.get('host') === 'mac') document.body.classList.add('mac'); + if (params.get('variant')) document.body.dataset.variant = params.get('variant'); // ?variant=a|b|c: the Mine layouts of the 6 October polish round // ?debug=1 prints a budget line every 5 s (ms of JS per second in the canvas, the state render and the log) and // exposes the counters as window.__igneumPerf var debug = params.get('debug') === '1'; @@ -1311,7 +1430,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 '
' + esc(e.kind) + '' + esc(View.eventLine(e.text)) + '' + clock(e.t) + '
'; }).join('') : '
No events yet. The node and the miner report here as they start.
'; + $('d-events').innerHTML = s.events.length ? s.events.slice(0, 8).map(function (e) { var pe = View.plainEvent(e.text); return '' + esc(pe.text) + '' + clock(e.t) + ''; }).join('') : '
No events yet. The node and the miner report here as they start.
'; } } // the Tuning card on Mine and the Tuning rows on Settings share the goal, the schedule and the Power control switch @@ -1442,18 +1561,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 ? '' + esc(s.display_name) + '
' : '') + (s.address.display ? 'pays ' + esc(s.address.display.slice(0, 6) + '…' + s.address.display.slice(-4)) : 'no address yet') + '
up ' + uptime(s.uptime_s); if (foot !== lastFoot) { lastFoot = foot; $('rail-status').innerHTML = foot; } var fv = 'v' + esc(s.version) + ' · ' + esc(s.chain || s.network || '') + ''; @@ -1492,7 +1603,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 ''; }).join(''); $('notice-prog').hidden = !(n.progress >= 0); strip.hidden = false; @@ -1514,6 +1626,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 +1772,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; diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index da1cfcee2..34171f340 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -50,6 +50,7 @@
+ diff --git a/app/igneum-app/ui/notices.test.mjs b/app/igneum-app/ui/notices.test.mjs index 22cd67ab3..81a290d97 100644 --- a/app/igneum-app/ui/notices.test.mjs +++ b/app/igneum-app/ui/notices.test.mjs @@ -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); }); diff --git a/app/igneum-app/ui/view.test.mjs b/app/igneum-app/ui/view.test.mjs index d181c28b0..a02c2115f 100644 --- a/app/igneum-app/ui/view.test.mjs +++ b/app/igneum-app/ui/view.test.mjs @@ -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: '' }); +}); diff --git a/docs/plans/miner-ui-3.md b/docs/plans/miner-ui-3.md index e90a8584c..ec407c76f 100644 --- a/docs/plans/miner-ui-3.md +++ b/docs/plans/miner-ui-3.md @@ -275,3 +275,20 @@ 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 behind `?variant=a|b|c` for the project lead to pick: A quieter, B denser, C the hero number. The chosen one becomes the default. | app.css | +| 390 px: the strip's text, chevron and × on one line. Light and dark checked. | app.css | + +Screenshots: `p01-mine-variant-a-quieter.png`, `p02-mine-variant-b-denser.png`, `p03-mine-variant-c-hero.png` (full +page, the rig at 28p/kWh, the 5090 tuned), `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. diff --git a/docs/plans/miner-ui-3/p01-mine-variant-a-quieter.png b/docs/plans/miner-ui-3/p01-mine-variant-a-quieter.png new file mode 100644 index 000000000..11e477e68 Binary files /dev/null and b/docs/plans/miner-ui-3/p01-mine-variant-a-quieter.png differ diff --git a/docs/plans/miner-ui-3/p02-mine-variant-b-denser.png b/docs/plans/miner-ui-3/p02-mine-variant-b-denser.png new file mode 100644 index 000000000..c083c9a93 Binary files /dev/null and b/docs/plans/miner-ui-3/p02-mine-variant-b-denser.png differ diff --git a/docs/plans/miner-ui-3/p03-mine-variant-c-hero.png b/docs/plans/miner-ui-3/p03-mine-variant-c-hero.png new file mode 100644 index 000000000..492b67e20 Binary files /dev/null and b/docs/plans/miner-ui-3/p03-mine-variant-c-hero.png differ diff --git a/docs/plans/miner-ui-3/p04-strip-job-done-plain.png b/docs/plans/miner-ui-3/p04-strip-job-done-plain.png new file mode 100644 index 000000000..ab2e4d6d3 Binary files /dev/null and b/docs/plans/miner-ui-3/p04-strip-job-done-plain.png differ diff --git a/docs/plans/miner-ui-3/p05-strip-job-done-open.png b/docs/plans/miner-ui-3/p05-strip-job-done-open.png new file mode 100644 index 000000000..acc3292bd Binary files /dev/null and b/docs/plans/miner-ui-3/p05-strip-job-done-open.png differ diff --git a/docs/plans/miner-ui-3/p06-strip-updated.png b/docs/plans/miner-ui-3/p06-strip-updated.png new file mode 100644 index 000000000..02667756f Binary files /dev/null and b/docs/plans/miner-ui-3/p06-strip-updated.png differ diff --git a/docs/plans/miner-ui-3/p07-header-paused.png b/docs/plans/miner-ui-3/p07-header-paused.png new file mode 100644 index 000000000..331020759 Binary files /dev/null and b/docs/plans/miner-ui-3/p07-header-paused.png differ diff --git a/docs/plans/miner-ui-3/p08-activity-plain.png b/docs/plans/miner-ui-3/p08-activity-plain.png new file mode 100644 index 000000000..8e393249a Binary files /dev/null and b/docs/plans/miner-ui-3/p08-activity-plain.png differ diff --git a/docs/plans/miner-ui-3/p09-light-mine.png b/docs/plans/miner-ui-3/p09-light-mine.png new file mode 100644 index 000000000..3d0bf81f4 Binary files /dev/null and b/docs/plans/miner-ui-3/p09-light-mine.png differ diff --git a/docs/plans/miner-ui-3/p10-phone-mine.png b/docs/plans/miner-ui-3/p10-phone-mine.png new file mode 100644 index 000000000..e322fce2f Binary files /dev/null and b/docs/plans/miner-ui-3/p10-phone-mine.png differ diff --git a/docs/plans/miner-ui-3/p11-phone-earnings.png b/docs/plans/miner-ui-3/p11-phone-earnings.png new file mode 100644 index 000000000..30b50e1ff Binary files /dev/null and b/docs/plans/miner-ui-3/p11-phone-earnings.png differ diff --git a/docs/plans/miner-ui-3/p12-light-earnings.png b/docs/plans/miner-ui-3/p12-light-earnings.png new file mode 100644 index 000000000..8cecd4a3f Binary files /dev/null and b/docs/plans/miner-ui-3/p12-light-earnings.png differ