| … | … | @@ -1543,42 +1543,105 @@ export function themeSettings (current) { |
| 1543 | 1543 | // renders a control the shell beneath it cannot honour is worse than one that renders nothing. |
| 1544 | 1544 | export function updateSettings (u) { |
| 1545 | 1545 | if (!u) return '' |
| 1546 | | const short = (h) => (h ? esc(String(h).slice(0, 12)) + '…' : '<span class="who">unknown</span>') |
| 1547 | | const checked = u.checkedAt ? '<p class="who">Last checked ' + esc(u.checkedAt) + '</p>' : '' |
| 1548 | | // ⚠ The node's own last OTA line, verbatim. It says things no derived field can — "no swarm |
| 1549 | | // fetcher", "downloading 0.0.6", "previously failed to boot" — and without it a device could only |
| 1550 | | // report that nothing happened. |
| 1551 | | const note = u.note ? '<p class="who">' + esc(u.note) + '</p>' : '' |
| 1552 | | // ⛔ THE VERSION FIRST, because it is the only line here a person can compare against a release |
| 1553 | | // page. Two tree hashes say something differs; they do not answer "is my update installed?", which |
| 1554 | | // is the question anyone opening this section actually has. |
| 1555 | | const built = u.builtAt ? ' <span class="who">built ' + esc(new Date(Number(u.builtAt)).toISOString()) + '</span>' : '' |
| 1556 | | const version = u.version |
| 1557 | | ? '<p><strong>Running ' + esc(u.version) + '</strong>' + built + '</p>' |
| 1546 | |
| 1547 | // ⛔ THIS SECTION ANSWERS ONE QUESTION: why am I not on the latest? It used to print two truncated |
| 1548 | // tree hashes and a note, which answers nothing — so every time an update did not apply, the only |
| 1549 | // way to find out why was a cable, a laptop and somebody who knows the codebase. That is not a |
| 1550 | // property a phone may have. |
| 1551 | // |
| 1552 | // ⛔ SO IT NAMES VERSIONS, AND IT NAMES THE OBSTACLE. A refusal is rendered in the words chooseBundle |
| 1553 | // used; a staged bundle identical to the running one is said to be identical rather than offered as |
| 1554 | // an update; and a bundle waiting for a restart says so. The hashes stay, underneath, because when |
| 1555 | // the version numbers are equal they are the only thing that distinguishes two builds — which is a |
| 1556 | // real state and one somebody created today by publishing one version twice. |
| 1557 | const short = (h) => (h ? esc(String(h).slice(0, 12)) + '…' : 'none') |
| 1558 | const ver = (v) => (v ? esc(v) : 'unknown') |
| 1559 | |
| 1560 | // ⛔ AN UNSTAMPED BUILD SAYS SO. Bundles older than the version stamp carry no version at all, and |
| 1561 | // printing "Running unknown" would read as a fault rather than as a fact about that build's age. |
| 1562 | const running = u.version |
| 1563 | ? '<p><strong>Running ' + ver(u.version) + '</strong>' + |
| 1564 | (u.builtAt ? ' <span class="who">built ' + esc(when(u.builtAt)) + '</span>' : '') + '</p>' |
| 1558 | 1565 | : '<p class="who">This build does not report its version — it predates the stamp.</p>' |
| 1559 | 1566 | |
| 1560 | | // ⛔ STAGED AND DIFFERENT is the only state that means "an update is waiting". Staged and IDENTICAL |
| 1561 | | // is the ordinary case — the node re-verified the bundle it is already running — and calling that |
| 1562 | | // "update available" is what makes a restart button lie. |
| 1567 | // ⛔ REFUSED IS ITS OWN STATE, and the most important one: something IS waiting and it cannot be |
| 1568 | // applied, so a restart button here would be a lie. chooseBundle's own sentence is used verbatim — |
| 1569 | // it knows why, and rewording it would be a second place to keep in step. |
| 1570 | if (u.refused && u.refused.reason) { |
| 1571 | return '<h2>App updates</h2>' + running + |
| 1572 | '<p class="warn">An update is downloaded but cannot be applied: ' + esc(u.refused.reason) + '</p>' + |
| 1573 | '<p class="who">Nothing to do from here — this usually needs a newer app, not another restart.</p>' + |
| 1574 | batteryLine(u) + facts(u, short) + checkedLine(u) |
| 1575 | } |
| 1576 | |
| 1563 | 1577 | const waiting = updateWaiting(u) |
| 1564 | | // ⛔ ALWAYS SHOWN, BOTH VALUES, IN EVERY STATE. This used to print `running` alone when it judged |
| 1565 | | // itself up to date — so when that judgement was WRONG (it required `running`, which is null on a |
| 1566 | | // fresh install) there was no way to see that a bundle had in fact been staged. A page that shows |
| 1567 | | // its inputs only when it already agrees with you cannot be used to find out that it is wrong, |
| 1568 | | // and several hours went into guessing at a state this line could have printed. |
| 1569 | | const facts = '<p class="who">running ' + short(u.running) + |
| 1570 | | ' · staged ' + short(u.staged) + '</p>' |
| 1571 | | |
| 1572 | | const body = waiting |
| 1573 | | ? '<p class="warn">An update is downloaded and will apply on the next start.</p>' + facts + |
| 1574 | | (u.ota ? '<p><button id="app-restart">restart now</button></p>' : '<p class="who">This app cannot restart itself; close and reopen it.</p>') |
| 1575 | | : '<p>Up to date.</p>' + facts |
| 1576 | | |
| 1577 | | return '<h2>App updates</h2>' + version + body + checked + note + |
| 1578 | if (waiting) { |
| 1579 | return '<h2>App updates</h2>' + running + |
| 1580 | '<p class="warn">' + ver(u.stagedVersion) + ' is downloaded and will apply when the node restarts.</p>' + |
| 1581 | (u.ota |
| 1582 | ? '<p><button id="app-restart">restart now</button></p>' |
| 1583 | : '<p class="who">This app cannot restart itself; close and reopen it.</p>') + |
| 1584 | batteryLine(u) + facts(u, short) + checkedLine(u) |
| 1585 | } |
| 1586 | |
| 1587 | // ⛔ "Up to date" ONLY when nothing is staged that differs. A staged bundle equal to the running one |
| 1588 | // is the ordinary case — the node re-verified what it is already running — and calling that an |
| 1589 | // update is exactly what made a restart button that did nothing. |
| 1590 | return '<h2>App updates</h2>' + running + |
| 1591 | '<p>Up to date.</p>' + |
| 1592 | batteryLine(u) + facts(u, short) + checkedLine(u) |
| 1593 | } |
| 1594 | |
| 1595 | // ⚠ ALWAYS SHOWN, IN EVERY STATE. A page that shows its inputs only when it agrees with you cannot be |
| 1596 | // used to discover that it is wrong — which cost several hours when `running` was null on a fresh |
| 1597 | // install and the banner silently required it. |
| 1598 | function facts (u, short) { |
| 1599 | return '<p class="who">running ' + short(u.running) + ' · staged ' + short(u.staged) + |
| 1600 | (u.stagedVersion ? ' (' + esc(u.stagedVersion) + ')' : '') + '</p>' |
| 1601 | } |
| 1602 | |
| 1603 | /** |
| 1604 | * ⛔ THE REASON UPDATES ARRIVE LATE OR NOT AT ALL, said out loud. |
| 1605 | * |
| 1606 | * A frozen app has no swarm, so the release watch is never told anything. Measured on an SM-S922U: |
| 1607 | * Samsung's Freecess logs "freeze to.favela.surface", and an update published while the app was in |
| 1608 | * the background did not arrive until it was foregrounded some minutes later. From the outside that |
| 1609 | * is indistinguishable from a broken updater, and it was read as one — repeatedly, for a whole day. |
| 1610 | * |
| 1611 | * ⚠ Only shown when the shell actually ASKED and the answer was no. `null` means an older shell that |
| 1612 | * cannot ask, and warning somebody about a state nothing has measured is how a page teaches people to |
| 1613 | * ignore it. |
| 1614 | */ |
| 1615 | function batteryLine (u) { |
| 1616 | if (u.batteryExempt !== false) return '' |
| 1617 | return '<p class="warn">This phone is allowed to freeze the node in the background. While it is ' + |
| 1618 | 'frozen it has no network, so updates and posts only arrive while you have the app open.</p>' + |
| 1619 | '<p><button id="app-battery">allow it to run in the background</button></p>' |
| 1620 | } |
| 1621 | |
| 1622 | function checkedLine (u) { |
| 1623 | return (u.checkedAt ? '<p class="who">Last checked ' + esc(when(u.checkedAt)) + '</p>' : '') + |
| 1624 | (u.note ? '<p class="who">' + esc(u.note) + '</p>' : '') + |
| 1578 | 1625 | (u.ota ? '<p><button id="app-check">check now</button> <span id="app-msg" class="who"></span></p>' : '') + |
| 1579 | 1626 | (u.ota ? '<script type="module">\n' + UPDATE_JS + '\n</script>' : '') |
| 1580 | 1627 | } |
| 1581 | 1628 | |
| 1629 | /** |
| 1630 | * A time somebody can read. |
| 1631 | * |
| 1632 | * ⚠ Relative, because "2026-09-11T19:52:39.148Z" is not an answer to "is this recent?" — which is the |
| 1633 | * only thing anyone asks of a timestamp on this page. |
| 1634 | */ |
| 1635 | function when (t) { |
| 1636 | const ms = typeof t === 'number' ? t : Date.parse(t) |
| 1637 | if (!ms) return String(t) |
| 1638 | const secs = Math.round((Date.now() - ms) / 1000) |
| 1639 | if (secs < 60) return 'just now' |
| 1640 | if (secs < 3600) return Math.round(secs / 60) + ' min ago' |
| 1641 | if (secs < 86400) return Math.round(secs / 3600) + ' hours ago' |
| 1642 | return new Date(ms).toISOString().slice(0, 10) |
| 1643 | } |
| 1644 | |
| 1582 | 1645 | // ⛔ THE ONLY THING THIS POSTS IS A VERB. `parseShellMessage` on the other side is an allow-list of |
| 1583 | 1646 | // three names and nothing else — no op passed through to a handler map, which is how a WebView bridge |
| 1584 | 1647 | // turns into a remote-call surface for anything that reaches the page. |
| … | … | @@ -1596,6 +1659,8 @@ if (check) check.addEventListener('click', (e) => { |
| 1596 | 1659 | }) |
| 1597 | 1660 | const restart = document.getElementById('app-restart') |
| 1598 | 1661 | if (restart) restart.addEventListener('click', (e) => { e.preventDefault(); post('restart') }) |
| 1662 | const battery = document.getElementById('app-battery') |
| 1663 | if (battery) battery.addEventListener('click', (e) => { e.preventDefault(); post('battery') }) |
| 1599 | 1664 | ` |
| 1600 | 1665 | |
| 1601 | 1666 | export function settings (profile, message, handle = null, namespace = null, token = null, theme = null, reach = null, update = null, known = [], did = null) { |