diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index 96b73cb3..9a43a8f3 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -941,9 +941,36 @@ var View = (function () { } function profileWords(on) { return on ? 'Your address page on igneum.network is public: your key ids, blocks in the window, weight rank, signing presence and card model are shown. The site reads it from the chain and from this switch.' : 'Off: igneum.network/address keeps your page unlisted (noindex). Nothing about this machine is published.'; } // the confirmations' words (in place, never a dialog) + // ui-ota (src/uiota.rs): Settings > Interface. u = state.ui. {line, eyebrow, help} + function interfaceWords(u) { + u = u || {}; + var ver = u.active_version || u.embedded_version || ''; + if (!ver) return { line: '', eyebrow: '', help: '' }; + var ota = u.source === 'ota'; + var when = ota && u.installed_at > 0 ? dateWords(u.installed_at) : ''; + var line = 'Interface ' + ver + (ota ? ', over the air' + (when ? ', ' + when : '') : ', built in'); + var help = ''; + if (ota && !u.confirmed) help = 'Waiting for this window to confirm the new interface.'; + else if (u.error) help = 'The last interface over the air was refused: ' + u.error + ' The built-in one serves.'; + else if (u.builtin && u.published_version && u.published_version !== u.embedded_version) help = 'Interface ' + u.published_version + ' is published over the air; switch this off to take it.'; + else if (u.busy) help = 'Interface ' + u.published_version + ' is downloading.'; + else if (u.note) help = cap(u.note) + '.'; + else if (ota) help = 'Built in: ' + (u.embedded_version || '') + '.'; + return { line: line, eyebrow: ota ? 'over the air' : 'built in', help: help }; + } + function dateWords(unix) { var d = new Date(unix * 1000); return d.getDate() + ' ' + ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()] + ' ' + d.getFullYear(); } + // the page reloads itself when the served interface changed under it and nobody is mid-task: + // loaded = the version this page came up with, u = state.ui, ctx = {typing, sheetOpen, phase} + function shouldReload(loaded, u, ctx) { + if (!loaded || !u || !u.active_version || u.active_version === loaded) return false; + ctx = ctx || {}; + if (ctx.typing || ctx.sheetOpen) return false; + if (ctx.phase && ctx.phase !== 'dashboard') return false; + return true; + } function addressAsk(a) { return 'Pay ' + shortHex(a, 6, 4) + ' from the next block? Mining restarts.'; } function trustAsk(on) { return on ? 'Include proofs the node never checked? Devnet only. The node restarts.' : 'Check proofs 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, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, ign: ign, balanceWords: balanceWords, fleetWatts: fleetWatts, fleetPounds: fleetPounds, isMine: isMine, liveNote: liveNote, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, tipAge: tipAge, nodeSource: nodeSource, finalityWords: finalityWords, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords }; + 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, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, ign: ign, balanceWords: balanceWords, fleetWatts: fleetWatts, fleetPounds: fleetPounds, isMine: isMine, liveNote: liveNote, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, tipAge: tipAge, nodeSource: nodeSource, finalityWords: finalityWords, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk, interfaceWords: interfaceWords, shouldReload: shouldReload, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords }; })(); /* ---------- 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 @@ -2081,6 +2108,9 @@ if (typeof document !== 'undefined') (function () { setText('s-node-dir', 'chain data: ' + s.node_dir); setText('s-log-dir', 'log files: ' + s.log_dir); $('s-live').hidden = !s.live_page; + var iw = View.interfaceWords(s.ui); + setText('s-ui-line', iw.line || 'Interface'); setText('s-ui-eyebrow', iw.eyebrow || 'built in'); setText('s-ui-help', iw.help || 'Small changes to this window arrive over the air, signed by Igneum, without a new version of the app. A bundle that fails to load is rolled back to the built-in interface by itself.'); + sw('s-ui-builtin', st.ui_builtin); var u = s.update || {}; setText('s-version', 'Igneum Miner ' + s.version); setText('s-update-note', settingsUpdateNote(u)); @@ -2299,6 +2329,20 @@ if (typeof document !== 'undefined') (function () { if (phase === 'dashboard') renderPage(s); if (s.quitting && !$('btn-quit').disabled) { $('btn-quit').disabled = true; } } + // ---------- ui-ota: the health ping after the first paint, the first-paint error, the gentle reload ---------- + var uiLoaded = '', uiPinged = false, uiFirstError = ''; + window.addEventListener('error', function (e) { if (!uiPinged && !uiFirstError) { uiFirstError = String(e && e.message || 'script error'); api('api/ui/health', { error: uiFirstError }).catch(function () { }); } }); + function uiHealth(s) { + if (uiPinged) return; + uiPinged = true; + uiLoaded = (s.ui && s.ui.active_version) || ''; + if (!uiFirstError) api('api/ui/health', {}).catch(function () { }); + } + function uiReloadIfChanged(s) { + var typing = !!(document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA' || document.activeElement.tagName === 'SELECT')); + if (View.shouldReload(uiLoaded, s.ui, { typing: typing, sheetOpen: !$('sheet-key').hidden || !$('upd').hidden, phase: phase })) { uiLoaded = ''; location.reload(); } + } + $('s-ui-builtin').addEventListener('change', function () { var on = this.checked; api('api/ui/builtin', { on: on }).then(function (r) { if (r.ok) toast(on ? 'The built-in interface serves from the next load' : 'The interface over the air serves again'); else { toast(r.error || 'could not change'); $('s-ui-builtin').checked = !on; } }); }); var failures = 0, first = true, pollTick = 0; function poll() { // hidden windows poll every 5 s (the pill and the tray text still need a recent state), visible ones every second @@ -2317,6 +2361,8 @@ if (typeof document !== 'undefined') (function () { } render(s); pollChain(false); + uiHealth(s); + uiReloadIfChanged(s); perf.render += performance.now() - a; perf.polls++; if (failures === 0 && $('pill').classList.contains('away')) { $('pill').classList.remove('away', 'warn'); } }).catch(function () { diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index 43c7283f..55590a61 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -477,6 +477,13 @@ +
Interface 1.0.0, built in
+Small changes to this window arrive over the air, signed by Igneum, without a new version of the app. A bundle that fails to load is rolled back to the built-in interface by itself.
+ +