diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dec9245f1..d495af3d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,5 +67,5 @@ jobs: run: bash tools/ci/pinned-guests-check.sh - name: relay unit tests (parsers, secret compare, the wake endpoint) run: node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs - - name: miner app notice strip (ordering, keys, wording, timers) - run: node --test app/igneum-app/ui/notices.test.mjs + - name: miner app notice strip and update card (ordering, keys, wording, timers, when the card shows) + run: node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs diff --git a/app/igneum-app/Cargo.lock b/app/igneum-app/Cargo.lock index 16aeb43c9..10a6524da 100644 --- a/app/igneum-app/Cargo.lock +++ b/app/igneum-app/Cargo.lock @@ -219,7 +219,7 @@ dependencies = [ [[package]] name = "igneum-app" -version = "0.3.7" +version = "0.3.8" dependencies = [ "ed25519-dalek", "getrandom", diff --git a/app/igneum-app/Cargo.toml b/app/igneum-app/Cargo.toml index 5c6630a89..e481e8951 100644 --- a/app/igneum-app/Cargo.toml +++ b/app/igneum-app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "igneum-app" -version = "0.3.7" +version = "0.3.8" edition = "2021" description = "Igneum Miner engine: supervises the node, the miner and the GPU workers, and serves the dashboard on 127.0.0.1" license = "MIT" diff --git a/app/igneum-app/resources/igneum-app.rc b/app/igneum-app/resources/igneum-app.rc index 154d5524d..804e239e9 100644 --- a/app/igneum-app/resources/igneum-app.rc +++ b/app/igneum-app/resources/igneum-app.rc @@ -6,8 +6,8 @@ 1 ICON "igneum.ico" 1 VERSIONINFO -FILEVERSION 0,3,7,0 -PRODUCTVERSION 0,3,7,0 +FILEVERSION 0,3,8,0 +PRODUCTVERSION 0,3,8,0 FILEFLAGSMASK 0x3fL FILEFLAGS 0x0L FILEOS VOS_NT_WINDOWS32 @@ -20,12 +20,12 @@ BEGIN BEGIN VALUE "CompanyName", "Igneum" VALUE "FileDescription", "Igneum Miner engine" - VALUE "FileVersion", "0.3.7" + VALUE "FileVersion", "0.3.8" VALUE "InternalName", "igneum-app" VALUE "LegalCopyright", "Igneum contributors" VALUE "OriginalFilename", "igneum-app.exe" VALUE "ProductName", "Igneum Miner" - VALUE "ProductVersion", "0.3.7" + VALUE "ProductVersion", "0.3.8" END END BLOCK "VarFileInfo" diff --git a/app/igneum-app/ui/app.css b/app/igneum-app/ui/app.css index 69ea67842..07eb252e1 100644 --- a/app/igneum-app/ui/app.css +++ b/app/igneum-app/ui/app.css @@ -299,6 +299,40 @@ td .sub{display:block;font-family:var(--mono);font-size:var(--t-xs);color:var(-- .feed .k.block{color:var(--ember)} .feed .k.build{color:var(--ink-2)} +/* the update card (app.js, UpdateCard; the wallet carries the same block): the mark with its ring, the name, one + line, up to three note lines, the size, Install now and Later. Over the key sheet and the settings panel. */ +.upd-wrap{position:fixed;inset:0;z-index:35;background:rgba(12,12,14,.72);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px;animation:fade .25s ease} +@keyframes fade{from{opacity:0}to{opacity:1}} +.upd-card{position:relative;width:100%;max-width:500px;max-height:calc(100vh - 48px);overflow:auto;background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:34px 32px 28px;display:flex;flex-direction:column;align-items:center;text-align:center;gap:var(--s-2);box-shadow:0 30px 80px rgba(0,0,0,.6),0 0 0 1px rgba(242,84,27,.08);animation:rise .3s ease;outline:none} +.upd-card::before{content:"";position:absolute;left:50%;top:-80px;width:360px;height:260px;transform:translateX(-50%);border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.22) 0,rgba(242,84,27,0) 62%);pointer-events:none} +.upd-mark{position:relative;width:96px;height:96px;display:flex;align-items:center;justify-content:center;margin-bottom:var(--s-3)} +.upd-mark img{position:relative;display:block;filter:drop-shadow(0 6px 18px rgba(242,84,27,.35))} +.upd-ring{position:absolute;inset:0;transform:rotate(-90deg)} +.upd-ring .track{fill:none;stroke:var(--line-2);stroke-width:3} +.upd-ring .arc{fill:none;stroke:var(--ember);stroke-width:3;stroke-linecap:round;stroke-dasharray:276.5;stroke-dashoffset:276.5;transition:stroke-dashoffset .4s linear,stroke .3s ease} +.upd-mark.none .track{stroke:var(--line)} +.upd-mark.full .arc{stroke:var(--molten);stroke-dashoffset:0} +.upd-mark.busy .arc{stroke-dasharray:69 207.5;stroke-dashoffset:0;animation:spin 1.1s linear infinite;transform-origin:50% 50%} +.upd-mark.failed .arc{stroke:var(--ember);stroke-dashoffset:0;opacity:.55} +.upd-pct{position:absolute;left:50%;bottom:-11px;transform:translateX(-50%);font-size:var(--t-xs);font-weight:500;letter-spacing:.06em;color:var(--molten);background:var(--obsidian);border:1px solid var(--line-2);border-radius:999px;padding:2px 8px;font-variant-numeric:tabular-nums} +.upd-name{font-size:24px;font-weight:700;letter-spacing:-.01em;position:relative} +.upd-line{font-size:var(--t-xl);color:var(--ink-2);line-height:1.4;position:relative} +.upd-cause{font-size:var(--t-sm);color:var(--ember);line-height:1.5;max-width:40ch;word-break:break-word} +.upd-notes{list-style:none;margin:var(--s-2) auto 0;padding:0;width:max-content;max-width:100%;display:flex;flex-direction:column;gap:6px;align-items:flex-start;font-size:var(--t-md);color:var(--bone);line-height:1.4} +.upd-notes:empty{display:none} +.upd-notes li{display:flex;align-items:baseline;gap:10px;text-align:left} +.upd-notes li::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--ember);flex:0 0 6px;position:relative;top:-2px} +.upd-more{font:inherit;font-size:var(--t-sm);font-family:var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);background:transparent;border:0;padding:4px 8px;cursor:pointer;border-radius:6px;margin-top:2px} +.upd-more:hover{color:var(--bone)} +.upd-all{list-style:none;margin:0;padding:10px 14px;width:100%;max-height:150px;overflow:auto;text-align:left;font-size:var(--t-base);color:var(--ink-2);line-height:1.5;background:var(--obsidian);border:1px solid var(--line);border-radius:12px;display:flex;flex-direction:column;gap:4px;user-select:text;-webkit-user-select:text} +.upd-meta{font-size:var(--t-sm);color:var(--ash);letter-spacing:.04em;margin-top:var(--s-2);min-height:18px;line-height:1.5;max-width:44ch} +.upd-meta:empty{display:none} +.upd-actions{display:flex;gap:var(--s-3);align-items:center;justify-content:center;flex-wrap:wrap;margin-top:var(--s-4);position:relative} +.upd-actions:empty{display:none} +.upd-actions .btn.primary{min-width:160px} +@media (prefers-reduced-motion:reduce){.upd-wrap,.upd-card{animation:none}.upd-ring .arc{transition:none}.upd-mark.busy .arc{animation:none;stroke-dasharray:207.5 69}} +@media (max-height:620px){.upd-card{padding:24px 24px 20px}.upd-mark{width:72px;height:72px;margin-bottom:var(--s-2)}.upd-mark img{width:32px;height:32px}.upd-name{font-size:20px}} + /* sheet (the key) */ .sheet-wrap,.panel-wrap{position:fixed;inset:0;z-index:30;background:rgba(12,12,14,.72);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px} .sheet{background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:32px;max-width:640px;width:100%;max-height:calc(100vh - 48px);overflow:auto;display:flex;flex-direction:column;gap:14px;box-shadow:0 30px 80px rgba(0,0,0,.6);animation:rise .3s ease} diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index 355404c67..3d9b91285 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -112,7 +112,110 @@ var Notices = (function () { } return { LEVEL: LEVEL, JOB_DONE_S: JOB_DONE_S, INSTALLED_S: INSTALLED_S, updateNotice: updateNotice, jobNotice: jobNotice, clockNotice: clockNotice, gather: gather, pick: pick, jobTitle: jobTitle, cap: cap, endDot: endDot }; })(); -if (typeof module === 'object' && module && module.exports) module.exports = Notices; + +/* ---------- the update card (pure; update-card.test.mjs loads this block) ---------- + A centred card over the window for one update: the mark with a progress ring, "Igneum Ember 0.3.7", one line + (is available, is downloading, is ready to install, Installing, did not install), up to three lines of release + notes with the rest behind "What changed", the size, Install now and Later. The same card, the same rules and the + same words sit in the wallet (app/igneum-wallet/ui/update-card.js). + model() turns state.update into what the card says. decide() says whether it shows now, given the window + (ctx) and what the card already did (mem: open, later, seen): + deferred while a job runs, in the engine's first 60 s, while the key sheet is up, while the app quits + later Later, Escape or the backdrop hides this version at this stage; the strip (Notices) keeps it + back a newer version, or the download ready while automatic updates are off (with them on it installs + by itself, so a dismissed download stays dismissed); an open card follows its update to the end + installing and failed show only on an open card (Install now was pressed here); the strip carries the rest */ +var UpdateCard = (function () { + 'use strict'; + var NAME = 'Igneum Ember', LINE_MAX = 70, LINES = 3, FIRST_S = 60; + var INSTALL = { act: 'install', label: 'Install now', primary: true }, LATER = { act: 'later', label: 'Later' }; + var RETRY = { act: 'retry', label: 'Try again', primary: true }, OPEN = { act: 'open', label: 'Open the download', primary: true }; + function cap(t) { return t ? t.charAt(0).toUpperCase() + t.slice(1) : ''; } + function firstLine(t, max) { t = String(t || '').split('\n')[0].trim(); max = max || 160; return t.length > max ? t.slice(0, max - 1).trim() + '…' : t; } + // the manifest's notes as sentences: split on line breaks, then after . ! ? ; (no lookbehind: the Mac WebView) + function sentences(text) { + var out = [], parts = String(text || '').split(/\n+/); + for (var i = 0; i < parts.length; i++) { + var p = parts[i], at = 0; + for (var j = 0; j < p.length; j++) { + var ch = p.charAt(j), next = p.charAt(j + 1); + if ((ch === '.' || ch === '!' || ch === '?' || ch === ';') && (next === ' ' || next === '')) { push(p.slice(at, j + 1)); at = j + 1; } + } + push(p.slice(at)); + } + function push(s) { s = s.trim().replace(/[;:,]+$/, ''); if (s) out.push(cap(s)); } + return out; + } + // at most LINES lines, each under LINE_MAX characters (cut at a word); more = something is left for "What changed" + function splitNotes(text) { + var all = sentences(text), lines = [], more = all.length > LINES; + for (var i = 0; i < all.length && lines.length < LINES; i++) { + var s = all[i].replace(/\.$/, ''); + if (s.length >= LINE_MAX) { var cut = s.lastIndexOf(' ', LINE_MAX - 2); s = s.slice(0, cut > 20 ? cut : LINE_MAX - 2).replace(/[,;:]$/, '') + '…'; more = true; } + lines.push(s); + } + return { lines: lines, more: more, all: all }; + } + function size(bytes) { if (!(bytes > 0)) return ''; return bytes < 1e6 ? (bytes / 1e6).toFixed(1) + ' MB' : Math.round(bytes / 1e6) + ' MB'; } + function card(stage, u, over) { + var ver = u.version || ''; + var m = { stage: stage, version: ver, key: 'update:' + ver + ':' + (stage === 'available' || stage === 'downloading' || stage === 'staging' ? 'pending' : stage), name: NAME + ' ' + ver, line: '', note: '', cause: '', size: size(u.size), pct: -1, ring: 'none', actions: [], dismissable: true, auto: !!u.auto }; + var n = splitNotes(u.notes); m.lines = n.lines; m.more = n.more; m.all = n.all; + if (over) for (var k in over) m[k] = over[k]; + return m; + } + // u = state.update; s = { version, uptime_s, quitting } + function model(u, s) { + if (!u || !u.version) return null; + s = s || {}; + var pct = u.progress > 0 ? Math.min(100, Math.round(u.progress * 100)) : 0; + if (u.status === 'error') { + if (/no update manifest configured/.test(u.error || '')) return null; + return card('failed', u, { line: u.rolled_back ? 'did not stay up and was rolled back.' : 'did not install.', cause: firstLine(u.error, 120), ring: 'failed', actions: [RETRY, LATER], lines: [], more: false }); + } + if (u.applying || u.status === 'applying' || (u.status === 'ready' && u.wait === 'installing now')) { + return card('installing', u, { line: 'Installing. The app restarts itself.', note: s.quitting ? 'A moment.' : 'Mining continues until then.', ring: 'busy', dismissable: false, lines: [], more: false }); + } + var m = null; + switch (u.status) { + case 'available': m = card('available', u, { line: 'is available.', actions: [INSTALL, LATER] }); break; + case 'downloading': m = card('downloading', u, { line: 'is downloading.', pct: pct, ring: 'progress', actions: [INSTALL, LATER] }); break; + case 'staging': m = card('staging', u, { line: 'is being checked.', pct: 100, ring: 'progress', actions: [INSTALL, LATER] }); break; + case 'ready': + var why = /failed to install before/.test(u.wait || '') ? 'It failed to install before.' : u.auto ? 'It installs by itself at a quiet moment.' : ''; + m = card('ready', u, { line: 'is ready to install.', note: why, ring: 'full', actions: [INSTALL, LATER] }); break; + case 'deferred': m = card('deferred', u, { line: 'is waiting for permission.', note: 'It installs the next time someone is at this PC. Mining continues.', ring: 'full', actions: [INSTALL, LATER] }); break; + case 'manual': m = card('manual', u, { line: 'is downloaded.', note: 'Open the disk image and drag the app over the old one.', ring: 'full', actions: [OPEN, LATER] }); break; + } + if (!m) return null; + if (u.urgent && u.urgent_text) { m.note = u.urgent_text; m.dismissable = false; m.actions = m.actions.filter(function (a) { return a.act !== 'later'; }); } + return m; + } + // why the card may not open now: ctx = { jobActive, uptime_s, quitting, sheetOpen } + function blocked(ctx) { + ctx = ctx || {}; + if (ctx.quitting) return 'the app is quitting'; + if (ctx.sheetOpen) return 'the key sheet is open'; + if (ctx.jobActive) return 'a job is running'; + if (ctx.uptime_s >= 0 && ctx.uptime_s < FIRST_S) return 'the engine started under a minute ago'; + return ''; + } + // mem = { open: the card is up, later: the key Later was pressed on, seen: the version the card was shown for } + function decide(m, ctx, mem) { + mem = mem || {}; + if (!m) return { show: false, why: 'none' }; + var b = blocked(ctx); + if (b && m.stage !== 'installing') return { show: false, why: 'deferred', reason: b }; + if (m.stage === 'installing' || m.stage === 'failed') return mem.open ? { show: true, why: 'open' } : { show: false, why: 'strip' }; + if (!m.dismissable) return { show: true, why: 'urgent' }; + if (mem.later === m.key) return { show: false, why: 'later' }; + if (mem.open) return { show: true, why: 'open' }; + if (m.stage === 'ready' && m.auto && mem.seen === m.version) return { show: false, why: 'auto' }; + return { show: true, why: m.stage === 'ready' ? 'ready' : 'new' }; + } + return { NAME: NAME, LINE_MAX: LINE_MAX, LINES: LINES, FIRST_S: FIRST_S, sentences: sentences, splitNotes: splitNotes, size: size, model: model, blocked: blocked, decide: decide }; +})(); +if (typeof module === 'object' && module && module.exports) { module.exports = Notices; module.exports.UpdateCard = UpdateCard; } if (typeof document !== 'undefined') (function () { 'use strict'; @@ -851,6 +954,63 @@ if (typeof document !== 'undefined') (function () { else if (act === 'sync') api('api/clock/sync', {}); }); + // ---------- the update card (UpdateCard, top of this file): one update, centred, over everything ---------- + // mem: open (the card is up), later (the key Later was pressed on), seen (the version it was shown for); + // asked: Install now was pressed on this card and the engine has not flipped to applying yet + var updMem = { open: false, later: '', seen: '' }, updSig = '', updAsked = '', updMore = false; + if (forcedUpdate && params.get('card') === '1') updMem.open = true; // screenshots: installing and failed only show on an open card + function cardCtx(s) { return { jobActive: !!(s.jobs && s.jobs.active), uptime_s: s.uptime_s, quitting: !!s.quitting, sheetOpen: !$('sheet-key').hidden }; } + function renderUpdateCard(s) { + var m = UpdateCard.model(s.update, s), d = UpdateCard.decide(m, cardCtx(s), updMem), wrap = $('upd'); + if (!d.show) { + if (updMem.open || !wrap.hidden) { updMem.open = false; wrap.hidden = true; updSig = ''; } + return; + } + if (!updMem.open) { updMem.open = true; updMem.seen = m.version; updMore = false; if (updAsked !== m.key) updAsked = ''; } + var asked = updAsked === m.key && m.stage !== 'installing' && m.stage !== 'failed'; + var sig = [m.key, m.stage, m.line, m.note, m.cause, m.ring, asked ? 'asked' : '', m.lines.join('|')].join('\u0001'); + if (sig !== updSig) { + updSig = sig; + $('upd-name').textContent = m.name; + $('upd-line').textContent = m.line; + $('upd-cause').textContent = m.cause; $('upd-cause').hidden = !m.cause; + $('upd-notes').innerHTML = m.lines.map(function (l) { return '
  • ' + esc(l) + '
  • '; }).join(''); + $('upd-all').innerHTML = m.all.map(function (l) { return '
  • ' + esc(l) + '
  • '; }).join(''); + $('upd-more').hidden = !m.more; if (!m.more) updMore = false; + $('upd-more').textContent = updMore ? 'Less' : 'What changed'; $('upd-more').setAttribute('aria-expanded', updMore ? 'true' : 'false'); + $('upd-all').hidden = !updMore; + $('upd-mark').className = 'upd-mark ' + m.ring; + var acts = asked ? [{ act: 'install', label: m.stage === 'ready' || m.stage === 'deferred' ? 'Installing' : 'Installs when ready', primary: true, disabled: true }, { act: 'later', label: 'Later' }] : m.actions; + $('upd-actions').innerHTML = acts.map(function (a) { return ''; }).join(''); + $('upd').dataset.dismiss = m.dismissable ? '1' : ''; + } + // the ring and the percent move every poll; the words above do not + if (m.ring === 'progress') { $('upd-arc').style.strokeDashoffset = (276.5 * (1 - m.pct / 100)).toFixed(1); $('upd-pct').textContent = m.pct + '%'; $('upd-pct').hidden = m.stage !== 'downloading'; } + else { $('upd-arc').style.strokeDashoffset = ''; $('upd-pct').hidden = true; } + var meta = []; + if (m.stage === 'downloading' && m.size) meta.push(m.pct + '% of ' + m.size); + else if (m.size && (m.stage === 'available')) meta.push(m.size + ' download'); + else if (m.size && (m.stage === 'ready' || m.stage === 'manual' || m.stage === 'deferred' || m.stage === 'staging')) meta.push(m.size + ', downloaded'); + if (m.note) meta.push(m.note); + $('upd-meta').textContent = meta.join(' · '); + if (wrap.hidden) { wrap.hidden = false; $('upd-card').focus({ preventScroll: true }); } + } + function cardLater() { + if (!updMem.open || !$('upd').dataset.dismiss) return; + var m = UpdateCard.model(state && state.update, state || {}); + updMem.later = m ? m.key : updMem.later; updMem.open = false; $('upd').hidden = true; updSig = ''; + } + $('upd').addEventListener('click', function (e) { + if (e.target === $('upd')) { cardLater(); return; } + var b = e.target.closest('[data-act]'); if (!b || b.disabled) return; + var act = b.dataset.act, m = UpdateCard.model(state && state.update, state || {}); + if (act === 'later') cardLater(); + else if (act === 'install' || act === 'retry') { api('api/update/install', {}); updAsked = m ? m.key : ''; updSig = ''; toast(act === 'retry' ? 'Trying the update again' : m && m.stage === 'ready' ? 'Installing at once' : 'Installs as soon as it is downloaded'); if (state) renderUpdateCard(state); } + else if (act === 'open') api('api/update/open', {}); + }); + $('upd-more').addEventListener('click', function () { updMore = !updMore; $('upd-all').hidden = !updMore; this.textContent = updMore ? 'Less' : 'What changed'; this.setAttribute('aria-expanded', updMore ? 'true' : 'false'); }); + document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !$('upd').hidden) { e.preventDefault(); cardLater(); } }); + // ---------- over-the-air updates (src/ota.rs): the settings note; the strip is Notices.updateNotice ---------- function settingsUpdateNote(u) { var n = Notices.updateNotice(u, state || {}), parts = []; @@ -864,16 +1024,19 @@ if (typeof document !== 'undefined') (function () { return parts.join(' '); } function sampleUpdate(kind) { - var u = { available: true, version: '0.3.1', notes: 'difficulty v2, OTA updates', status: 'ready', downloaded: true, ready: true, applying: false, progress: 1, size: 20588331, auto: true, wait: 'installs at the next safe moment', urgent: false, urgent_text: '', activation_height: 0, error: '', updated_from: '', rolled_back: '' }; + // ?update=[&auto=0]: the card and the strip for each state, without an engine + var u = { available: true, version: '0.3.7', notes: 'The update card: one centred card, Install now and Later. Worker restarts no longer drop the hourly program. The log drawer wraps long lines and jumps to the last error. Windows Hello is written but untested; Touch ID confirms every send on the Mac.', status: 'ready', downloaded: true, ready: true, applying: false, progress: 1, size: 20588331, auto: params.get('auto') !== '0', wait: 'installs at the next safe moment', urgent: false, urgent_text: '', activation_height: 0, error: '', updated_from: '', rolled_back: '' }; + if (!u.auto) u.wait = 'waiting for Install now (automatic updates are off)'; if (kind === 'available') { u.status = 'available'; u.downloaded = false; u.ready = false; u.progress = 0; } - if (kind === 'downloading') { u.status = 'downloading'; u.downloaded = false; u.ready = false; u.progress = 0.43; } + if (kind === 'downloading') { u.status = 'downloading'; u.downloaded = false; u.ready = false; u.progress = Math.min(0.97, 0.43 + ((Date.now() / 1000) % 60) / 110); } + if (kind === 'staging') { u.status = 'staging'; u.ready = false; u.progress = 1; } if (kind === 'waiting') { u.wait = 'hourly program boundary in 97 s; installing after it'; } if (kind === 'applying') { u.status = 'applying'; u.applying = true; } if (kind === 'deferred') { u.status = 'deferred'; u.wait = 'waits for the next time someone is at this PC (Windows asks for permission); mining continues'; } - if (kind === 'urgent') { u.urgent = true; u.activation_height = 120000; u.urgent_text = 'Consensus upgrade at height 120000 (difficulty v2): the node is 1,240 blocks away. Installing 0.3.1 now.'; } + if (kind === 'urgent') { u.status = 'downloading'; u.ready = false; u.downloaded = false; u.progress = 0.27; u.urgent = true; u.activation_height = 120000; u.urgent_text = 'Consensus upgrade at height 120000 (difficulty v2): the node is 1,240 blocks away. Installing 0.3.7 now.'; } if (kind === 'manual') { u.status = 'manual'; u.ready = false; u.wait = '/Applications is not writable; open the downloaded disk image and drag the app over the old one'; } if (kind === 'error') { u.status = 'error'; u.error = 'sha256 mismatch: the file is not what the manifest signed'; u.downloaded = false; u.ready = false; } - if (kind === 'updated') { u.status = 'current'; u.available = false; u.ready = false; u.downloaded = false; u.updated_from = '0.3.0'; u.version = '0.3.1'; } + if (kind === 'updated') { u.status = 'current'; u.available = false; u.ready = false; u.downloaded = false; u.updated_from = '0.3.6'; u.version = '0.3.7'; } return u; } // ---------- remote jobs (src/jobrun.rs): the settings block; the strip is Notices.jobNotice ---------- @@ -906,11 +1069,13 @@ if (typeof document !== 'undefined') (function () { } function render(s) { state = s; stateAt = performance.now(); - if (forcedUpdate) { s.update = sampleUpdate(forcedUpdate); if (forcedUpdate === 'updated') { s.version = '0.3.1'; s.uptime_s = 20; } } + if (forcedUpdate) { s.update = sampleUpdate(forcedUpdate); if (forcedUpdate === 'updated') { s.version = '0.3.7'; s.uptime_s = 20; } } if (forcedJob) s.jobs = sampleJob(forcedJob, s.now || 0); + if (params.get('uptime')) s.uptime_s = parseInt(params.get('uptime'), 10) || 0; // the card's first-minute rule, for screenshots renderPill(s); renderClock(s); renderNotices(s); + renderUpdateCard(s); if (phase === 'cards') renderCards(s); if (phase === 'dashboard') renderDashboard(s); if (s.quitting && !$('btn-quit').disabled) { $('btn-quit').disabled = true; } diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index e99c75909..ab1f16256 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -218,6 +218,26 @@ + + +