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
113
settings.html
113
settings.html
|
|
@ -602,11 +602,6 @@
|
||||||
<button id="addonsOpenDir" class="btn" type="button">Open extensions folder</button>
|
<button id="addonsOpenDir" class="btn" type="button">Open extensions folder</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="addonsUpdStatus" class="pmuted" style="font-size:12.5px;margin-top:6px;text-align:right">—</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>
|
<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 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.
|
<div class="note">Extensions run with full app access — treat installing one like installing an unsigned executable.
|
||||||
|
|
@ -1242,6 +1237,34 @@
|
||||||
|
|
||||||
// ---- Add-ons management ----
|
// ---- Add-ons management ----
|
||||||
const addonsList = document.getElementById("addonsList");
|
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) {
|
function renderAddons(snap) {
|
||||||
const items = (snap && snap.installed) || [];
|
const items = (snap && snap.installed) || [];
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
|
|
@ -1269,7 +1292,7 @@
|
||||||
const iconHtml = /^data:image\//i.test(a.icon || "")
|
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">'
|
? '<img src="' + escapeAttr(a.icon) + '" alt="" style="width:16px;height:16px;vertical-align:middle;margin-right:4px">'
|
||||||
: escapeHtml(a.icon || "•");
|
: 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) =>
|
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>'
|
'<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()); }
|
try { renderAddons(await C.listAddons()); }
|
||||||
catch (e) { addonsList.textContent = "Failed to load extensions: " + (e?.message || e); }
|
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 () => {
|
document.getElementById("addonsReload").addEventListener("click", async () => {
|
||||||
await C.reloadAddons(); loadAddons();
|
await C.reloadAddons(); await loadAddonUpdates(); loadAddons();
|
||||||
});
|
});
|
||||||
document.getElementById("addonsOpenDir").addEventListener("click", () => C.openAddonsDir());
|
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
|
// 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
|
// doesn't display stale "checking…" text if the background poll finished
|
||||||
// while another section was open. Aegis card loads on page-init and its
|
// while another section was open. Aegis card loads on page-init and its
|
||||||
|
|
@ -1311,19 +1345,9 @@
|
||||||
if (ar) ar.click();
|
if (ar) ar.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Manual "Check for updates" for extensions — runs the same signed-update
|
// (Old separate "Pending updates" strip lived here. Per-card update
|
||||||
// polling the boot timer runs, and surfaces what's staged for the next
|
// line is now painted by updateLineFor() inside each addon row —
|
||||||
// launch. Staged folders live in <userData>/addons-updates-staged/ and
|
// loadAddonUpdates() populates addonUpdates.staged before renderAddons.)
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
document.getElementById("addonsCheckUpdates").addEventListener("click", async () => {
|
document.getElementById("addonsCheckUpdates").addEventListener("click", async () => {
|
||||||
const btn = document.getElementById("addonsCheckUpdates");
|
const btn = document.getElementById("addonsCheckUpdates");
|
||||||
const status = document.getElementById("addonsUpdStatus");
|
const status = document.getElementById("addonsUpdStatus");
|
||||||
|
|
@ -1337,42 +1361,35 @@
|
||||||
const staged = Array.isArray(res) ? res : (res?.staged || []);
|
const staged = Array.isArray(res) ? res : (res?.staged || []);
|
||||||
if (skipped === "no-pubkeys") {
|
if (skipped === "no-pubkeys") {
|
||||||
status.textContent = "Update endpoint disabled — no operator pubkey baked into this build.";
|
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 {
|
} else {
|
||||||
// Show per-addon status so "no update" is never mistaken for a
|
// Fold the per-addon report into addonUpdates.report so the
|
||||||
// silent fetch failure.
|
// per-card update line reflects the freshest check. A summary at
|
||||||
const rows = report.map((r) => {
|
// the top counts staged/updated vs. up-to-date, but the per-addon
|
||||||
const label = escapeHtml(r.id) + " " + escapeHtml(r.currentVer || "?");
|
// detail lives on each card.
|
||||||
let msg = "";
|
addonUpdates.report = {};
|
||||||
switch (r.status) {
|
for (const r of report) addonUpdates.report[r.id] = r;
|
||||||
case "up-to-date": msg = "up to date"; break;
|
const stagedNow = report.filter((r) => r.status === "staged" || r.status === "already-staged").length;
|
||||||
case "no-update-url": msg = "no updateURL declared"; break;
|
const failed = report.filter((r) => ["fetch-failed","signature-invalid","sha256-mismatch","extract-failed","manifest-mismatch"].includes(r.status)).length;
|
||||||
case "fetch-failed": msg = "fetch failed — " + escapeHtml(r.detail || "network"); break;
|
if (stagedNow) {
|
||||||
case "signature-invalid": msg = "endpoint offered " + escapeHtml(r.newVer || "?") + " with a BAD signature — rejected"; break;
|
status.textContent = stagedNow + " update" + (stagedNow > 1 ? "s" : "") + " staged; restart Theseus to apply.";
|
||||||
case "sha256-mismatch": msg = "endpoint offered " + escapeHtml(r.newVer || "?") + " but its tarball hash didn't match"; break;
|
} else if (failed) {
|
||||||
case "extract-failed": msg = "extract failed — " + escapeHtml(r.detail || ""); break;
|
status.textContent = failed + " failed — see the extension card" + (failed > 1 ? "s" : "") + " below.";
|
||||||
case "manifest-mismatch": msg = "extracted manifest didn't match signed values"; break;
|
} else if (!report.length) {
|
||||||
case "already-staged": msg = escapeHtml(r.newVer || "?") + " already staged; restart to apply"; break;
|
status.textContent = "No extensions with an update endpoint.";
|
||||||
case "staged": msg = "staged " + escapeHtml(r.newVer || "?") + "; restart to apply"; break;
|
} else {
|
||||||
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) {
|
} catch (e) {
|
||||||
status.textContent = "Check failed: " + (e?.message || e);
|
status.textContent = "Check failed: " + (e?.message || e);
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false; btn.textContent = orig;
|
btn.disabled = false; btn.textContent = orig;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
loadStagedAddonUpdates();
|
|
||||||
// Populate on first paint so the tab is ready when the user clicks in.
|
// Populate on first paint so the tab is ready when the user clicks in.
|
||||||
loadAddons();
|
(async () => { await loadAddonUpdates(); loadAddons(); })();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue