treefmt: apply prettier
Pure `nix fmt` output from the commit before this one — no hand edits. 203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs. Reproduce with `nix develop -c nix fmt` on the parent commit; the result should be byte-identical to this tree. None of the 13 `.prettierignore` entries appears here — verified by intersecting the changed-file list against the ignore file, with a control proving the intersection finds a match when one exists.
This commit is contained in:
parent
5d24bedd60
commit
39b95c2ede
203 changed files with 10090 additions and 6085 deletions
|
|
@ -19,11 +19,11 @@
|
|||
// themselves and pastes it in, same trust model as GITHUB.
|
||||
// Per-tab detail comments live next to their section below.
|
||||
|
||||
import { $, esc, fmtAgeSecs, renderServerWarnings } from './common.js';
|
||||
import { el } from '@hive/shared/dom.js';
|
||||
import '@hive/shared/hive-tab-strip.js';
|
||||
import { themedConfirm, themedToast } from '@hive/shared/modal.js';
|
||||
import { readApiError, problemMessage } from '@hive/shared/api-error.js';
|
||||
import { $, esc, fmtAgeSecs, renderServerWarnings } from "./common.js";
|
||||
import { el } from "@hive/shared/dom.js";
|
||||
import "@hive/shared/hive-tab-strip.js";
|
||||
import { themedConfirm, themedToast } from "@hive/shared/modal.js";
|
||||
import { readApiError, problemMessage } from "@hive/shared/api-error.js";
|
||||
|
||||
let agents = [];
|
||||
// agent name → container running (bool), from /api/state. Cross-referenced by
|
||||
|
|
@ -35,7 +35,7 @@ const containerRunning = new Map();
|
|||
|
||||
async function loadState() {
|
||||
try {
|
||||
const resp = await fetch('/api/state');
|
||||
const resp = await fetch("/api/state");
|
||||
if (!resp.ok) return;
|
||||
const s = await resp.json();
|
||||
renderServerWarnings(s.server_warnings);
|
||||
|
|
@ -43,7 +43,7 @@ async function loadState() {
|
|||
// object carrying `.name` + `.running`); there is no top-level `agents`
|
||||
// field, so the picker stays compatible with both string + object shapes.
|
||||
const containers = (s.containers || [])
|
||||
.map((a) => (typeof a === 'string' ? { name: a } : a))
|
||||
.map((a) => (typeof a === "string" ? { name: a } : a))
|
||||
.filter((c) => c && c.name);
|
||||
agents = containers.map((c) => c.name).sort();
|
||||
containerRunning.clear();
|
||||
|
|
@ -56,14 +56,14 @@ async function loadState() {
|
|||
}
|
||||
|
||||
function renderAgentPicker() {
|
||||
const sel = $('ma-agent');
|
||||
const sel = $("ma-agent");
|
||||
sel.replaceChildren();
|
||||
if (!agents.length) {
|
||||
sel.append(el('option', { value: '' }, '— no agents —'));
|
||||
sel.append(el("option", { value: "" }, "— no agents —"));
|
||||
return;
|
||||
}
|
||||
sel.append(el('option', { value: '' }, '— select agent —'));
|
||||
for (const a of agents) sel.append(el('option', { value: a }, a));
|
||||
sel.append(el("option", { value: "" }, "— select agent —"));
|
||||
for (const a of agents) sel.append(el("option", { value: a }, a));
|
||||
}
|
||||
|
||||
// Shape-agnostic error-body parsing (shared by both tabs' submit handlers)
|
||||
|
|
@ -86,91 +86,116 @@ function renderAgentPicker() {
|
|||
// v1 backend (no `live` field) falls back to token-present rendering.
|
||||
|
||||
async function loadAccounts(agent) {
|
||||
const list = $('ma-list');
|
||||
const list = $("ma-list");
|
||||
if (!agent) {
|
||||
list.replaceChildren(el('p', { class: 'meta' }, 'select an agent to see its matrix accounts.'));
|
||||
list.replaceChildren(
|
||||
el("p", { class: "meta" }, "select an agent to see its matrix accounts."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
list.replaceChildren(el('p', { class: 'meta' }, 'loading…'));
|
||||
list.replaceChildren(el("p", { class: "meta" }, "loading…"));
|
||||
let data;
|
||||
try {
|
||||
const resp = await fetch('/api/matrix-accounts?agent=' + encodeURIComponent(agent));
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
const resp = await fetch(
|
||||
"/api/matrix-accounts?agent=" + encodeURIComponent(agent),
|
||||
);
|
||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
||||
data = await resp.json();
|
||||
} catch (err) {
|
||||
list.replaceChildren(el('p', { class: 'err' },
|
||||
'could not load accounts: ' + esc(String(err)) + ' (the backend endpoint may not be deployed yet).'));
|
||||
list.replaceChildren(
|
||||
el(
|
||||
"p",
|
||||
{ class: "err" },
|
||||
"could not load accounts: " +
|
||||
esc(String(err)) +
|
||||
" (the backend endpoint may not be deployed yet).",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const accounts = data.accounts || [];
|
||||
const asOf = typeof data.as_of_unix === 'number' ? data.as_of_unix : null;
|
||||
const asOf = typeof data.as_of_unix === "number" ? data.as_of_unix : null;
|
||||
// `false` only when the container is explicitly down; `undefined` (unknown,
|
||||
// e.g. a failed /api/state read) is treated as not-down so we never flag a
|
||||
// false stale.
|
||||
const running = containerRunning.get(agent);
|
||||
// The daemon force-rewrites its snapshot every ~30s, so `as_of_unix` advances
|
||||
// while it's alive — this is a heartbeat, and a stalled value is meaningful.
|
||||
const ageSecs = asOf != null
|
||||
? Math.max(0, Math.floor(Date.now() / 1000) - asOf)
|
||||
: null;
|
||||
const asOfText = asOf != null
|
||||
? 'matrix snapshot · live as of ' + fmtAgeSecs(ageSecs) + ' ago'
|
||||
: 'no daemon snapshot yet';
|
||||
const ageSecs =
|
||||
asOf != null ? Math.max(0, Math.floor(Date.now() / 1000) - asOf) : null;
|
||||
const asOfText =
|
||||
asOf != null
|
||||
? "matrix snapshot · live as of " + fmtAgeSecs(ageSecs) + " ago"
|
||||
: "no daemon snapshot yet";
|
||||
// 3 missed ~30s heartbeats. Past this a `live` snapshot whose container is
|
||||
// NOT down means the daemon stopped publishing (dead/wedged) — dim its dot.
|
||||
const STALE_AGE_SECS = 90;
|
||||
const staleByAge = ageSecs != null && ageSecs > STALE_AGE_SECS;
|
||||
list.replaceChildren();
|
||||
if (!accounts.length) {
|
||||
list.append(el('p', { class: 'meta' }, 'no matrix accounts configured for this agent.'));
|
||||
list.append(
|
||||
el(
|
||||
"p",
|
||||
{ class: "meta" },
|
||||
"no matrix accounts configured for this agent.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const ul = el('ul', { class: 'ma-accounts' });
|
||||
const ul = el("ul", { class: "ma-accounts" });
|
||||
for (const acc of accounts) {
|
||||
const present = !!acc.token_present;
|
||||
// 3-state dot. `live` is absent on the v1 backend (pre BE-4); when
|
||||
// undefined, fall back to the v1 token-present rendering so the page
|
||||
// degrades cleanly before the snapshot backend deploys.
|
||||
let cls; let statusText; let dotTitle;
|
||||
let cls;
|
||||
let statusText;
|
||||
let dotTitle;
|
||||
if (acc.live === undefined) {
|
||||
cls = present ? 'ok' : 'absent';
|
||||
statusText = present ? 'token stored ✓' : 'no token';
|
||||
dotTitle = present ? 'token stored' : 'no token yet';
|
||||
cls = present ? "ok" : "absent";
|
||||
statusText = present ? "token stored ✓" : "no token";
|
||||
dotTitle = present ? "token stored" : "no token yet";
|
||||
} else if (acc.live && running === false) {
|
||||
// container down ⟹ daemon down ⟹ a "live" snapshot is stale.
|
||||
cls = 'stale';
|
||||
statusText = 'container stopped';
|
||||
dotTitle = 'container is stopped — live status is stale. ' + asOfText;
|
||||
cls = "stale";
|
||||
statusText = "container stopped";
|
||||
dotTitle = "container is stopped — live status is stale. " + asOfText;
|
||||
} else if (acc.live && staleByAge) {
|
||||
// Snapshot says live, but the heartbeat (snapshot mtime = as_of) hasn't
|
||||
// advanced in > ~90s while the container is NOT down — the daemon stopped
|
||||
// publishing, so the "live" is no longer trustworthy. Keep the green
|
||||
// family but dim it (distinct from the amber container-down 'stale').
|
||||
cls = 'live stale-age';
|
||||
statusText = 'online · no heartbeat';
|
||||
dotTitle = 'snapshot says live but the daemon heartbeat stalled '
|
||||
+ fmtAgeSecs(ageSecs) + ' ago (publishes every ~30s) — likely dead or wedged. '
|
||||
+ asOfText;
|
||||
cls = "live stale-age";
|
||||
statusText = "online · no heartbeat";
|
||||
dotTitle =
|
||||
"snapshot says live but the daemon heartbeat stalled " +
|
||||
fmtAgeSecs(ageSecs) +
|
||||
" ago (publishes every ~30s) — likely dead or wedged. " +
|
||||
asOfText;
|
||||
} else if (acc.live) {
|
||||
cls = 'live';
|
||||
statusText = 'online ✓';
|
||||
cls = "live";
|
||||
statusText = "online ✓";
|
||||
dotTitle = asOfText;
|
||||
} else if (present) {
|
||||
cls = 'offline';
|
||||
statusText = 'token stored · offline';
|
||||
dotTitle = 'provisioned but not live. ' + asOfText;
|
||||
cls = "offline";
|
||||
statusText = "token stored · offline";
|
||||
dotTitle = "provisioned but not live. " + asOfText;
|
||||
} else {
|
||||
cls = 'absent';
|
||||
statusText = 'no token';
|
||||
dotTitle = 'no token yet';
|
||||
cls = "absent";
|
||||
statusText = "no token";
|
||||
dotTitle = "no token yet";
|
||||
}
|
||||
ul.append(el('li', { class: 'ma-account' },
|
||||
el('span', { class: 'ma-dot ' + cls, title: dotTitle }),
|
||||
el('span', { class: 'ma-name' }, acc.name || '(unnamed)'),
|
||||
acc.user_id ? el('span', { class: 'ma-uid' }, acc.user_id) : null,
|
||||
el('span', { class: 'ma-hs' }, acc.homeserver || '—'),
|
||||
el('span', { class: 'ma-status ' + cls, title: asOfText }, statusText),
|
||||
));
|
||||
ul.append(
|
||||
el(
|
||||
"li",
|
||||
{ class: "ma-account" },
|
||||
el("span", { class: "ma-dot " + cls, title: dotTitle }),
|
||||
el("span", { class: "ma-name" }, acc.name || "(unnamed)"),
|
||||
acc.user_id ? el("span", { class: "ma-uid" }, acc.user_id) : null,
|
||||
el("span", { class: "ma-hs" }, acc.homeserver || "—"),
|
||||
el("span", { class: "ma-status " + cls, title: asOfText }, statusText),
|
||||
),
|
||||
);
|
||||
}
|
||||
list.append(ul);
|
||||
}
|
||||
|
|
@ -181,72 +206,90 @@ async function loadAccounts(agent) {
|
|||
// both — would be submitted).
|
||||
function toggleModeFields() {
|
||||
const mode = document.querySelector('input[name="mode"]:checked');
|
||||
const value = mode ? mode.value : 'password';
|
||||
const pw = $('ma-pw-fields');
|
||||
const tok = $('ma-token-fields');
|
||||
pw.hidden = value !== 'password';
|
||||
tok.hidden = value !== 'token';
|
||||
pw.querySelectorAll('input').forEach((i) => { i.disabled = pw.hidden; });
|
||||
tok.querySelectorAll('input').forEach((i) => { i.disabled = tok.hidden; });
|
||||
const value = mode ? mode.value : "password";
|
||||
const pw = $("ma-pw-fields");
|
||||
const tok = $("ma-token-fields");
|
||||
pw.hidden = value !== "password";
|
||||
tok.hidden = value !== "token";
|
||||
pw.querySelectorAll("input").forEach((i) => {
|
||||
i.disabled = pw.hidden;
|
||||
});
|
||||
tok.querySelectorAll("input").forEach((i) => {
|
||||
i.disabled = tok.hidden;
|
||||
});
|
||||
}
|
||||
|
||||
function clearSecrets(formEl) {
|
||||
formEl.querySelectorAll('input[type="password"], input[name="token"]')
|
||||
.forEach((i) => { i.value = ''; });
|
||||
formEl
|
||||
.querySelectorAll('input[type="password"], input[name="token"]')
|
||||
.forEach((i) => {
|
||||
i.value = "";
|
||||
});
|
||||
}
|
||||
|
||||
async function submitLogin(e) {
|
||||
e.preventDefault();
|
||||
const formEl = e.target;
|
||||
const out = $('ma-result');
|
||||
out.className = 'ma-result';
|
||||
out.textContent = '';
|
||||
const out = $("ma-result");
|
||||
out.className = "ma-result";
|
||||
out.textContent = "";
|
||||
|
||||
const agent = $('ma-agent').value;
|
||||
const agent = $("ma-agent").value;
|
||||
if (!agent) {
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = 'select an agent first.';
|
||||
out.className = "ma-result err";
|
||||
out.textContent = "select an agent first.";
|
||||
return;
|
||||
}
|
||||
|
||||
const fd = new FormData(formEl);
|
||||
fd.set('agent', agent);
|
||||
fd.set("agent", agent);
|
||||
|
||||
const btn = formEl.querySelector('button[type="submit"]');
|
||||
const orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'logging in…';
|
||||
btn.textContent = "logging in…";
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/matrix-account-login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
const resp = await fetch("/api/matrix-account-login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams(fd),
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
// Success is 200 + JSON { ok, user_id }.
|
||||
let body = {};
|
||||
try { body = await resp.json(); } catch { /* tolerate odd 2xx body */ }
|
||||
try {
|
||||
body = await resp.json();
|
||||
} catch {
|
||||
/* tolerate odd 2xx body */
|
||||
}
|
||||
if (body.ok) {
|
||||
out.className = 'ma-result ok';
|
||||
out.textContent = '✓ logged in as ' + (body.user_id || '(unknown)') + ' — token stored.';
|
||||
out.className = "ma-result ok";
|
||||
out.textContent =
|
||||
"✓ logged in as " +
|
||||
(body.user_id || "(unknown)") +
|
||||
" — token stored.";
|
||||
clearSecrets(formEl);
|
||||
loadAccounts(agent);
|
||||
} else {
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ login failed (unexpected response).';
|
||||
out.className = "ma-result err";
|
||||
out.textContent = "✗ login failed (unexpected response).";
|
||||
clearSecrets(formEl);
|
||||
}
|
||||
} else {
|
||||
const msg = problemMessage(await readApiError(resp));
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ ' + (msg || ('login failed (HTTP ' + resp.status + ')'));
|
||||
out.className = "ma-result err";
|
||||
out.textContent =
|
||||
"✗ " + (msg || "login failed (HTTP " + resp.status + ")");
|
||||
clearSecrets(formEl);
|
||||
}
|
||||
} catch (err) {
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ request failed: ' + String(err) + ' (the backend endpoint may not be deployed yet).';
|
||||
out.className = "ma-result err";
|
||||
out.textContent =
|
||||
"✗ request failed: " +
|
||||
String(err) +
|
||||
" (the backend endpoint may not be deployed yet).";
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
|
|
@ -256,81 +299,111 @@ async function submitLogin(e) {
|
|||
// ─── GITHUB tab ─────────────────────────────────────────────────────────
|
||||
|
||||
async function loadGithubStatus(agent) {
|
||||
const status = $('gh-status');
|
||||
const status = $("gh-status");
|
||||
if (!agent) {
|
||||
status.replaceChildren(el('p', { class: 'meta' }, 'select an agent to see its github credential status.'));
|
||||
status.replaceChildren(
|
||||
el(
|
||||
"p",
|
||||
{ class: "meta" },
|
||||
"select an agent to see its github credential status.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
status.replaceChildren(el('p', { class: 'meta' }, 'loading…'));
|
||||
status.replaceChildren(el("p", { class: "meta" }, "loading…"));
|
||||
let data;
|
||||
try {
|
||||
const resp = await fetch('/api/github-account?agent=' + encodeURIComponent(agent));
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
const resp = await fetch(
|
||||
"/api/github-account?agent=" + encodeURIComponent(agent),
|
||||
);
|
||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
||||
data = await resp.json();
|
||||
} catch (err) {
|
||||
status.replaceChildren(el('p', { class: 'err' },
|
||||
'could not load status: ' + esc(String(err)) + ' (the backend endpoint may not be deployed yet).'));
|
||||
status.replaceChildren(
|
||||
el(
|
||||
"p",
|
||||
{ class: "err" },
|
||||
"could not load status: " +
|
||||
esc(String(err)) +
|
||||
" (the backend endpoint may not be deployed yet).",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const present = !!data.present;
|
||||
status.replaceChildren(el('div', { class: 'gh-status-line' },
|
||||
el('span', { class: 'gh-dot ' + (present ? 'present' : 'absent') }),
|
||||
el('span', { class: 'gh-status-text ' + (present ? 'present' : 'absent') },
|
||||
present ? 'token stored ✓' : 'not set'),
|
||||
));
|
||||
status.replaceChildren(
|
||||
el(
|
||||
"div",
|
||||
{ class: "gh-status-line" },
|
||||
el("span", { class: "gh-dot " + (present ? "present" : "absent") }),
|
||||
el(
|
||||
"span",
|
||||
{ class: "gh-status-text " + (present ? "present" : "absent") },
|
||||
present ? "token stored ✓" : "not set",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function submitGithub(e) {
|
||||
e.preventDefault();
|
||||
const formEl = e.target;
|
||||
const out = $('gh-result');
|
||||
out.className = 'ma-result';
|
||||
out.textContent = '';
|
||||
const out = $("gh-result");
|
||||
out.className = "ma-result";
|
||||
out.textContent = "";
|
||||
|
||||
const agent = $('ma-agent').value;
|
||||
const agent = $("ma-agent").value;
|
||||
if (!agent) {
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = 'select an agent first.';
|
||||
out.className = "ma-result err";
|
||||
out.textContent = "select an agent first.";
|
||||
return;
|
||||
}
|
||||
|
||||
const fd = new FormData(formEl);
|
||||
fd.set('agent', agent);
|
||||
fd.set("agent", agent);
|
||||
|
||||
const btn = formEl.querySelector('button[type="submit"]');
|
||||
const orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'storing…';
|
||||
btn.textContent = "storing…";
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/github-account', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
const resp = await fetch("/api/github-account", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams(fd),
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
let body = {};
|
||||
try { body = await resp.json(); } catch { /* tolerate odd 2xx body */ }
|
||||
try {
|
||||
body = await resp.json();
|
||||
} catch {
|
||||
/* tolerate odd 2xx body */
|
||||
}
|
||||
if (body.ok) {
|
||||
out.className = 'ma-result ok';
|
||||
out.textContent = '✓ token stored.';
|
||||
out.className = "ma-result ok";
|
||||
out.textContent = "✓ token stored.";
|
||||
clearSecrets(formEl);
|
||||
loadGithubStatus(agent);
|
||||
} else {
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ store failed (unexpected response).';
|
||||
out.className = "ma-result err";
|
||||
out.textContent = "✗ store failed (unexpected response).";
|
||||
clearSecrets(formEl);
|
||||
}
|
||||
} else {
|
||||
const msg = problemMessage(await readApiError(resp));
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ ' + (msg || ('store failed (HTTP ' + resp.status + ')'));
|
||||
out.className = "ma-result err";
|
||||
out.textContent =
|
||||
"✗ " + (msg || "store failed (HTTP " + resp.status + ")");
|
||||
clearSecrets(formEl);
|
||||
}
|
||||
} catch (err) {
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ request failed: ' + String(err) + ' (the backend endpoint may not be deployed yet).';
|
||||
out.className = "ma-result err";
|
||||
out.textContent =
|
||||
"✗ request failed: " +
|
||||
String(err) +
|
||||
" (the backend endpoint may not be deployed yet).";
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
|
|
@ -347,39 +420,56 @@ async function submitGithub(e) {
|
|||
// forge themselves.
|
||||
|
||||
async function loadForgeAccounts(agent) {
|
||||
const list = $('ef-list');
|
||||
const list = $("ef-list");
|
||||
if (!agent) {
|
||||
list.replaceChildren(el('p', { class: 'meta' }, 'select an agent to see its forge accounts.'));
|
||||
list.replaceChildren(
|
||||
el("p", { class: "meta" }, "select an agent to see its forge accounts."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
list.replaceChildren(el('p', { class: 'meta' }, 'loading…'));
|
||||
list.replaceChildren(el("p", { class: "meta" }, "loading…"));
|
||||
let forges;
|
||||
try {
|
||||
const resp = await fetch('/api/extra-forges?agent=' + encodeURIComponent(agent));
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
const resp = await fetch(
|
||||
"/api/extra-forges?agent=" + encodeURIComponent(agent),
|
||||
);
|
||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
||||
forges = (await resp.json()).forges || [];
|
||||
} catch (err) {
|
||||
list.replaceChildren(el('p', { class: 'err' },
|
||||
'could not load forge accounts: ' + esc(String(err)) + ' (the backend endpoint may not be deployed yet).'));
|
||||
list.replaceChildren(
|
||||
el(
|
||||
"p",
|
||||
{ class: "err" },
|
||||
"could not load forge accounts: " +
|
||||
esc(String(err)) +
|
||||
" (the backend endpoint may not be deployed yet).",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
list.replaceChildren();
|
||||
if (!forges.length) {
|
||||
list.replaceChildren(el('p', { class: 'meta' }, 'no forge accounts stored for this agent.'));
|
||||
list.replaceChildren(
|
||||
el("p", { class: "meta" }, "no forge accounts stored for this agent."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const ul = el('ul', { class: 'ma-accounts' });
|
||||
const ul = el("ul", { class: "ma-accounts" });
|
||||
for (const forge of forges) {
|
||||
const btn = el('button', { class: 'btn', type: 'button' }, 'remove');
|
||||
btn.addEventListener('click', () => onForgeRemoveClick(agent, forge, btn));
|
||||
ul.append(el('li', { class: 'ma-account' },
|
||||
el('span', { class: 'ma-dot ok' }),
|
||||
el('span', { class: 'ma-name' }, forge.label),
|
||||
el('span', { class: 'ma-hs' }, forge.base_url || '—'),
|
||||
el('span', { class: 'ma-status ok' }, 'token stored ✓'),
|
||||
btn,
|
||||
));
|
||||
const btn = el("button", { class: "btn", type: "button" }, "remove");
|
||||
btn.addEventListener("click", () => onForgeRemoveClick(agent, forge, btn));
|
||||
ul.append(
|
||||
el(
|
||||
"li",
|
||||
{ class: "ma-account" },
|
||||
el("span", { class: "ma-dot ok" }),
|
||||
el("span", { class: "ma-name" }, forge.label),
|
||||
el("span", { class: "ma-hs" }, forge.base_url || "—"),
|
||||
el("span", { class: "ma-status ok" }, "token stored ✓"),
|
||||
btn,
|
||||
),
|
||||
);
|
||||
}
|
||||
list.append(ul);
|
||||
}
|
||||
|
|
@ -388,18 +478,22 @@ async function onForgeRemoveClick(agent, forge, btn) {
|
|||
const r = await themedConfirm({
|
||||
message: `remove ${agent}'s stored token for ${forge.label}? this only deletes the local copy — nothing changes on the remote forge.`,
|
||||
danger: true,
|
||||
confirmLabel: '⊘ remove',
|
||||
confirmLabel: "⊘ remove",
|
||||
});
|
||||
if (!r) return;
|
||||
|
||||
btn.disabled = true;
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = 'removing…';
|
||||
btn.textContent = "removing…";
|
||||
try {
|
||||
const resp = await fetch('/api/extra-forge-account', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ agent, label: forge.label, action: 'remove' }),
|
||||
const resp = await fetch("/api/extra-forge-account", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
agent,
|
||||
label: forge.label,
|
||||
action: "remove",
|
||||
}),
|
||||
});
|
||||
if (resp.ok) {
|
||||
loadForgeAccounts(agent);
|
||||
|
|
@ -408,66 +502,76 @@ async function onForgeRemoveClick(agent, forge, btn) {
|
|||
const msg = problemMessage(await readApiError(resp));
|
||||
btn.textContent = orig;
|
||||
btn.disabled = false;
|
||||
themedToast('✗ ' + (msg || ('remove failed (HTTP ' + resp.status + ')')), { type: 'error' });
|
||||
themedToast("✗ " + (msg || "remove failed (HTTP " + resp.status + ")"), {
|
||||
type: "error",
|
||||
});
|
||||
} catch (err) {
|
||||
btn.textContent = orig;
|
||||
btn.disabled = false;
|
||||
themedToast('✗ request failed: ' + String(err), { type: 'error' });
|
||||
themedToast("✗ request failed: " + String(err), { type: "error" });
|
||||
}
|
||||
}
|
||||
|
||||
async function submitForgeAccount(e) {
|
||||
e.preventDefault();
|
||||
const formEl = e.target;
|
||||
const out = $('ef-result');
|
||||
out.className = 'ma-result';
|
||||
out.textContent = '';
|
||||
const out = $("ef-result");
|
||||
out.className = "ma-result";
|
||||
out.textContent = "";
|
||||
|
||||
const agent = $('ma-agent').value;
|
||||
const agent = $("ma-agent").value;
|
||||
if (!agent) {
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = 'select an agent first.';
|
||||
out.className = "ma-result err";
|
||||
out.textContent = "select an agent first.";
|
||||
return;
|
||||
}
|
||||
|
||||
const fd = new FormData(formEl);
|
||||
fd.set('agent', agent);
|
||||
fd.set('action', 'add');
|
||||
fd.set("agent", agent);
|
||||
fd.set("action", "add");
|
||||
|
||||
const btn = formEl.querySelector('button[type="submit"]');
|
||||
const orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'storing…';
|
||||
btn.textContent = "storing…";
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/extra-forge-account', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
const resp = await fetch("/api/extra-forge-account", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams(fd),
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
let body = {};
|
||||
try { body = await resp.json(); } catch { /* tolerate odd 2xx body */ }
|
||||
try {
|
||||
body = await resp.json();
|
||||
} catch {
|
||||
/* tolerate odd 2xx body */
|
||||
}
|
||||
if (body.ok) {
|
||||
out.className = 'ma-result ok';
|
||||
out.textContent = '✓ forge account stored.';
|
||||
out.className = "ma-result ok";
|
||||
out.textContent = "✓ forge account stored.";
|
||||
clearSecrets(formEl);
|
||||
loadForgeAccounts(agent);
|
||||
} else {
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ store failed (unexpected response).';
|
||||
out.className = "ma-result err";
|
||||
out.textContent = "✗ store failed (unexpected response).";
|
||||
clearSecrets(formEl);
|
||||
}
|
||||
} else {
|
||||
const msg = problemMessage(await readApiError(resp));
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ ' + (msg || ('store failed (HTTP ' + resp.status + ')'));
|
||||
out.className = "ma-result err";
|
||||
out.textContent =
|
||||
"✗ " + (msg || "store failed (HTTP " + resp.status + ")");
|
||||
clearSecrets(formEl);
|
||||
}
|
||||
} catch (err) {
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ request failed: ' + String(err) + ' (the backend endpoint may not be deployed yet).';
|
||||
out.className = "ma-result err";
|
||||
out.textContent =
|
||||
"✗ request failed: " +
|
||||
String(err) +
|
||||
" (the backend endpoint may not be deployed yet).";
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
|
|
@ -485,24 +589,27 @@ async function onAgentChange(agent) {
|
|||
async function init() {
|
||||
await loadState();
|
||||
renderAgentPicker();
|
||||
$('ma-agent').addEventListener('change', (e) => onAgentChange(e.target.value));
|
||||
document.querySelectorAll('input[name="mode"]')
|
||||
.forEach((r) => r.addEventListener('change', toggleModeFields));
|
||||
$("ma-agent").addEventListener("change", (e) =>
|
||||
onAgentChange(e.target.value),
|
||||
);
|
||||
document
|
||||
.querySelectorAll('input[name="mode"]')
|
||||
.forEach((r) => r.addEventListener("change", toggleModeFields));
|
||||
toggleModeFields();
|
||||
$('ma-form').addEventListener('submit', submitLogin);
|
||||
$('gh-form').addEventListener('submit', submitGithub);
|
||||
$('ef-form').addEventListener('submit', submitForgeAccount);
|
||||
$("ma-form").addEventListener("submit", submitLogin);
|
||||
$("gh-form").addEventListener("submit", submitGithub);
|
||||
$("ef-form").addEventListener("submit", submitForgeAccount);
|
||||
|
||||
document.getElementById('cred-tabbar').configure({
|
||||
document.getElementById("cred-tabbar").configure({
|
||||
tabs: [
|
||||
{ id: 'matrix', label: 'MATRIX' },
|
||||
{ id: 'github', label: 'GITHUB' },
|
||||
{ id: 'forges', label: 'FORGES' },
|
||||
{ id: "matrix", label: "MATRIX" },
|
||||
{ id: "github", label: "GITHUB" },
|
||||
{ id: "forges", label: "FORGES" },
|
||||
],
|
||||
defaultId: 'matrix',
|
||||
defaultId: "matrix",
|
||||
});
|
||||
|
||||
onAgentChange('');
|
||||
onAgentChange("");
|
||||
}
|
||||
|
||||
init();
|
||||
|
|
|
|||
Loading…
Reference in a new issue