feat(theseus/ariadne): settings panel — policy + per-source toggles + status report
Ariadne 0.1.13 exposed /api/status and per-source enable flags in
policy.json. Theseus's Plug-ins > Ariadne's Thread sub-page now wires those
into a full UI, no daemon restart, no UAC.
Added to the plugins-ariadne sub-page (after Status, before Remove):
Collision policy -- radio group (BCNR-first / ICANN-first) writes
C:\ProgramData\Ariadne\policy.json.policy; hot-reloaded
by the daemon within 5 s.
Sources -- 3-column grid, one row per source (snapshotHttps,
electrumWss, perQueryLookup, diskCache, localApi):
enable checkbox + last-state summary
(last success / last error / hit-miss counters /
disk-cache size+mtime). Toggle writes
policy.json.sources.<name>.enabled and re-polls after
the 5-s hot-reload tick so the state text catches up.
Status report -- <pre> JSON dump of GET http://127.0.0.1/api/status
with Copy report + Refresh report buttons. This is
the paste-me-into-support artefact for any diagnosis.
IPC wiring:
main.js
ariadne-get-status -> GET http://127.0.0.1/api/status ({ok, status|error})
ariadne-get-policy -> read C:\ProgramData\Ariadne\policy.json (or {})
ariadne-set-policy -> merge {policy}, write back (validates enum)
ariadne-set-source -> merge {sources.<name>.enabled}, write back
(validates against the known 5 names)
settings-preload.js
ariadneGetStatus, ariadneGetPolicy, ariadneSetPolicy, ariadneSetSource
All four handlers write policy.json as the local user; no UAC. Works because
install.ps1 grants BUILTIN\Users Modify on the file (0.1.7+).
Sub-page auto-refreshes state every time it opens (listens on the existing
'section' custom event dispatched by showSection).
Not building/shipping Theseus here -- this rides the next Theseus release.
Panel gracefully handles: daemon down (shows 'Daemon unreachable' with a
pointer to the Status toggle), localApi disabled (daemon returns 503, panel
shows the error), missing policy.json (all sources default to true).
This commit is contained in:
parent
a3fb8917c3
commit
596ea09fc7
3 changed files with 195 additions and 0 deletions
60
main.js
60
main.js
|
|
@ -5623,6 +5623,66 @@ ipcMain.handle("ariadne-toggle", (_e, on) => ariadneSetState(!!on).catch((e) =>
|
||||||
ipcMain.handle("ariadne-install", () => ariadneInstall().then(() => ({ ok: true })).catch((e) => ({ ok: false, error: e?.message || String(e) })));
|
ipcMain.handle("ariadne-install", () => ariadneInstall().then(() => ({ ok: true })).catch((e) => ({ ok: false, error: e?.message || String(e) })));
|
||||||
ipcMain.handle("ariadne-update", () => ariadneUpdate().then(() => ({ ok: true })).catch((e) => ({ ok: false, error: e?.message || String(e) })));
|
ipcMain.handle("ariadne-update", () => ariadneUpdate().then(() => ({ ok: true })).catch((e) => ({ ok: false, error: e?.message || String(e) })));
|
||||||
ipcMain.handle("ariadne-uninstall", () => ariadneUninstall().then(() => ({ ok: true })).catch((e) => ({ ok: false, error: e?.message || String(e) })));
|
ipcMain.handle("ariadne-uninstall", () => ariadneUninstall().then(() => ({ ok: true })).catch((e) => ({ ok: false, error: e?.message || String(e) })));
|
||||||
|
|
||||||
|
// ---- Ariadne 0.1.13+ settings + status wiring --------------------------
|
||||||
|
// The daemon's own read/write surface lives at http://127.0.0.1/api/status
|
||||||
|
// and C:\ProgramData\Ariadne\policy.json. The policy file is ACL'd user-
|
||||||
|
// writable by install.ps1, so all four of these run WITHOUT UAC.
|
||||||
|
const ARIADNE_POLICY_FILE = (() => {
|
||||||
|
const dir = process.env.PROGRAMDATA || "C:\\ProgramData";
|
||||||
|
return path.join(dir, "Ariadne", "policy.json");
|
||||||
|
})();
|
||||||
|
function ariadneReadPolicyFile() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(ARIADNE_POLICY_FILE)) return {};
|
||||||
|
const raw = fs.readFileSync(ARIADNE_POLICY_FILE, "utf8").replace(/^\uFEFF/, "");
|
||||||
|
return JSON.parse(raw) || {};
|
||||||
|
} catch { return {}; }
|
||||||
|
}
|
||||||
|
function ariadneWritePolicyFile(obj) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(ARIADNE_POLICY_FILE), { recursive: true });
|
||||||
|
fs.writeFileSync(ARIADNE_POLICY_FILE, JSON.stringify(obj, null, 2), "utf8");
|
||||||
|
return { ok: true };
|
||||||
|
} catch (e) { return { ok: false, error: e?.message || String(e) }; }
|
||||||
|
}
|
||||||
|
// GET http://127.0.0.1/api/status against the local daemon. Returns the full
|
||||||
|
// status document, or {ok:false, error} on any failure (daemon down, port
|
||||||
|
// blocked, localApi turned off in policy.json -> the daemon returns 503).
|
||||||
|
function ariadneFetchStatus() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const req = http.request({ host: "127.0.0.1", port: 80, path: "/api/status", method: "GET", headers: { "user-agent": "TheseusNavigator/ariadne-panel" } }, (r) => {
|
||||||
|
const chunks = [];
|
||||||
|
r.on("data", (c) => chunks.push(c));
|
||||||
|
r.on("end", () => {
|
||||||
|
if (r.statusCode !== 200) return resolve({ ok: false, error: `daemon HTTP ${r.statusCode}`, body: Buffer.concat(chunks).toString("utf8").slice(0, 400) });
|
||||||
|
try { resolve({ ok: true, status: JSON.parse(Buffer.concat(chunks).toString("utf8")) }); }
|
||||||
|
catch (e) { resolve({ ok: false, error: "malformed JSON from daemon: " + e.message }); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.setTimeout(4000, () => req.destroy(new Error("daemon timeout on 127.0.0.1/api/status")));
|
||||||
|
req.on("error", (e) => resolve({ ok: false, error: e.message }));
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ipcMain.handle("ariadne-get-status", () => ariadneFetchStatus());
|
||||||
|
ipcMain.handle("ariadne-get-policy", () => ({ ok: true, policy: ariadneReadPolicyFile() }));
|
||||||
|
ipcMain.handle("ariadne-set-policy", (_e, value) => {
|
||||||
|
const v = String(value || "").toLowerCase();
|
||||||
|
if (!["bcnr-first", "icann-first"].includes(v)) return { ok: false, error: `invalid policy '${value}' (expected bcnr-first or icann-first)` };
|
||||||
|
const cur = ariadneReadPolicyFile();
|
||||||
|
cur.policy = v;
|
||||||
|
return ariadneWritePolicyFile(cur);
|
||||||
|
});
|
||||||
|
ipcMain.handle("ariadne-set-source", (_e, name, enabled) => {
|
||||||
|
const known = ["snapshotHttps", "electrumWss", "perQueryLookup", "diskCache", "localApi"];
|
||||||
|
if (!known.includes(String(name || ""))) return { ok: false, error: `unknown source '${name}' (known: ${known.join(", ")})` };
|
||||||
|
const cur = ariadneReadPolicyFile();
|
||||||
|
if (!cur.sources || typeof cur.sources !== "object") cur.sources = {};
|
||||||
|
if (!cur.sources[name] || typeof cur.sources[name] !== "object") cur.sources[name] = {};
|
||||||
|
cur.sources[name].enabled = !!enabled;
|
||||||
|
return ariadneWritePolicyFile(cur);
|
||||||
|
});
|
||||||
// Storage: clear right now (any subset). "history" also drops the saved-session file.
|
// Storage: clear right now (any subset). "history" also drops the saved-session file.
|
||||||
// ---- Password vault -------------------------------------------------------
|
// ---- Password vault -------------------------------------------------------
|
||||||
// The vault lives at userData/passwords.vault (encrypted). Unlock state is
|
// The vault lives at userData/passwords.vault (encrypted). Unlock state is
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,13 @@ contextBridge.exposeInMainWorld("cfg", {
|
||||||
ariadneInstall: () => ipcRenderer.invoke("ariadne-install"),
|
ariadneInstall: () => ipcRenderer.invoke("ariadne-install"),
|
||||||
ariadneUpdate: () => ipcRenderer.invoke("ariadne-update"),
|
ariadneUpdate: () => ipcRenderer.invoke("ariadne-update"),
|
||||||
ariadneUninstall: () => ipcRenderer.invoke("ariadne-uninstall"),
|
ariadneUninstall: () => ipcRenderer.invoke("ariadne-uninstall"),
|
||||||
|
// Local BNS daemon settings + status (0.1.13+). All read/write against
|
||||||
|
// C:\ProgramData\Ariadne\policy.json (user-writable ACL) and the daemon's
|
||||||
|
// own /api/status endpoint on 127.0.0.1. No UAC required for any of these.
|
||||||
|
ariadneGetStatus: () => ipcRenderer.invoke("ariadne-get-status"),
|
||||||
|
ariadneGetPolicy: () => ipcRenderer.invoke("ariadne-get-policy"),
|
||||||
|
ariadneSetPolicy: (policy) => ipcRenderer.invoke("ariadne-set-policy", String(policy || "")),
|
||||||
|
ariadneSetSource: (name, enabled) => ipcRenderer.invoke("ariadne-set-source", String(name || ""), !!enabled),
|
||||||
// Manual "Check for updates" — un-dismisses any existing chip and re-
|
// Manual "Check for updates" — un-dismisses any existing chip and re-
|
||||||
// fetches the release manifest. Returns { updateAvailable, currentVersion }.
|
// fetches the release manifest. Returns { updateAvailable, currentVersion }.
|
||||||
recheckUpdate: () => ipcRenderer.invoke("recheck-update"),
|
recheckUpdate: () => ipcRenderer.invoke("recheck-update"),
|
||||||
|
|
|
||||||
128
settings.html
128
settings.html
|
|
@ -105,6 +105,16 @@
|
||||||
.polrow{display:flex;align-items:center;gap:10px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer}
|
.polrow{display:flex;align-items:center;gap:10px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer}
|
||||||
.polrow:hover{background:#141c28}
|
.polrow:hover{background:#141c28}
|
||||||
.polrow input{accent-color:var(--acid)}
|
.polrow input{accent-color:var(--acid)}
|
||||||
|
/* Ariadne sources table -- 3-col grid instead of <table> so it inherits our row styling and reflows nicely. */
|
||||||
|
.ariadne-sources{display:flex;flex-direction:column;gap:1px;background:var(--line);border:1px solid var(--line);border-radius:8px;overflow:hidden;font-size:13px}
|
||||||
|
.ariadne-sources .asrc-head, .ariadne-sources .asrc-row{display:grid;grid-template-columns:1fr 70px minmax(180px,2fr);align-items:center;gap:12px;padding:8px 12px;background:#10151f}
|
||||||
|
.ariadne-sources .asrc-head{font-weight:600;color:var(--muted);background:#0c1119;font-size:12px;text-transform:uppercase;letter-spacing:.4px}
|
||||||
|
.ariadne-sources .asrc-row .aname{color:var(--ink);font-weight:500}
|
||||||
|
.ariadne-sources .asrc-row .aname small{display:block;color:var(--muted);font-weight:400;margin-top:2px;font-size:11.5px}
|
||||||
|
.ariadne-sources .asrc-row .astate{color:var(--muted);font-size:11.5px;font-family:ui-monospace,'SF Mono',Menlo,Consolas,monospace}
|
||||||
|
.ariadne-sources .asrc-row .astate.ok{color:var(--acid-text)}
|
||||||
|
.ariadne-sources .asrc-row .astate.err{color:#f6768a}
|
||||||
|
.ariadne-sources input[type=checkbox]{accent-color:var(--acid);width:18px;height:18px;cursor:pointer}
|
||||||
.pmuted{color:var(--mut)}
|
.pmuted{color:var(--mut)}
|
||||||
.ceng{display:flex;align-items:center;gap:8px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:6px 10px;margin-bottom:6px;font-size:13px}
|
.ceng{display:flex;align-items:center;gap:8px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:6px 10px;margin-bottom:6px;font-size:13px}
|
||||||
.ceng .cs{font-size:14px;flex:none}
|
.ceng .cs{font-size:14px;flex:none}
|
||||||
|
|
@ -217,6 +227,12 @@
|
||||||
hover tints keyed to BCH dark for consistent depth. */
|
hover tints keyed to BCH dark for consistent depth. */
|
||||||
.polrow{ background:#f4f8fb; color:#253A49; }
|
.polrow{ background:#f4f8fb; color:#253A49; }
|
||||||
.polrow:hover{ background:#eaf0f5; }
|
.polrow:hover{ background:#eaf0f5; }
|
||||||
|
.ariadne-sources{background:#e6ecf1;border-color:#e6ecf1}
|
||||||
|
.ariadne-sources .asrc-head, .ariadne-sources .asrc-row{background:#f4f8fb;color:#253A49}
|
||||||
|
.ariadne-sources .asrc-head{background:#eaf0f5;color:#5a7080}
|
||||||
|
.ariadne-sources .asrc-row .aname{color:#253A49}
|
||||||
|
.ariadne-sources .asrc-row .aname small{color:#5a7080}
|
||||||
|
#ariadneStatusJson{background:#f4f8fb !important;color:#253A49}
|
||||||
.polrow input:checked ~ span,
|
.polrow input:checked ~ span,
|
||||||
.polrow:has(input:checked){ background:rgb(from var(--acid) r g b / .10); border-color:color-mix(in srgb, var(--acid) 35%, transparent); }
|
.polrow:has(input:checked){ background:rgb(from var(--acid) r g b / .10); border-color:color-mix(in srgb, var(--acid) 35%, transparent); }
|
||||||
.segseg{ background:#eef1f6; }
|
.segseg{ background:#eef1f6; }
|
||||||
|
|
@ -398,6 +414,28 @@
|
||||||
<button id="ariadneRefreshSub" class="btn">Refresh</button>
|
<button id="ariadneRefreshSub" class="btn">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2 class="sub">Collision policy</h2>
|
||||||
|
<p class="subd">When a name exists on both BCNR and ICANN, which one wins? Applies machine-wide — every browser sees the same answer. Change is picked up within 5 seconds; no daemon restart, no UAC.</p>
|
||||||
|
<div class="row" style="flex-direction:column;align-items:stretch;gap:6px">
|
||||||
|
<label class="polrow"><input type="radio" name="ariadnePolicy" value="bcnr-first"><span><b>BCNR first</b> <span class="pmuted">— use BCNR when the name is on chain; forward to ICANN on miss. Default.</span></span></label>
|
||||||
|
<label class="polrow"><input type="radio" name="ariadnePolicy" value="icann-first"><span><b>ICANN first</b> <span class="pmuted">— forward to ICANN; use BCNR only when ICANN returns NXDOMAIN.</span></span></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="sub">Sources</h2>
|
||||||
|
<p class="subd">Each index source can be toggled independently to isolate a path or measure without it. Changes hot-reload every 5 s via <code>C:\ProgramData\Ariadne\policy.json</code> — no restart. State column shows the last attempt result for each.</p>
|
||||||
|
<div class="ariadne-sources" role="table" aria-label="Ariadne index sources">
|
||||||
|
<div class="asrc-head" role="row"><span>Source</span><span>Enabled</span><span>State</span></div>
|
||||||
|
<div id="ariadneSourcesBody"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="sub">Status report</h2>
|
||||||
|
<p class="subd">Full snapshot of what the daemon currently sees — daemon version, permitted TLDs, index size, last fetch per source. Paste into a support conversation.</p>
|
||||||
|
<div class="row" style="justify-content:flex-end;gap:8px">
|
||||||
|
<button id="ariadneStatusCopy" class="btn">Copy report</button>
|
||||||
|
<button id="ariadneStatusRefresh" class="btn">Refresh report</button>
|
||||||
|
</div>
|
||||||
|
<pre id="ariadneStatusJson" style="max-height:320px;overflow:auto;background:rgba(0,0,0,.25);padding:12px;border-radius:6px;font-size:11.5px;font-family:ui-monospace,'SF Mono',Menlo,Consolas,monospace;line-height:1.45;margin:8px 0 0">Loading…</pre>
|
||||||
|
|
||||||
<h2 class="sub">Remove</h2>
|
<h2 class="sub">Remove</h2>
|
||||||
<p class="subd">Uninstall the resolver from this machine. Theseus itself keeps working — its built-in resolver isn't affected.</p>
|
<p class="subd">Uninstall the resolver from this machine. Theseus itself keeps working — its built-in resolver isn't affected.</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
@ -1547,6 +1585,96 @@
|
||||||
arRefreshSub.onclick = refreshAriadne;
|
arRefreshSub.onclick = refreshAriadne;
|
||||||
refreshAriadne();
|
refreshAriadne();
|
||||||
|
|
||||||
|
// ---- Ariadne 0.1.13+ sub-page: policy + sources + status report -----
|
||||||
|
// All read/write against the daemon's /api/status (over 127.0.0.1) and
|
||||||
|
// C:\ProgramData\Ariadne\policy.json (user-writable ACL) -- no UAC.
|
||||||
|
const arPolicyRadios = document.querySelectorAll('input[name="ariadnePolicy"]');
|
||||||
|
const arSourcesBody = q("ariadneSourcesBody");
|
||||||
|
const arStatusJson = q("ariadneStatusJson");
|
||||||
|
const arStatusCopy = q("ariadneStatusCopy");
|
||||||
|
const arStatusRefr = q("ariadneStatusRefresh");
|
||||||
|
const SOURCE_DESCRIPTIONS = {
|
||||||
|
snapshotHttps: { label: "HTTPS snapshot (VPS + Sia)", hint: "dl.silentmode.st + s3.silentmode.st, polled every 30 s with ETag" },
|
||||||
|
electrumWss: { label: "Electrum WSS (chipnet)", hint: "direct chain read via WebSocket; secondary refresh" },
|
||||||
|
perQueryLookup: { label: "Per-query indexer fallback", hint: "navigate.st/api/name/<host> on index miss, 30-s LRU" },
|
||||||
|
diskCache: { label: "Disk cache", hint: "C:\\ProgramData\\Ariadne\\bns-name-snapshot.json — read at boot, written on any HTTPS win" },
|
||||||
|
localApi: { label: "Local BNS indexer API", hint: "http://127.0.0.1/api/tlds, /api/name/:n, /api/registry, /api/status" },
|
||||||
|
};
|
||||||
|
function fmtTime(iso) {
|
||||||
|
if (!iso) return "never";
|
||||||
|
try { const d = new Date(iso); const s = Math.round((Date.now() - d.getTime()) / 1000); return s < 60 ? `${s} s ago` : s < 3600 ? `${Math.round(s/60)} m ago` : d.toLocaleString(); } catch { return String(iso); }
|
||||||
|
}
|
||||||
|
function sourceState(name, st) {
|
||||||
|
if (!st) return { text: "no state yet", ok: false, err: false };
|
||||||
|
if (name === "diskCache") return { text: st.exists ? `${(st.size||0).toLocaleString()} B · ${fmtTime(st.mtime)}` : "no file yet", ok: !!st.exists, err: false };
|
||||||
|
if (name === "localApi") return { text: st.enabled ? "listening" : "disabled", ok: !!st.enabled, err: false };
|
||||||
|
if (name === "perQueryLookup") return { text: `${st.hits||0} hit · ${st.misses||0} miss · ${st.errors||0} err · last ${fmtTime(st.lastAttempt)}`, ok: (st.hits||0) > 0 || !st.lastAttempt, err: (st.errors||0) > 0 };
|
||||||
|
if (st.lastError) return { text: `error: ${st.lastError.slice(0, 80)} · last ${fmtTime(st.lastAttempt)}`, ok: false, err: true };
|
||||||
|
if (st.lastSuccess) return { text: `ok · last success ${fmtTime(st.lastSuccess)}` + (st.refreshes ? ` · ${st.refreshes} refresh${st.refreshes>1?"es":""}` : ""), ok: true, err: false };
|
||||||
|
return { text: `waiting · last try ${fmtTime(st.lastAttempt)}`, ok: false, err: false };
|
||||||
|
}
|
||||||
|
let arLastStatus = null;
|
||||||
|
async function refreshAriadneStatus() {
|
||||||
|
arStatusJson.textContent = "Loading…";
|
||||||
|
arSourcesBody.innerHTML = "";
|
||||||
|
const r = await C.ariadneGetStatus();
|
||||||
|
if (!r || !r.ok) {
|
||||||
|
arStatusJson.textContent = `Cannot reach the daemon: ${r?.error || "unknown error"}\n\nIs Ariadne's Thread running? Turn it on from the Status section above.`;
|
||||||
|
arSourcesBody.innerHTML = '<div class="asrc-row"><span class="aname" style="grid-column:1 / -1;color:var(--muted)">Daemon unreachable — start it from Status above.</span></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
arLastStatus = r.status;
|
||||||
|
arStatusJson.textContent = JSON.stringify(r.status, null, 2);
|
||||||
|
const sources = r.status.sources || {};
|
||||||
|
arSourcesBody.innerHTML = "";
|
||||||
|
for (const name of Object.keys(SOURCE_DESCRIPTIONS)) {
|
||||||
|
const d = SOURCE_DESCRIPTIONS[name];
|
||||||
|
const st = sources[name] || {};
|
||||||
|
const s = sourceState(name, st);
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "asrc-row";
|
||||||
|
row.innerHTML = `<span class="aname">${d.label}<small>${d.hint}</small></span>` +
|
||||||
|
`<span><input type="checkbox" data-src="${name}" ${st.enabled ? "checked" : ""}></span>` +
|
||||||
|
`<span class="astate ${s.ok ? "ok" : s.err ? "err" : ""}">${s.text}</span>`;
|
||||||
|
arSourcesBody.appendChild(row);
|
||||||
|
}
|
||||||
|
// Wire the enable checkboxes AFTER they're in the DOM.
|
||||||
|
arSourcesBody.querySelectorAll('input[type="checkbox"][data-src]').forEach((cb) => {
|
||||||
|
cb.addEventListener("change", async () => {
|
||||||
|
cb.disabled = true;
|
||||||
|
const w = await C.ariadneSetSource(cb.dataset.src, cb.checked);
|
||||||
|
cb.disabled = false;
|
||||||
|
if (!w?.ok) { alert(`Could not save: ${w?.error || "unknown error"}`); cb.checked = !cb.checked; return; }
|
||||||
|
// Poll again after the daemon's 5-s hot-reload tick so state text catches up.
|
||||||
|
setTimeout(refreshAriadneStatus, 6000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function loadAriadnePolicy() {
|
||||||
|
const r = await C.ariadneGetPolicy();
|
||||||
|
const p = (r?.policy?.policy || "bcnr-first").toLowerCase();
|
||||||
|
arPolicyRadios.forEach((rb) => { rb.checked = (rb.value === p); });
|
||||||
|
}
|
||||||
|
arPolicyRadios.forEach((rb) => {
|
||||||
|
rb.addEventListener("change", async () => {
|
||||||
|
if (!rb.checked) return;
|
||||||
|
const w = await C.ariadneSetPolicy(rb.value);
|
||||||
|
if (!w?.ok) alert(`Could not save policy: ${w?.error || "unknown error"}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
arStatusRefr.onclick = refreshAriadneStatus;
|
||||||
|
arStatusCopy.onclick = async () => {
|
||||||
|
if (!arLastStatus) return;
|
||||||
|
try { await navigator.clipboard.writeText(JSON.stringify(arLastStatus, null, 2)); arStatusCopy.textContent = "Copied"; setTimeout(() => (arStatusCopy.textContent = "Copy report"), 1500); }
|
||||||
|
catch { arStatusCopy.textContent = "Copy failed"; setTimeout(() => (arStatusCopy.textContent = "Copy report"), 1500); }
|
||||||
|
};
|
||||||
|
// Load once on script boot; also every time the user navigates INTO the
|
||||||
|
// Ariadne sub-page (fresh state, no cached-stale JSON on re-entry).
|
||||||
|
loadAriadnePolicy(); refreshAriadneStatus();
|
||||||
|
document.addEventListener("section", (e) => {
|
||||||
|
if (e.detail === "plugins/ariadne") { loadAriadnePolicy(); refreshAriadneStatus(); }
|
||||||
|
});
|
||||||
|
|
||||||
// AEGIS ——————————————————————————————————————————————————————————
|
// AEGIS ——————————————————————————————————————————————————————————
|
||||||
// Bundled add-on shipped OTA. Same addons-check-updates IPC the
|
// Bundled add-on shipped OTA. Same addons-check-updates IPC the
|
||||||
// Extensions page uses, filtered for the aegis id. Legacy id
|
// Extensions page uses, filtered for the aegis id. Legacy id
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue