feat(theseus/settings): per-extension update info inline on the card
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.
This commit is contained in:
parent
0d583fb749
commit
124325673f
1 changed files with 65 additions and 48 deletions
107
settings.html
107
settings.html
|
|
@ -602,11 +602,6 @@
|
|||
<button id="addonsOpenDir" class="btn" type="button">Open extensions folder</button>
|
||||
</div>
|
||||
<div id="addonsUpdStatus" class="pmuted" style="font-size:12.5px;margin-top:6px;text-align:right">—</div>
|
||||
<div id="addonsStagedBox" hidden style="margin-top:1rem;padding:12px 14px;border:1px solid var(--acid);border-radius:10px;background:rgba(214,255,61,.06)">
|
||||
<div style="font-weight:600;margin-bottom:6px">Pending updates</div>
|
||||
<div id="addonsStaged" style="font-size:13px"></div>
|
||||
<div style="color:var(--dim);font-size:12px;margin-top:8px">These apply on the next Theseus restart.</div>
|
||||
</div>
|
||||
<h2 class="sub" style="border-top:0;padding-top:0;margin-top:1.5rem">Installed</h2>
|
||||
<div id="addonsList"><div class="d" style="color:var(--dim)">Loading…</div></div>
|
||||
<div class="note">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 '<div class="d" style="color:var(--acid);margin-top:4px">↻ Update <b>v' + escapeHtml(st.version) + '</b> staged — restart Theseus to apply.</div>';
|
||||
}
|
||||
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 '<div class="d" style="' + cls + ';margin-top:4px">' + msg + '</div>';
|
||||
}
|
||||
function renderAddons(snap) {
|
||||
const items = (snap && snap.installed) || [];
|
||||
if (!items.length) {
|
||||
|
|
@ -1269,7 +1292,7 @@
|
|||
const iconHtml = /^data:image\//i.test(a.icon || "")
|
||||
? '<img src="' + escapeAttr(a.icon) + '" alt="" style="width:16px;height:16px;vertical-align:middle;margin-right:4px">'
|
||||
: escapeHtml(a.icon || "•");
|
||||
return '<div class="row"><div class="txt"><div class="t">' + iconHtml + ' ' + escapeHtml(a.name) + builtInBadge + ' <span style="color:var(--dim);font-weight:400">v' + escapeHtml(a.version) + '</span>' + caps + '</div><div class="d">' + escapeHtml(a.description || "") + (a.author ? ' <span style="color:var(--dim)">— ' + escapeHtml(a.author) + '</span>' : '') + '</div></div><div style="display:flex;gap:8px;align-items:center"><button class="btn" data-reveal="' + escapeAttr(a.folder) + '">Show folder</button><label class="sw"><input type="checkbox" data-toggle="' + escapeAttr(a.id) + '" ' + (a.enabled ? "checked" : "") + '><span class="track"><span class="knob"></span></span></label></div></div>';
|
||||
return '<div class="row"><div class="txt"><div class="t">' + iconHtml + ' ' + escapeHtml(a.name) + builtInBadge + ' <span style="color:var(--dim);font-weight:400">v' + escapeHtml(a.version) + '</span>' + caps + '</div><div class="d">' + escapeHtml(a.description || "") + (a.author ? ' <span style="color:var(--dim)">— ' + escapeHtml(a.author) + '</span>' : '') + '</div>' + updateLineFor(a) + '</div><div style="display:flex;gap:8px;align-items:center"><button class="btn" data-reveal="' + escapeAttr(a.folder) + '">Show folder</button><label class="sw"><input type="checkbox" data-toggle="' + escapeAttr(a.id) + '" ' + (a.enabled ? "checked" : "") + '><span class="track"><span class="knob"></span></span></label></div></div>';
|
||||
};
|
||||
const sectionHeader = (label, hint) =>
|
||||
'<div class="d" style="color:var(--dim);margin:12px 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.04em">' + escapeHtml(label) + '</div>'
|
||||
|
|
@ -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 <userData>/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) => '<div>' + escapeHtml(s.name) + ' → <b>' + escapeHtml(s.version) + '</b></div>').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 {
|
||||
// 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 {
|
||||
// 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");
|
||||
status.textContent = "All extensions up to date.";
|
||||
}
|
||||
return '<div><b>' + label + '</b> — ' + msg + '</div>';
|
||||
}).join("");
|
||||
status.innerHTML = rows;
|
||||
}
|
||||
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(); })();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue