| … | … | @@ -336,6 +336,10 @@ input[type=file]::file-selector-button:hover{border-color:var(--dim);background: |
| 336 | 336 | /* The update notice. Deliberately loud — it is the one thing on the page that expires. */ |
| 337 | 337 | .update-bar{background:var(--warn-bg,#8a5a00);text-align:center;padding:.5rem} |
| 338 | 338 | .update-bar a{color:#fff;text-decoration:none;font-weight:600} |
| 339 | /* Inline state words in the status table — loud enough to find, quiet enough to read past. */ |
| 340 | .warn-inline{color:var(--warn,#c88)} |
| 341 | table{border-collapse:collapse;width:100%} |
| 342 | td,th{text-align:left;padding:.25rem .5rem;vertical-align:top} |
| 339 | 343 | .ns-list{list-style:none;padding:0} |
| 340 | 344 | .ns-list li{margin:.35rem 0} |
| 341 | 345 | form.inline{display:inline} |
| … | … | @@ -589,7 +593,7 @@ function oneLine (text, max) { |
| 589 | 593 | const NAV_ITEMS = [['/', 'timeline'], ['/rooms', 'rooms'], ['/search', 'search'], ['/discover', 'discover'], |
| 590 | 594 | ['/following', 'following'], |
| 591 | 595 | ['/notifications', 'notifications'], ['/mentions', 'mentions'], ['/knocks', 'knocks'], |
| 592 | | ['/graph', 'graph'], ['/explore', 'explore'], |
| 596 | ['/graph', 'graph'], ['/explore', 'explore'], ['/status', 'status'], |
| 593 | 597 | ['/settings', 'settings'], ['/logout', 'sign out']] |
| 594 | 598 | const NAV = (title) => '<nav>' + NAV_ITEMS.map(([href, label]) => |
| 595 | 599 | '<a href="' + href + '"' + (label === title || (title === 'favela' && href === '/') ? ' aria-current="page"' : '') + |
| … | … | @@ -733,7 +737,8 @@ export function timeline (rows, { unreadable = 0, profiles = null, files = null, |
| 733 | 737 | const warn = unreadable |
| 734 | 738 | // ⛔ Never silent. "nothing here" and "nothing here, and N records I hold pointers for but |
| 735 | 739 | // cannot read" are different answers, and the daemon distinguishes them — so the page must too. |
| 736 | | ? '<p class="warn">' + unreadable + ' record(s) could not be read — the log arrived, the bytes did not.</p>' |
| 740 | ? '<p class="warn">' + unreadable + ' record(s) could not be read — the log arrived, the bytes did not. ' + |
| 741 | '<a href="/status">what this means</a></p>' |
| 737 | 742 | : '' |
| 738 | 743 | /** |
| 739 | 744 | * ⛔ TWO KINDS OF EMPTY, and they are not the same problem. |
| … | … | @@ -1768,3 +1773,77 @@ export function discover (handles, following = [], profiles = {}, ns = null) { |
| 1768 | 1773 | 'their posts in your timeline — joining a namespace on its own shows you nothing.</p>' + |
| 1769 | 1774 | '<ul class="ns-list">' + rows.map(row).join('') + '</ul>') |
| 1770 | 1775 | } |
| 1776 | |
| 1777 | // ── status ─────────────────────────────────────────────────────────────────────────────────────── |
| 1778 | // |
| 1779 | // ⛔ THE PAGE EVERY DEAD-END WARNING SHOULD POINT AT. "1 record could not be read — the log arrived, |
| 1780 | // the bytes did not" is accurate and useless on its own: it does not say WHOSE feed, or what would |
| 1781 | // fix it, so the only way to find out was to ask someone with a shell. The same was true of every |
| 1782 | // OTA failure until settings started printing the daemon's own last line — and that instrument |
| 1783 | // found a real bug on its first run, after hours of guessing at it from the outside. |
| 1784 | // |
| 1785 | // ⚠ It reports STATE, never a verdict. A page that prints a conclusion cannot be used to discover |
| 1786 | // the conclusion is wrong, which is exactly how a staged update sat invisible behind "Up to date". |
| 1787 | export function status ({ me = null, ns = null, following = [], missing = [], profiles = {}, update = null } = {}) { |
| 1788 | const short = (k) => esc(String(k || '').slice(0, 12)) |
| 1789 | const row = (k, v, why) => '<tr><td class="who">' + esc(k) + '</td><td>' + v + |
| 1790 | (why ? '<br><span class="who">' + esc(why) + '</span>' : '') + '</td></tr>' |
| 1791 | |
| 1792 | const held = (following || []).map((f) => (typeof f === 'string' ? f : f && f.feed)).filter(Boolean) |
| 1793 | const gone = new Set((missing || []).map((f) => (typeof f === 'string' ? f : f && f.feed)).filter(Boolean)) |
| 1794 | |
| 1795 | const feedRows = held.length |
| 1796 | ? held.map((f) => { |
| 1797 | const p = (profiles || {})[f] || {} |
| 1798 | const name = p.name ? esc(p.name) : '<span class="who">no profile yet</span>' |
| 1799 | // ⛔ THE TWO STATES A PERSON ACTUALLY HITS, named in the terms the warnings use. |
| 1800 | const state = gone.has(f) |
| 1801 | ? '<span class="warn-inline">followed, not held</span>' |
| 1802 | : (p.name ? 'held' : '<span class="warn-inline">held, profile not read</span>') |
| 1803 | return '<tr><td><a href="/u/' + esc(f) + '"><code>' + short(f) + '</code></a></td>' + |
| 1804 | '<td>' + name + '</td><td>' + state + '</td></tr>' |
| 1805 | }).join('') |
| 1806 | : '<tr><td colspan="3" class="who">You follow nobody yet — try <a href="/discover">discover</a>.</td></tr>' |
| 1807 | |
| 1808 | return layout('status', |
| 1809 | '<h1>Status</h1>' + |
| 1810 | '<p class="who">What this node holds, and what it is still waiting for. Every warning elsewhere ' + |
| 1811 | 'in favela means one of the states below.</p>' + |
| 1812 | |
| 1813 | '<h2>This node</h2><table>' + |
| 1814 | row('feed', me && me.feed ? '<code>' + short(me.feed) + '</code>' : '<span class="who">unknown</span>') + |
| 1815 | row('identity', me && me.linked ? 'linked' : '<span class="warn-inline">not linked</span>', |
| 1816 | me && me.linked ? '' : 'This node cannot claim a name or sign until an identity is minted.') + |
| 1817 | '</table>' + |
| 1818 | |
| 1819 | '<h2>Namespace</h2><table>' + |
| 1820 | row('key', ns && ns.key ? '<code>' + short(ns.key) + '</code>' : '<span class="who">none</span>', |
| 1821 | 'Handles resolve only within the namespace you are in.') + |
| 1822 | row('claims', ns ? String(ns.claims == null ? '?' : ns.claims) : '?', |
| 1823 | ns && ns.claims === 0 ? 'Nobody has claimed a handle here — you may be in the wrong namespace.' : '') + |
| 1824 | row('carrying', ns && ns.carrying ? 'yes' : 'no', |
| 1825 | ns && ns.carrying ? '' : 'This node reads the namespace but does not carry its log.') + |
| 1826 | '</table>' + |
| 1827 | |
| 1828 | '<h2>Feeds</h2>' + |
| 1829 | // ⛔ THE ANSWER TO "the log arrived, the bytes did not". A feed can be followed without being |
| 1830 | // held, and held without its records being readable — three states that look identical from a |
| 1831 | // timeline, and are fixed by completely different things. |
| 1832 | '<p class="who"><b>followed, not held</b> — the feed is in your follow list but has not ' + |
| 1833 | 'replicated. It arrives when a peer holding it is reachable.<br>' + |
| 1834 | '<b>held, profile not read</b> — the log arrived and some bytes have not. That is what an ' + |
| 1835 | 'unreadable record means; it resolves as the content replicates.</p>' + |
| 1836 | '<table><tr><th>feed</th><th>who</th><th>state</th></tr>' + feedRows + '</table>' + |
| 1837 | |
| 1838 | (update |
| 1839 | ? '<h2>Updates</h2><table>' + |
| 1840 | row('version', esc(update.version || 'unstamped')) + |
| 1841 | row('running', short(update.running) || '<span class="who">the app\'s own bundle</span>') + |
| 1842 | row('staged', short(update.staged) || '<span class="who">nothing staged</span>') + |
| 1843 | row('last checked', esc(update.checkedAt || 'never')) + |
| 1844 | // ⛔ The daemon's own words, verbatim. It is the only line that distinguishes "cannot reach |
| 1845 | // the swarm" from "nothing published" from "downloading" — three different problems. |
| 1846 | row('last message', update.note ? '<code>' + esc(update.note) + '</code>' : '<span class="who">none</span>') + |
| 1847 | '</table>' |
| 1848 | : '')) |
| 1849 | } |