From 124325673f6ccf636c5633151a91ebd9d9ddf13d Mon Sep 17 00:00:00 2001 From: Local Dev Date: Wed, 9 Sep 2026 02:43:28 +0200 Subject: [PATCH] feat(theseus/settings): per-extension update info inline on the card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Extensions page had a "Pending updates" strip at the top listing the staged versions AND a "Check for updates" button that dumped a summary of every extension's status into a global status blob just below the button. Two places to look for what a single card was doing. Fold both surfaces into the extension card itself: - Each card grows a small update line under its description: green ↻ "Update vX.Y.Z staged — restart to apply" when a staged tarball is waiting, red "Update failed" (with the addon-updater's detail) when the last check-updates run couldn't advance the version, plain "Up to date" when it could and there was nothing newer. - The top strip is gone. The "Check for updates" button now just prints a one-line summary (N staged / N failed / all up to date) — the detail lives on each card. - listStagedAddonUpdates fires on tab visit and after Reload, so the card badge reflects the background poll without needing the user to click Check. --- settings.html | 113 +++++++++++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 48 deletions(-) diff --git a/settings.html b/settings.html index 6485a68..1c19ecb 100644 --- a/settings.html +++ b/settings.html @@ -602,11 +602,6 @@
-

Installed

Loading…
Extensions run with full app access — treat installing one like installing an unsigned executable. @@ -1242,6 +1237,34 @@ // ---- Add-ons management ---- const addonsList = document.getElementById("addonsList"); + // Per-addon update state, keyed by addon id. Populated by loadAddonUpdates() + // (staged: from listStagedAddonUpdates, background-polled) and by the + // manual "Check for updates" button (report: fresh per-addon status). + // renderAddons reads both when drawing each card so the update line lives + // inside the card — no separate "Pending updates" strip at the top. + const addonUpdates = { staged: {}, report: {} }; + function updateLineFor(a) { + const st = addonUpdates.staged[a.id]; + const rep = addonUpdates.report[a.id]; + if (st) { + return '
↻ Update v' + escapeHtml(st.version) + ' staged — restart Theseus to apply.
'; + } + if (!rep) return ""; + let msg = "", cls = "color:var(--dim)"; + switch (rep.status) { + case "up-to-date": msg = "Up to date."; break; + case "no-update-url": return ""; // don't clutter cards that never opted in + case "fetch-failed": msg = "Update check failed — " + escapeHtml(rep.detail || "network"); cls = "color:#f6768a"; break; + case "signature-invalid": msg = "Endpoint offered v" + escapeHtml(rep.newVer || "?") + " with a BAD signature — rejected."; cls = "color:#f6768a"; break; + case "sha256-mismatch": msg = "Endpoint offered v" + escapeHtml(rep.newVer || "?") + " but the tarball hash didn't match."; cls = "color:#f6768a"; break; + case "extract-failed": msg = "v" + escapeHtml(rep.newVer || "?") + " downloaded but wouldn't extract — " + escapeHtml(rep.detail || ""); cls = "color:#f6768a"; break; + case "manifest-mismatch": msg = "Extracted manifest didn't match signed values."; cls = "color:#f6768a"; break; + case "already-staged": msg = "↻ v" + escapeHtml(rep.newVer || "?") + " already staged — restart to apply."; cls = "color:var(--acid)"; break; + case "staged": msg = "↻ Staged v" + escapeHtml(rep.newVer || "?") + " — restart to apply."; cls = "color:var(--acid)"; break; + default: msg = escapeHtml(rep.status || "unknown"); + } + return '
' + msg + '
'; + } function renderAddons(snap) { const items = (snap && snap.installed) || []; if (!items.length) { @@ -1269,7 +1292,7 @@ const iconHtml = /^data:image\//i.test(a.icon || "") ? '' : escapeHtml(a.icon || "•"); - return '
' + iconHtml + ' ' + escapeHtml(a.name) + builtInBadge + ' v' + escapeHtml(a.version) + '' + caps + '
' + escapeHtml(a.description || "") + (a.author ? ' — ' + escapeHtml(a.author) + '' : '') + '
'; + return '
' + iconHtml + ' ' + escapeHtml(a.name) + builtInBadge + ' v' + escapeHtml(a.version) + '' + caps + '
' + escapeHtml(a.description || "") + (a.author ? ' — ' + escapeHtml(a.author) + '' : '') + '
' + updateLineFor(a) + '
'; }; const sectionHeader = (label, hint) => '
' + escapeHtml(label) + '
' @@ -1296,11 +1319,22 @@ try { renderAddons(await C.listAddons()); } catch (e) { addonsList.textContent = "Failed to load extensions: " + (e?.message || e); } } + // Refresh the staged-updates map from the background poll. Whatever came + // back through listStagedAddonUpdates is what promoteStagedUpdates will + // pick up on the next launch — the per-card badge reflects exactly that. + async function loadAddonUpdates() { + let staged = []; + try { staged = await C.listStagedAddonUpdates(); } catch { staged = []; } + addonUpdates.staged = {}; + for (const s of (staged || [])) addonUpdates.staged[s.id] = { version: s.version, name: s.name }; + } document.getElementById("addonsReload").addEventListener("click", async () => { - await C.reloadAddons(); loadAddons(); + await C.reloadAddons(); await loadAddonUpdates(); loadAddons(); }); document.getElementById("addonsOpenDir").addEventListener("click", () => C.openAddonsDir()); - document.querySelector('.side a[data-sec="addons"]').addEventListener("click", () => { loadAddons(); loadStagedAddonUpdates(); }); + document.querySelector('.side a[data-sec="addons"]').addEventListener("click", async () => { + await loadAddonUpdates(); loadAddons(); + }); // Plug-ins tab: refresh Ariadne's live daemon state on every visit so it // doesn't display stale "checking…" text if the background poll finished // while another section was open. Aegis card loads on page-init and its @@ -1311,19 +1345,9 @@ if (ar) ar.click(); }); - // Manual "Check for updates" for extensions — runs the same signed-update - // polling the boot timer runs, and surfaces what's staged for the next - // launch. Staged folders live in /addons-updates-staged/ and - // promoteStagedUpdates() applies them on next init. - async function loadStagedAddonUpdates() { - let staged = []; - try { staged = await C.listStagedAddonUpdates(); } catch { staged = []; } - const box = document.getElementById("addonsStagedBox"); - const list = document.getElementById("addonsStaged"); - if (!staged || !staged.length) { box.hidden = true; return; } - list.innerHTML = staged.map((s) => '
' + escapeHtml(s.name) + ' → ' + escapeHtml(s.version) + '
').join(""); - box.hidden = false; - } + // (Old separate "Pending updates" strip lived here. Per-card update + // line is now painted by updateLineFor() inside each addon row — + // loadAddonUpdates() populates addonUpdates.staged before renderAddons.) document.getElementById("addonsCheckUpdates").addEventListener("click", async () => { const btn = document.getElementById("addonsCheckUpdates"); const status = document.getElementById("addonsUpdStatus"); @@ -1337,42 +1361,35 @@ const staged = Array.isArray(res) ? res : (res?.staged || []); if (skipped === "no-pubkeys") { status.textContent = "Update endpoint disabled — no operator pubkey baked into this build."; - } else if (staged.length) { - status.textContent = staged.length + " update" + (staged.length > 1 ? "s" : "") + " staged; restart Theseus to apply."; - } else if (!report.length) { - status.textContent = "No extensions with an update endpoint."; } else { - // Show per-addon status so "no update" is never mistaken for a - // silent fetch failure. - const rows = report.map((r) => { - const label = escapeHtml(r.id) + " " + escapeHtml(r.currentVer || "?"); - let msg = ""; - switch (r.status) { - case "up-to-date": msg = "up to date"; break; - case "no-update-url": msg = "no updateURL declared"; break; - case "fetch-failed": msg = "fetch failed — " + escapeHtml(r.detail || "network"); break; - case "signature-invalid": msg = "endpoint offered " + escapeHtml(r.newVer || "?") + " with a BAD signature — rejected"; break; - case "sha256-mismatch": msg = "endpoint offered " + escapeHtml(r.newVer || "?") + " but its tarball hash didn't match"; break; - case "extract-failed": msg = "extract failed — " + escapeHtml(r.detail || ""); break; - case "manifest-mismatch": msg = "extracted manifest didn't match signed values"; break; - case "already-staged": msg = escapeHtml(r.newVer || "?") + " already staged; restart to apply"; break; - case "staged": msg = "staged " + escapeHtml(r.newVer || "?") + "; restart to apply"; break; - default: msg = escapeHtml(r.status || "unknown"); - } - return '
' + label + ' — ' + msg + '
'; - }).join(""); - status.innerHTML = rows; + // Fold the per-addon report into addonUpdates.report so the + // per-card update line reflects the freshest check. A summary at + // the top counts staged/updated vs. up-to-date, but the per-addon + // detail lives on each card. + addonUpdates.report = {}; + for (const r of report) addonUpdates.report[r.id] = r; + const stagedNow = report.filter((r) => r.status === "staged" || r.status === "already-staged").length; + const failed = report.filter((r) => ["fetch-failed","signature-invalid","sha256-mismatch","extract-failed","manifest-mismatch"].includes(r.status)).length; + if (stagedNow) { + status.textContent = stagedNow + " update" + (stagedNow > 1 ? "s" : "") + " staged; restart Theseus to apply."; + } else if (failed) { + status.textContent = failed + " failed — see the extension card" + (failed > 1 ? "s" : "") + " below."; + } else if (!report.length) { + status.textContent = "No extensions with an update endpoint."; + } else { + status.textContent = "All extensions up to date."; + } } - await loadStagedAddonUpdates(); + await loadAddonUpdates(); + loadAddons(); } catch (e) { status.textContent = "Check failed: " + (e?.message || e); } finally { btn.disabled = false; btn.textContent = orig; } }); - loadStagedAddonUpdates(); // Populate on first paint so the tab is ready when the user clicks in. - loadAddons(); + (async () => { await loadAddonUpdates(); loadAddons(); })();