diff --git a/settings.html b/settings.html
index 6485a68..1c19ecb 100644
--- a/settings.html
+++ b/settings.html
@@ -602,11 +602,6 @@
Open extensions folder
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) + ' ' : '') + '
Show folder
';
+ return '
' + iconHtml + ' ' + escapeHtml(a.name) + builtInBadge + ' v' + escapeHtml(a.version) + ' ' + caps + '
' + escapeHtml(a.description || "") + (a.author ? ' — ' + escapeHtml(a.author) + ' ' : '') + '
' + updateLineFor(a) + '
Show folder
';
};
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(); })();