From 6a1d85c24fa7266c3ef334bf35c91a27ea22d00d Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 29 Sep 2026 10:40:40 +0200 Subject: [PATCH] hive-dashboard: remove the MATRIX credentials tab and its login route The CR3D3NTIALS page's MATRIX tab was the only caller of `POST /api/matrix-account-login` (provision/log in an external matrix account through the hive) and `GET /api/matrix-accounts` (its account list). External matrix accounts are linked from the swarm UI now (`LinkMatrixAccountForm` -> swarm-controller), so the hive-side UI and both routes go. `priv_client::restart_matrix_daemon` had no other caller and goes with them. Already-provisioned credentials keep working: the `matrix-token-` files and `matrix-account-.json` sidecars the old route wrote are still discovered by hive-matrix-mcp (`accounts::configured` -> `discover_token_accounts`), the `matrix-token*` path unit still re-fires the daemon, and `WriteAgentMatrixToken` stays for the swarm credential worker. Removing that usage waits on moving the existing creds to swarm level. The GITHUB tab is the credentials page's default tab now. Refs #4348 --- docs/integrations/github.md | 5 +- docs/web-ui/README.md | 2 +- docs/web-ui/dashboard.md | 61 +-- .../packages/dashboard/src/credentials.css | 75 +-- .../packages/dashboard/src/credentials.html | 118 +---- .../packages/dashboard/src/credentials.js | 238 +-------- frontend/packages/dashboard/src/index.html | 2 +- hive-c0re/src/dashboard/extra_forges.rs | 11 +- hive-c0re/src/dashboard/matrix_accounts.rs | 466 +----------------- hive-c0re/src/dashboard/mod.rs | 6 +- hive-c0re/src/priv_client.rs | 11 - swarm-controller/src/matrix_account.rs | 12 +- 12 files changed, 35 insertions(+), 972 deletions(-) diff --git a/docs/integrations/github.md b/docs/integrations/github.md index aefec8ae..a6242c0f 100644 --- a/docs/integrations/github.md +++ b/docs/integrations/github.md @@ -5,9 +5,8 @@ HTTPS, both authenticated by an operator-supplied personal access token (PAT) — so it can run GitHub API calls and push commits without any manual `gh auth login`. -Provisioning is UI-driven, mirroring the dashboard side of the -[matrix account](matrix.md) flow: paste a PAT into the agent's credentials -tab and it works. No per-agent nix declaration, no rebuild — hive-c0re +Provisioning is UI-driven: paste a PAT into the agent's credentials tab +and it works. No per-agent nix declaration, no rebuild — hive-c0re injects the token into the agent's state dir out of band. ## Enabling diff --git a/docs/web-ui/README.md b/docs/web-ui/README.md index 18fadbf0..d604035a 100644 --- a/docs/web-ui/README.md +++ b/docs/web-ui/README.md @@ -29,7 +29,7 @@ the whole swarm), **Logs** (`/logs.html`, per-agent and host journals), **Stats** (`/stats.html`, swarm-wide usage stats), **Builds** (`/builds.html`, the rebuild queue and build history), **Core** (`/core.html`, tombstones and container resource use), and -**Credentials** (`/credentials.html`, provisioning matrix/GitHub/forge +**Credentials** (`/credentials.html`, provisioning GitHub/forge accounts per agent). Your local browser preferences (notifications) live in the dashboard's Y3R C4LL tab now, not a separate page. diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 89342d12..a3f66cf0 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -296,60 +296,9 @@ on the H0M3 hub, same minimal chrome as `/logs.html` (a `← home` back-link than `/core.html`'s plain title. Its own esbuild bundle (`credentials.js`); no SSE — it reads `/api/state` once for the (shared) agent picker and otherwise works off purpose-built endpoints per tab. -Three sub-tabs: +Two sub-tabs: -### MATRIX tab - - -Provision / log in a per-agent **external** matrix account and store its -access token (this half is unchanged from the old `/matrix-accounts.html` -page it replaces — only the URL and surrounding chrome moved). - - -An agent picker (populated from `state.containers`, the live roster) drives a list of that -agent's accounts — name, homeserver, user id, and a status dot — -read from `GET /api/matrix-accounts?agent=` → -`{ accounts: [ { name, homeserver, token_present, live, user_id } ], as_of_unix }`. -`token_present` is whether a token is **stored**; the matrix daemon -backfills `live`, `homeserver`, and `user_id` from its -`matrix-accounts.json` snapshot — a host-visible file it -**force-rewrites every ~30s** (a heartbeat), so `as_of_unix` (the -snapshot mtime) advances while the daemon is alive and a *stalled* value -genuinely means "stopped publishing," not just "old snapshot." An account -with a token but absent from the snapshot reports `live: false`. - -The status dot renders these states: - -- **green** — `live` and the container is running: online. -- **dim green** — `live` but `as_of_unix` hasn't advanced in > ~90s (3 - missed heartbeats) while the container is *not* down: the daemon stopped - publishing, so the snapshot's `live` is no longer trustworthy (likely - dead/wedged). Labelled "online · no heartbeat." -- **amber** — `live` but the container is **down** (a stopped container - ⟹ a dead daemon, so the snapshot is stale); also the `token_present && - !live` "provisioned but offline" case. -- **grey** — no token (not provisioned). - -The container-down cross-reference (`/api/state`) takes precedence over -the age check. The dashboard tooltips `as_of_unix` ("live as of N ago") -throughout so freshness is always legible. When `live` is absent (an older hive-c0re -without the snapshot) the dot falls back to a token-present rendering. - -The provision form (account name, homeserver, login method) posts -`POST /api/matrix-account-login` (`x-www-form-urlencoded`, operator-auth): -fields `agent, account, homeserver, mode=password|token, user_id?, -password?, token?` → `200 { ok, user_id }` on success. Failures come back -as RFC 9457 `application/problem+json` (`{ type, title, status, detail }`) -with the human-readable message in `detail` and the status code reflecting -the cause (400 for a validation error, 500 for a login / `whoami` / -internal failure); the page reads `detail` for display. The host coordinator performs the login -(password) or validates the token (`whoami`) and writes the bearer to -the agent's `matrixAccounts..tokenFile` via the same -privileged write path as the hive-internal `matrix-token`; the token is -**never** echoed back, and the page clears the secret inputs on submit -regardless of outcome. The account list reflects what's *provisioned* -(an account with a stored token), so a config-declared-but-unprovisioned -account appears only once the operator provisions it through the form. +Link an external matrix account from the swarm UI (`LinkMatrixAccountForm` → swarm-controller). Accounts already linked through the old MATRIX tab keep working until the operator moves them to swarm level. ### GITHUB tab @@ -363,10 +312,10 @@ minimally scoped token) and a link to Status reads `GET /api/github-account?agent=` → `{ present: bool }` — whether the agent's `github-token` file exists. There's no live/heartbeat concept for a static PAT, so this is just a -"token stored ✓" / "not set" line, unlike MATRIX's status-dot taxonomy. +"token stored ✓" / "not set" line. Provisioning posts `POST /api/github-account` (form-encoded `agent`, -`token`) → `200 { ok: true }` on success, or the same `error_response` -shape `/api/matrix-account-login` uses on failure. The token is never +`token`) → `200 { ok: true }` on success; failures come back as RFC 9457 +`application/problem+json` with the message in `detail`. The token is never echoed back in either direction. ### FORGES tab diff --git a/frontend/packages/dashboard/src/credentials.css b/frontend/packages/dashboard/src/credentials.css index e665d9f0..30842d4d 100644 --- a/frontend/packages/dashboard/src/credentials.css +++ b/frontend/packages/dashboard/src/credentials.css @@ -2,9 +2,9 @@ (.page-header / .page-back / .page-title) comes from the shared chrome.css imported by common.css; base tab styling lives in @hive/shared/tabs.css (.hive-tab*) same as /logs.html. This file holds - the account-list + provision-form styling specific to this surface, - carried over from the old /matrix-accounts.html (`.ma-*` classes) plus - the tab-strip layout delta + github-tab additions (`.cred-*`). */ + the account-list + provision-form styling specific to this surface + (`.ma-*` classes) plus the tab-strip layout delta + github-tab + additions (`.cred-*`). */ body.cred-shell { margin: 0; @@ -81,29 +81,6 @@ body.cred-shell { border-color: var(--purple); } -.ma-mode { - border: 1px solid var(--border); - border-radius: 4px; - padding: 0.45rem 0.75rem 0.6rem; - margin: 0.85rem 0; -} -.ma-mode legend { - font-size: 0.8rem; - color: var(--muted); - padding: 0 0.3rem; -} -.ma-mode label { - margin-right: 1.3rem; - cursor: pointer; -} - -.ma-modefields { - margin: 0.4rem 0; -} - -.ma-list { - margin: 0.5rem 0 1.2rem; -} .ma-accounts { list-style: none; padding: 0; @@ -122,38 +99,13 @@ body.cred-shell { border-radius: 50%; flex: none; } -/* `ok` is the v1 (pre BE-4) token-present green; `live` is the v2 online green. - `offline`/`stale` are the amber states (provisioned-not-live / container-down - ⟹ daemon-down). `absent` = no token. */ -.ma-dot.ok, -.ma-dot.live { +.ma-dot.ok { background: var(--green); } -.ma-dot.offline, -.ma-dot.stale { - background: var(--amber); -} -.ma-dot.absent { - background: var(--muted); -} -/* `live stale-age`: snapshot still says live but the daemon heartbeat stalled - (> ~90s). Keep the green hue but dim + desaturate so it reads "was online, - now uncertain" — visually distinct from the solid amber container-down - `stale`. More-specific (3 classes) so it overrides `.ma-dot.live`. */ -.ma-dot.live.stale-age { - background: var(--green); - opacity: 0.4; - filter: saturate(0.45); -} .ma-name { font-weight: 600; color: var(--fg); } -.ma-uid { - color: var(--muted); - font-size: 0.8rem; - margin-left: 0.4em; -} .ma-hs { color: var(--muted); font-size: 0.85rem; @@ -162,21 +114,9 @@ body.cred-shell { margin-left: auto; font-size: 0.8rem; } -.ma-status.ok, -.ma-status.live { +.ma-status.ok { color: var(--green); } -.ma-status.offline, -.ma-status.stale { - color: var(--amber); -} -.ma-status.absent { - color: var(--muted); -} -.ma-status.live.stale-age { - color: var(--green); - opacity: 0.6; -} .ma-result { margin-top: 0.7rem; @@ -189,8 +129,3 @@ body.cred-shell { .ma-result.err { color: var(--red); } - -.ma-list .err { - color: var(--red); - font-size: 0.9rem; -} diff --git a/frontend/packages/dashboard/src/credentials.html b/frontend/packages/dashboard/src/credentials.html index 3466bdca..c6ba4910 100644 --- a/frontend/packages/dashboard/src/credentials.html +++ b/frontend/packages/dashboard/src/credentials.html @@ -12,7 +12,7 @@
- +

◇ agent

- -
-

- provision or log in an external matrix account for an - agent and store its access token. the token is written to the agent's - matrixAccounts.<account>.tokenFile by the host - coordinator — it is never displayed back on this page. -

- -

◇ provisioned accounts

-

- accounts that have a stored token (provision one below to add it - here); a config-declared account that hasn't been provisioned yet - won't appear until it has a token. status reflects whether a - token is stored, not a live session — a true - online/offline indicator is a follow-up that needs the daemon's - account registry. -

-
-

select an agent to see its matrix accounts.

-
- -

◇ provision / log in

-
- - - -
- login method - - -
- -
- - -
- - - - -

-
-
- @@ -151,7 +42,6 @@ data-tab-pane="github" role="tabpanel" aria-labelledby="cred-tab-github" - hidden > ⚠ use a dedicated bot account, not a human's — @@ -194,7 +84,7 @@ diff --git a/frontend/packages/dashboard/src/credentials.js b/frontend/packages/dashboard/src/credentials.js index ef4f1f87..65120d5f 100644 --- a/frontend/packages/dashboard/src/credentials.js +++ b/frontend/packages/dashboard/src/credentials.js @@ -2,12 +2,9 @@ // // Operator surface to provision per-agent credentials without editing the // agent's config repo. Two sub-tabs, sharing one agent picker: -// MATRIX — external matrix account login (carried over verbatim from the -// old /matrix-accounts.html — see matrix_accounts.rs backend doc -// comments for the account/status contract + endpoint shapes). // GITHUB — single-account PAT paste against /api/github-account // (GET -> {present}, POST form-encoded {agent, token} -> -// {ok:true}; same error_response shape as matrix-account-login). +// {ok:true}; error_response shape on failure). // No account name / homeserver / login mode, and no // live/heartbeat concept for a static PAT — just present/absent. // FORGES — external forge accounts, entirely dashboard-provisioned (no @@ -19,19 +16,13 @@ // 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 { $, esc, 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 -// the live dot: a `live: true` account whose container is DOWN is definitively -// stale (the daemon can't be up if the container isn't), so we flag it rather -// than show a lying green. `undefined` (agent not in the map) = unknown → we -// don't flag stale. -const containerRunning = new Map(); async function loadState() { try { @@ -46,8 +37,6 @@ async function loadState() { .map((a) => (typeof a === "string" ? { name: a } : a)) .filter((c) => c && c.name); agents = containers.map((c) => c.name).sort(); - containerRunning.clear(); - for (const c of containers) containerRunning.set(c.name, !!c.running); } catch { // best-effort: on a failed state read the picker renders empty // ("— no agents —") and the submit guard blocks until an agent is @@ -74,151 +63,6 @@ function renderAgentPicker() { // originally; promoted so swarm-ui shares the same // shape-agnostic reader instead of each side maintaining its own copy. -// ─── MATRIX tab ──────────────────────────────────────────────────────────── -// Live status dot — the daemon heartbeats every ~30s (advances as_of_unix), -// so a stalled as_of = daemon dead, not just stale snapshot: -// green live + running + fresh = online -// dim green live but as_of stale > ~90s = heartbeat stopped -// amber live + container DOWN = definitively stale -// amber token_present + !live = provisioned but offline -// grey no token = not provisioned -// Container state takes precedence; as_of_unix is tooltipped for freshness. -// v1 backend (no `live` field) falls back to token-present rendering. - -async function loadAccounts(agent) { - const list = $("ma-list"); - if (!agent) { - list.replaceChildren( - el("p", { class: "meta" }, "select an agent to see its matrix accounts."), - ); - return; - } - 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); - 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).", - ), - ); - return; - } - const accounts = data.accounts || []; - 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"; - // 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.", - ), - ); - return; - } - 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; - if (acc.live === undefined) { - 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; - } 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; - } else if (acc.live) { - cls = "live"; - statusText = "online ✓"; - dotTitle = asOfText; - } else if (present) { - cls = "offline"; - statusText = "token stored · offline"; - dotTitle = "provisioned but not live. " + asOfText; - } else { - 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), - ), - ); - } - list.append(ul); -} - -// Show only the fields for the selected login method, and DISABLE the -// hidden section's inputs so they don't ride along in the FormData (both -// sections carry a `user_id` field, so without this the wrong one — or -// 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; - }); -} - function clearSecrets(formEl) { formEl .querySelectorAll('input[type="password"], input[name="token"]') @@ -227,75 +71,6 @@ function clearSecrets(formEl) { }); } -async function submitLogin(e) { - e.preventDefault(); - const formEl = e.target; - const out = $("ma-result"); - out.className = "ma-result"; - out.textContent = ""; - - const agent = $("ma-agent").value; - if (!agent) { - out.className = "ma-result err"; - out.textContent = "select an agent first."; - return; - } - - const fd = new FormData(formEl); - fd.set("agent", agent); - - const btn = formEl.querySelector('button[type="submit"]'); - const orig = btn.textContent; - btn.disabled = true; - btn.textContent = "logging in…"; - - try { - 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 */ - } - if (body.ok) { - 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)."; - clearSecrets(formEl); - } - } else { - const msg = problemMessage(await readApiError(resp)); - 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)."; - } finally { - btn.disabled = false; - btn.textContent = orig; - } -} - // ─── GITHUB tab ───────────────────────────────────────────────────────── async function loadGithubStatus(agent) { @@ -581,7 +356,6 @@ async function submitForgeAccount(e) { // ─── init ───────────────────────────────────────────────────────────── async function onAgentChange(agent) { - loadAccounts(agent); loadGithubStatus(agent); loadForgeAccounts(agent); } @@ -592,21 +366,15 @@ async function init() { $("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); document.getElementById("cred-tabbar").configure({ tabs: [ - { id: "matrix", label: "MATRIX" }, { id: "github", label: "GITHUB" }, { id: "forges", label: "FORGES" }, ], - defaultId: "matrix", + defaultId: "github", }); onAgentChange(""); diff --git a/frontend/packages/dashboard/src/index.html b/frontend/packages/dashboard/src/index.html index f2062109..25bdf175 100644 --- a/frontend/packages/dashboard/src/index.html +++ b/frontend/packages/dashboard/src/index.html @@ -96,7 +96,7 @@ Credentials provision per-agent matrix + github accountsprovision per-agent github + forge accounts diff --git a/hive-c0re/src/dashboard/extra_forges.rs b/hive-c0re/src/dashboard/extra_forges.rs index 12e759fb..078f7fbc 100644 --- a/hive-c0re/src/dashboard/extra_forges.rs +++ b/hive-c0re/src/dashboard/extra_forges.rs @@ -5,17 +5,15 @@ //! the external forge themselves (however that forge lets them: PAT UI, a //! teammate with admin, whatever) and pastes a label + base URL + token into //! the dashboard's FORGES tab, same shape as the GitHub PAT flow -//! (`post_github_account`) plus the homeserver field from the matrix extra- -//! account flow (`post_matrix_account_login`). +//! (`post_github_account`) plus a base URL. //! //! No remote account minting, no admin API, no revoke-on-the-remote-side — //! this module only ever touches the *local* agent state dir. hive-c0re //! persists the token to `/forge-