feat: rename matrix-accounts page to credentials, add github PAT tab

Part 2 of hyperhive#1970 (backend/CLI landed in PR #2378). Renames
/matrix-accounts.html -> /credentials.html and restructures it with a
sub-tab strip (MATRIX / GITHUB), reusing the shared @hive/shared/tabs.js
tab strip already used by /logs.html.

MATRIX tab carries over the existing account list + login form
unchanged. GITHUB tab adds a single-PAT provisioning form: status line
(present/absent, read from GET /api/github-account), a security-warning
banner (dedicated bot account + minimally-scoped token), a link to
generate a PAT at github.com/settings/tokens, and a paste-token form
posting to POST /api/github-account. Both tabs share one agent picker.

Updated docs/web-ui.md + docs/web-ui/dashboard.md to describe the new
page shape, and the H0M3 hub tile (index.html) to point at the renamed
page.
This commit is contained in:
iris 2026-07-11 12:38:47 +02:00
commit 5c2cae41a2
8 changed files with 376 additions and 148 deletions

View file

@ -15,7 +15,7 @@ This doc has been split for readability. Pick the section you need:
- **[Dashboard layout](web-ui/dashboard.md)** — tab contents
(SW4RM, Y3R C4LL, P3RM1SS10NS, SCH3DUL3S) + standalone pages
(C0R3 `/core.html`, BU1LDS `/builds.html`, L0GS `/logs.html`,
M4TR1X ACC0UNTS `/matrix-accounts.html`), container row,
CR3D3NTIALS `/credentials.html`), container row,
topology tree, selection bar, approval card, browser
notifications, dashboard endpoints + event channel.
- **[Per-agent page](web-ui/agent.md)** — header, main terminal,

View file

@ -254,15 +254,22 @@ badge with a ticking elapsed-time chip; expanding streams output via
(suspends on manual scroll-up). Deep-link: `?id=N#buildlogs` opens the
entry with that id pre-expanded.
## M4TR1X ACC0UNTS page (`/matrix-accounts.html`)
## CR3D3NTIALS page (`/credentials.html`)
Operator surface to provision / log in a per-agent **external** matrix
account and store its access token, without editing the agent's config
repo. Standalone page reached from the **Matrix accounts** tile on the
H0M3 hub, same minimal chrome as `/core.html` (a `← home` back-link +
title). Its own esbuild bundle (`matrix-accounts.js`); no SSE — it reads
`/api/state` once for the agent picker and otherwise works off two
purpose-built endpoints.
Operator surface to provision per-agent credentials without editing the
agent's config repo. Standalone page reached from the **Credentials** tile
on the H0M3 hub, same minimal chrome as `/logs.html` (a `← home` back-link
+ a sub-tab strip, via the shared `@hive/shared/tabs.js` tab strip) rather
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.
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 —
@ -288,6 +295,24 @@ The status dot renders these states:
!live` "provisioned but offline" case.
- **grey** — no token (not provisioned).
### GITHUB tab
Provision a single per-agent GitHub personal access token (see
[`docs/github.md`](../github.md) for the injection + `gh`/git-push
mechanics). No login flow — the operator pastes an existing PAT for a
dedicated bot account, with a security-warning banner (dedicated account +
minimally-scoped token) and a link to
[github.com/settings/tokens](https://github.com/settings/tokens).
Status reads `GET /api/github-account?agent=<name>`
`{ 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.
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
echoed back in either direction.
The container-down cross-reference (`/api/state`) takes precedence over
the age check. `as_of_unix` is tooltipped ("live as of N ago") throughout
so freshness is always legible. When `live` is absent (an older backend

View file

@ -10,6 +10,8 @@
// state / container load)
// dist/builds.html served at GET /builds.html (BU1LDS:
// rebuild queue / meta inputs / build logs)
// dist/credentials.html served at GET /credentials.html
// (CR3D3NTIALS: matrix + github accounts)
// dist/static/home.js index.html (H0M3) entry — menu tiles +
// matrix-tile gating + identity line
// dist/static/tabs.js dashboard.html entry — tab renderers +
@ -20,7 +22,10 @@
// log viewer sub-tabs
// dist/static/builds.js /builds.html entry — rebuild queue,
// meta inputs, build log history
// dist/static/{home,tabs,flow,logs,builds}.js.map source map siblings
// dist/static/credentials.js /credentials.html entry — matrix +
// github account sub-tabs
// dist/static/{home,tabs,flow,logs,builds,credentials}.js.map source map
// siblings
// dist/static/common.css loaded by every page (@hive/shared
// imports + shared typography/badges/
// buttons/inbox/side-panel)
@ -29,6 +34,8 @@
// dist/static/logs.css /logs.html only (log viewer sub-tabs)
// dist/static/builds.css /builds.html only (build lifecycle hub)
// dist/static/home.css index.html (H0M3) only (tile grid)
// dist/static/credentials.css /credentials.html only (matrix + github
// account forms)
//
// Each JS entry bundles `./common.js` (DOM helpers, Panel singleton,
// NOTIF, path linkification) independently — esbuild inlines the shared
@ -59,7 +66,7 @@ mkdirSync(staticDir(''), { recursive: true });
// follow-up once asset sizes warrant it). esbuild writes each entry
// to `static/<name>.js` based on the entryPoint basename.
await build({
entryPoints: [src('tabs.js'), src('flow.js'), src('logs.js'), src('home.js'), src('settings.js'), src('stats.js'), src('core.js'), src('builds.js'), src('matrix-accounts.js')],
entryPoints: [src('tabs.js'), src('flow.js'), src('logs.js'), src('home.js'), src('settings.js'), src('stats.js'), src('core.js'), src('builds.js'), src('credentials.js')],
outdir: staticDir(''),
bundle: true,
format: 'esm',
@ -98,7 +105,7 @@ await build({
// so a swap replaces only it) + theme.css (the semantic derivation
// layer) + common.css (shared typography, badges, buttons, inbox, side
// panel) plus its own page-specific bundle.
for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', 'flow.css', 'logs.css', 'home.css', 'settings.css', 'stats.css', 'core.css', 'builds.css', 'matrix-accounts.css']) {
for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', 'flow.css', 'logs.css', 'home.css', 'settings.css', 'stats.css', 'core.css', 'builds.css', 'credentials.css']) {
await build({
entryPoints: [src(entry)],
outfile: staticDir(entry),
@ -108,7 +115,7 @@ for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', '
});
}
for (const html of ['index.html', 'dashboard.html', 'flow.html', 'logs.html', 'settings.html', 'stats.html', 'core.html', 'builds.html', 'matrix-accounts.html']) {
for (const html of ['index.html', 'dashboard.html', 'flow.html', 'logs.html', 'settings.html', 'stats.html', 'core.html', 'builds.html', 'credentials.html']) {
copyFileSync(src(html), dist(html));
}

View file

@ -1,14 +1,58 @@
/* M4TR1X ACC0UNTS page (/matrix-accounts.html) only. Page chrome
/* CR3D3NTIALS page (/credentials.html) only. Page chrome
(.page-header / .page-back / .page-title) comes from the shared
chrome.css imported by common.css; this file holds the account-list
+ provision-form styling specific to this surface. */
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-*`). */
.ma-main {
body.cred-shell {
margin: 0;
padding: 0;
}
/* Same layout delta as .logs-tabbar: fill the header row next to the
home back-link, and let the flex:1 nav shrink below its intrinsic
width instead of wrapping onto its own row. */
.cred-tabbar {
flex: 1;
min-width: 0;
}
.cred-main {
max-width: 720px;
margin: 0 auto;
padding: 1rem 1.25rem 3rem;
padding: 1.2em 1.25rem 3rem;
}
.cred-pane[hidden] { display: none; }
.cred-warning {
border: 1px solid var(--amber);
border-radius: 4px;
padding: 0.7rem 0.9rem;
margin: 0.6rem 0 1.1rem;
font-size: 0.9rem;
background: color-mix(in srgb, var(--amber) 10%, transparent);
}
.gh-status { margin: 0.5rem 0 1.2rem; }
.gh-status-line {
display: flex;
align-items: center;
gap: 0.6rem;
}
.gh-dot {
width: 0.6rem;
height: 0.6rem;
border-radius: 50%;
flex: none;
}
.gh-dot.present { background: var(--green); }
.gh-dot.absent { background: var(--muted); }
.gh-status-text.present { color: var(--green); }
.gh-status-text.absent { color: var(--muted); }
.ma-field {
display: flex;
flex-direction: column;

View file

@ -0,0 +1,132 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>hyperhive // CR3D3NTIALS</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/static/colors.css">
<link rel="stylesheet" href="/static/theme.css">
<link rel="stylesheet" href="/static/common.css">
<link rel="stylesheet" href="/static/credentials.css">
</head>
<body class="cred-shell">
<!-- Minimal chrome: back link + sub-tab strip, same pattern as
logs.html (MATRIX / GITHUB instead of AGENT/SYSTEM/AUDIT). Back
link points to the H0M3 hub (served at /). -->
<header class="page-header">
<a class="page-back" href="/">← home</a>
<nav class="hive-tabbar cred-tabbar" id="cred-tabbar" role="tablist">
<a class="hive-tab" id="cred-tab-matrix" href="#matrix" role="tab"
aria-controls="cred-pane-matrix" data-tab="matrix">
<span class="cred-tab-label">MATRIX</span>
</a>
<a class="hive-tab" id="cred-tab-github" href="#github" role="tab"
aria-controls="cred-pane-github" data-tab="github">
<span class="cred-tab-label">GITHUB</span>
</a>
</nav>
</header>
<main class="cred-main">
<!-- Agent picker: shared across both tabs (one agent selected at a
time drives both the matrix account list and the github status). -->
<h3>◇ agent</h3>
<label class="ma-field">
<span>agent</span>
<select id="ma-agent"></select>
</label>
<!-- MATRIX tab: unchanged from the old /matrix-accounts.html, just
moved under a tab pane. -->
<section class="cred-pane" id="cred-pane-matrix" data-tab-pane="matrix"
role="tabpanel" aria-labelledby="cred-tab-matrix">
<p class="meta">provision or log in an <strong>external</strong> matrix account for an agent and store its access token. the token is written to the agent's <code>matrixAccounts.&lt;account&gt;.tokenFile</code> by the host coordinator &mdash; it is never displayed back on this page.</p>
<h3>◇ provisioned accounts</h3>
<p class="meta">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 <em>token is stored</em>, not a live session &mdash; a true online/offline indicator is a follow-up that needs the daemon's account registry.</p>
<div id="ma-list" class="ma-list"><p class="meta">select an agent to see its matrix accounts.</p></div>
<h3>◇ provision / log in</h3>
<form id="ma-form" class="ma-form" autocomplete="off">
<label class="ma-field">
<span>account name</span>
<input type="text" name="account" placeholder="e.g. public" required>
</label>
<label class="ma-field">
<span>homeserver</span>
<input type="text" name="homeserver" placeholder="https://matrix.org" required>
</label>
<fieldset class="ma-mode">
<legend>login method</legend>
<label><input type="radio" name="mode" value="password" checked> password</label>
<label><input type="radio" name="mode" value="token"> existing token</label>
</fieldset>
<div id="ma-pw-fields" class="ma-modefields">
<label class="ma-field">
<span>user id</span>
<input type="text" name="user_id" placeholder="@user:matrix.org" autocomplete="username">
</label>
<label class="ma-field">
<span>password</span>
<input type="password" name="password" autocomplete="new-password">
</label>
</div>
<div id="ma-token-fields" class="ma-modefields" hidden>
<label class="ma-field">
<span>access token</span>
<input type="password" name="token" autocomplete="off">
</label>
<label class="ma-field">
<span>user id <span class="meta">(optional &mdash; derived via whoami)</span></span>
<input type="text" name="user_id" placeholder="@user:matrix.org">
</label>
</div>
<button type="submit" class="btn btn-spawn">log in &amp; store token</button>
<p id="ma-result" class="ma-result" aria-live="polite"></p>
</form>
</section>
<!-- GITHUB tab: single-account PAT paste. No login flow — the
operator pastes an existing PAT for a dedicated bot account.
Security-warning banner + a link to generate a PAT. -->
<section class="cred-pane" id="cred-pane-github" data-tab-pane="github"
role="tabpanel" aria-labelledby="cred-tab-github" hidden>
<div class="cred-warning">
⚠ use a <strong>dedicated bot account</strong>, not a human's &mdash;
and a <strong>minimally-scoped</strong> personal access token (only
the repos/scopes the agent actually needs, e.g. <code>repo</code> +
<code>workflow</code>). the container boundary is the enforcement:
anything within the token's scopes is reachable if the agent is
ever compromised. the token is injected into the agent's state dir
and is <strong>never displayed back</strong> on this page.
</div>
<h3>◇ status</h3>
<div id="gh-status" class="gh-status"><p class="meta">select an agent to see its github credential status.</p></div>
<h3>◇ provision</h3>
<p class="meta">generate a token at
<a href="https://github.com/settings/tokens" target="_blank" rel="noopener">github.com/settings/tokens</a>
and paste it below. one account per agent &mdash; pasting a new token replaces the stored one.</p>
<form id="gh-form" class="ma-form" autocomplete="off">
<label class="ma-field">
<span>personal access token</span>
<input type="password" name="token" autocomplete="off" required>
</label>
<button type="submit" class="btn btn-spawn">store token</button>
<p id="gh-result" class="ma-result" aria-live="polite"></p>
</form>
</section>
</main>
<script type="module" src="/static/credentials.js" defer></script>
</body>
</html>

View file

@ -1,31 +1,19 @@
// M4TR1X ACC0UNTS page entry (/matrix-accounts.html).
// CR3D3NTIALS page entry (/credentials.html).
//
// Operator surface to provision / log in a per-agent EXTERNAL matrix
// account and store its access token, without editing the agent's config
// repo. Companion to the multi-account harness support.
//
// Backend contract:
// GET /api/matrix-accounts?agent=<name>
// -> { accounts: [{name, homeserver, token_present, live, user_id}],
// as_of_unix: int|null }
// POST /api/matrix-account-login (x-www-form-urlencoded)
// fields: agent, account, homeserver, mode=password|token,
// user_id?, password?, token?
// -> 200 { ok: true, user_id }. Error body shape-agnostic: plain text
// today, migrating to RFC 9457 problem+json { detail, … }.
// Token is never echoed; page never re-renders a submitted secret.
//
// 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.
// 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).
// No account name / homeserver / login mode, and no
// live/heartbeat concept for a static PAT — just present/absent.
// Per-tab detail comments live next to their section below.
import { $, el, esc, fmtAgeSecs, renderServerWarnings } from './common.js';
import { createTabStrip } from '@hive/shared/tabs.js';
let agents = [];
// agent name → container running (bool), from /api/state. Cross-referenced by
@ -68,6 +56,40 @@ function renderAgentPicker() {
for (const a of agents) sel.append(el('option', { value: a }, a));
}
// ─── shape-agnostic error-body parsing (shared by both tabs' submit
// handlers) ───────────────────────────────────────────────────────────────
// The BE error-body shape is in transition: today hive-c0re's
// error_response sends a bare plain-text body; the RFC 9457 rework moves it
// to application/problem+json ({ type, title, detail, … }). Read shape-
// agnostically: pull the body once as text, and if it parses as JSON
// surface `detail` (problem+json) → `error`/`title` fallback, else use the
// raw text. A bare HTTP code is the last resort.
async function readErrorBody(resp) {
try {
const raw = (await resp.text()).trim();
if (raw && (raw[0] === '{' || raw[0] === '[')) {
try {
const body = JSON.parse(raw);
return body.detail || body.error || body.title || raw;
} catch { /* not JSON after all — keep the raw text */ }
}
return raw;
} catch {
return '';
}
}
// ─── 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) {
@ -222,25 +244,7 @@ async function submitLogin(e) {
clearSecrets(formEl);
}
} else {
// The BE error-body shape is in transition: today hive-c0re's
// error_response sends a bare plain-text body (e.g. "matrix-account-
// login: password mode needs user_id + password"); the RFC 9457 rework
// moves it to application/problem+json ({ type, title, detail, … }).
// Read shape-agnostically so the FE handles both with no merge-order
// coupling: pull the body once as text, and if it parses as JSON
// surface `detail` (problem+json) → `error`/`title` fallback, else use
// the raw text. A bare HTTP code is the last resort.
let msg = '';
try {
const raw = (await resp.text()).trim();
msg = raw;
if (raw && (raw[0] === '{' || raw[0] === '[')) {
try {
const body = JSON.parse(raw);
msg = body.detail || body.error || body.title || raw;
} catch { /* not JSON after all — keep the raw text */ }
}
} catch { /* fall back to the status code below */ }
const msg = await readErrorBody(resp);
out.className = 'ma-result err';
out.textContent = '✗ ' + (msg || ('login failed (HTTP ' + resp.status + ')'));
clearSecrets(formEl);
@ -254,15 +258,112 @@ async function submitLogin(e) {
}
}
// ─── GITHUB tab ─────────────────────────────────────────────────────────
async function loadGithubStatus(agent) {
const status = $('gh-status');
if (!agent) {
status.replaceChildren(el('p', { class: 'meta' }, 'select an agent to see its github credential status.'));
return;
}
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);
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).'));
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'),
));
}
async function submitGithub(e) {
e.preventDefault();
const formEl = e.target;
const out = $('gh-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 = 'storing…';
try {
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 */ }
if (body.ok) {
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).';
clearSecrets(formEl);
}
} else {
const msg = await readErrorBody(resp);
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).';
} finally {
btn.disabled = false;
btn.textContent = orig;
}
}
// ─── init ─────────────────────────────────────────────────────────────
async function onAgentChange(agent) {
loadAccounts(agent);
loadGithubStatus(agent);
}
async function init() {
await loadState();
renderAgentPicker();
$('ma-agent').addEventListener('change', (e) => loadAccounts(e.target.value));
$('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);
loadAccounts('');
$('gh-form').addEventListener('submit', submitGithub);
createTabStrip(document.getElementById('cred-tabbar'), {
defaultId: 'matrix',
});
onAgentChange('');
}
init();

View file

@ -91,12 +91,12 @@
<span class="home-tile-desc">kept state · container load</span>
</a>
<a class="home-tile" href="/matrix-accounts.html">
<a class="home-tile" href="/credentials.html">
<span class="home-tile-head">
<span class="home-tile-icon" aria-hidden="true">🔑</span>
<span class="home-tile-label">Matrix accounts</span>
<span class="home-tile-label">Credentials</span>
</span>
<span class="home-tile-desc">provision · log in · store per-agent matrix tokens</span>
<span class="home-tile-desc">provision per-agent matrix + github accounts</span>
</a>
<!-- Forge tile: hidden until home.js confirms the hive-forge

View file

@ -1,81 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>hyperhive // M4TR1X ACC0UNTS</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/static/colors.css">
<link rel="stylesheet" href="/static/theme.css">
<link rel="stylesheet" href="/static/common.css">
<link rel="stylesheet" href="/static/matrix-accounts.css">
</head>
<body class="ma-shell">
<!-- Minimal chrome: back link + title, same pattern as settings.html /
core.html. Back link points to the H0M3 hub (served at /). -->
<header class="page-header">
<a class="page-back" href="/">← home</a>
<span class="page-title">M4TR1X ACC0UNTS</span>
</header>
<main class="ma-main">
<p class="meta">provision or log in an <strong>external</strong> matrix account for an agent and store its access token. the token is written to the agent's <code>matrixAccounts.&lt;account&gt;.tokenFile</code> by the host coordinator &mdash; it is never displayed back on this page.</p>
<h3>◇ agent</h3>
<label class="ma-field">
<span>agent</span>
<select id="ma-agent"></select>
</label>
<h3>◇ provisioned accounts</h3>
<p class="meta">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 <em>token is stored</em>, not a live session &mdash; a true online/offline indicator is a follow-up that needs the daemon's account registry.</p>
<div id="ma-list" class="ma-list"><p class="meta">select an agent to see its matrix accounts.</p></div>
<h3>◇ provision / log in</h3>
<form id="ma-form" class="ma-form" autocomplete="off">
<label class="ma-field">
<span>account name</span>
<input type="text" name="account" placeholder="e.g. public" required>
</label>
<label class="ma-field">
<span>homeserver</span>
<input type="text" name="homeserver" placeholder="https://matrix.org" required>
</label>
<fieldset class="ma-mode">
<legend>login method</legend>
<label><input type="radio" name="mode" value="password" checked> password</label>
<label><input type="radio" name="mode" value="token"> existing token</label>
</fieldset>
<div id="ma-pw-fields" class="ma-modefields">
<label class="ma-field">
<span>user id</span>
<input type="text" name="user_id" placeholder="@user:matrix.org" autocomplete="username">
</label>
<label class="ma-field">
<span>password</span>
<input type="password" name="password" autocomplete="new-password">
</label>
</div>
<div id="ma-token-fields" class="ma-modefields" hidden>
<label class="ma-field">
<span>access token</span>
<input type="password" name="token" autocomplete="off">
</label>
<label class="ma-field">
<span>user id <span class="meta">(optional &mdash; derived via whoami)</span></span>
<input type="text" name="user_id" placeholder="@user:matrix.org">
</label>
</div>
<button type="submit" class="btn btn-spawn">log in &amp; store token</button>
<p id="ma-result" class="ma-result" aria-live="polite"></p>
</form>
</main>
<script type="module" src="/static/matrix-accounts.js" defer></script>
</body>
</html>