dashboard: matrix-accounts page — provision/login per-agent external matrix accounts
New standalone H0M3 page (/matrix-accounts.html) and tile. An agent
picker drives a list of that agent's configured matrix accounts (name,
homeserver, token-stored status) and a provision form that logs in by
password or stores an existing token.
Frontend half of the per-agent external matrix-account provisioning
work. Built against the v1 backend contract:
GET /matrix-accounts?agent=<name>
-> { accounts: [ { name, homeserver, token_present } ] }
POST /matrix-account-login (x-www-form-urlencoded, operator-auth)
fields: agent, account, homeserver, mode=password|token,
user_id?, password?, token?
-> 2xx { ok, user_id } | 4xx { error }
The token is never echoed back; secret inputs are cleared on submit.
Token-status dot reflects token-stored, not live session (a true
up/down indicator needs the daemon account registry, a follow-up). The
form carries an experimental notice pending per-account failure
isolation on the matrix daemon.
Blocked from merge on the backend endpoints and the daemon
failure-isolation fix; opening for review + to pin the UI/backend wire
contract.
This commit is contained in:
parent
c187366961
commit
9293fe3ac9
6 changed files with 394 additions and 3 deletions
|
|
@ -200,6 +200,37 @@ omitted — agents share the host netns, so there is no per-container net
|
||||||
counter (per-agent network needs the netns-isolation roadmap in
|
counter (per-agent network needs the netns-isolation roadmap in
|
||||||
`docs/network.md`).
|
`docs/network.md`).
|
||||||
|
|
||||||
|
## M4TR1X ACC0UNTS page (`/matrix-accounts.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.
|
||||||
|
|
||||||
|
An agent picker (populated from `state.agents`) drives a list of that
|
||||||
|
agent's configured accounts — name, homeserver, and a token-status dot —
|
||||||
|
read from `GET /matrix-accounts?agent=<name>` →
|
||||||
|
`{ accounts: [ { name, homeserver, token_present } ] }`. The status
|
||||||
|
reflects only whether a token is **stored** (labelled "token stored",
|
||||||
|
not "online"); a true live up/down indicator needs the matrix daemon's
|
||||||
|
account registry and is a follow-up.
|
||||||
|
|
||||||
|
The provision form (account name, homeserver, login method) posts
|
||||||
|
`POST /matrix-account-login` (`x-www-form-urlencoded`, operator-auth):
|
||||||
|
fields `agent, account, homeserver, mode=password|token, user_id?,
|
||||||
|
password?, token?` → `2xx { ok, user_id }` on success or
|
||||||
|
`4xx { error }` on failure. The host coordinator performs the login
|
||||||
|
(password) or validates the token (`whoami`) and writes the bearer to
|
||||||
|
the agent's `matrixAccounts.<account>.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. Because a bad credential can currently disturb
|
||||||
|
the agent's whole matrix session until per-account failure isolation
|
||||||
|
lands on the daemon, the form carries an explicit experimental notice.
|
||||||
|
|
||||||
## P3RM1SS10NS tab
|
## P3RM1SS10NS tab
|
||||||
|
|
||||||
Per-agent permission configuration. Two sections, each rendered as a
|
Per-agent permission configuration. Two sections, each rendered as a
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ mkdirSync(staticDir(''), { recursive: true });
|
||||||
// follow-up once asset sizes warrant it). esbuild writes each entry
|
// follow-up once asset sizes warrant it). esbuild writes each entry
|
||||||
// to `static/<name>.js` based on the entryPoint basename.
|
// to `static/<name>.js` based on the entryPoint basename.
|
||||||
await build({
|
await build({
|
||||||
entryPoints: [src('tabs.js'), src('flow.js'), src('logs.js'), src('home.js'), src('settings.js'), src('stats.js'), src('core.js')],
|
entryPoints: [src('tabs.js'), src('flow.js'), src('logs.js'), src('home.js'), src('settings.js'), src('stats.js'), src('core.js'), src('matrix-accounts.js')],
|
||||||
outdir: staticDir(''),
|
outdir: staticDir(''),
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
|
|
@ -94,7 +94,7 @@ await build({
|
||||||
// so a swap replaces only it) + theme.css (the semantic derivation
|
// so a swap replaces only it) + theme.css (the semantic derivation
|
||||||
// layer) + common.css (shared typography, badges, buttons, inbox, side
|
// layer) + common.css (shared typography, badges, buttons, inbox, side
|
||||||
// panel) plus its own page-specific bundle.
|
// 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']) {
|
for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', 'flow.css', 'logs.css', 'home.css', 'settings.css', 'stats.css', 'core.css', 'matrix-accounts.css']) {
|
||||||
await build({
|
await build({
|
||||||
entryPoints: [src(entry)],
|
entryPoints: [src(entry)],
|
||||||
outfile: staticDir(entry),
|
outfile: staticDir(entry),
|
||||||
|
|
@ -104,7 +104,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']) {
|
for (const html of ['index.html', 'dashboard.html', 'flow.html', 'logs.html', 'settings.html', 'stats.html', 'core.html', 'matrix-accounts.html']) {
|
||||||
copyFileSync(src(html), dist(html));
|
copyFileSync(src(html), dist(html));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,14 @@
|
||||||
<span class="home-tile-desc">rebuild queue · meta inputs · kept state · container load</span>
|
<span class="home-tile-desc">rebuild queue · meta inputs · kept state · container load</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a class="home-tile" href="/matrix-accounts.html">
|
||||||
|
<span class="home-tile-head">
|
||||||
|
<span class="home-tile-icon" aria-hidden="true">🔑</span>
|
||||||
|
<span class="home-tile-label">Matrix accounts</span>
|
||||||
|
</span>
|
||||||
|
<span class="home-tile-desc">provision · log in · store per-agent matrix tokens</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
<!-- Forge tile: hidden until home.js confirms the hive-forge
|
<!-- Forge tile: hidden until home.js confirms the hive-forge
|
||||||
container is up (state.forge_present); home.js also fills the
|
container is up (state.forge_present); home.js also fills the
|
||||||
href from state.forge_public_url (or the :3000 fallback), so
|
href from state.forge_public_url (or the :3000 fallback), so
|
||||||
|
|
|
||||||
87
frontend/packages/dashboard/src/matrix-accounts.css
Normal file
87
frontend/packages/dashboard/src/matrix-accounts.css
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
/* M4TR1X ACC0UNTS page (/matrix-accounts.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. */
|
||||||
|
|
||||||
|
.ma-main {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem 1.25rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ma-experimental {
|
||||||
|
border: 1px solid var(--amber);
|
||||||
|
background: color-mix(in srgb, var(--amber) 10%, transparent);
|
||||||
|
color: var(--fg);
|
||||||
|
padding: 0.6rem 0.85rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 0.8rem 0 1.4rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ma-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin: 0.55rem 0;
|
||||||
|
}
|
||||||
|
.ma-field > span { font-size: 0.8rem; color: var(--muted); }
|
||||||
|
.ma-field input,
|
||||||
|
.ma-field select {
|
||||||
|
padding: 0.4rem 0.5rem;
|
||||||
|
background: var(--bg-elev);
|
||||||
|
color: var(--fg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
.ma-field input:focus,
|
||||||
|
.ma-field select:focus {
|
||||||
|
outline: none;
|
||||||
|
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; margin: 0; }
|
||||||
|
.ma-account {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
padding: 0.45rem 0.2rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.ma-dot {
|
||||||
|
width: 0.6rem;
|
||||||
|
height: 0.6rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.ma-dot.ok { background: var(--green); }
|
||||||
|
.ma-dot.absent { background: var(--muted); }
|
||||||
|
.ma-name { font-weight: 600; color: var(--fg); }
|
||||||
|
.ma-hs { color: var(--muted); font-size: 0.85rem; }
|
||||||
|
.ma-status { margin-left: auto; font-size: 0.8rem; }
|
||||||
|
.ma-status.ok { color: var(--green); }
|
||||||
|
.ma-status.absent { color: var(--muted); }
|
||||||
|
|
||||||
|
.ma-result {
|
||||||
|
margin-top: 0.7rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
min-height: 1.2em;
|
||||||
|
}
|
||||||
|
.ma-result.ok { color: var(--green); }
|
||||||
|
.ma-result.err { color: var(--red); }
|
||||||
|
|
||||||
|
.ma-list .err { color: var(--red); font-size: 0.9rem; }
|
||||||
85
frontend/packages/dashboard/src/matrix-accounts.html
Normal file
85
frontend/packages/dashboard/src/matrix-accounts.html
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<!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.<account>.tokenFile</code> by the host coordinator — it is never displayed back on this page.</p>
|
||||||
|
|
||||||
|
<div class="ma-experimental" role="note">
|
||||||
|
⚠ <strong>experimental.</strong> a wrong password or token entered here can currently disrupt the target agent's whole matrix session until per-account failure isolation lands on the daemon. use with care on a live agent.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>◇ agent</h3>
|
||||||
|
<label class="ma-field">
|
||||||
|
<span>agent</span>
|
||||||
|
<select id="ma-agent"></select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<h3>◇ configured accounts</h3>
|
||||||
|
<p class="meta">status reflects whether a <em>token is stored</em>, not a live session — 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 — 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 & 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>
|
||||||
180
frontend/packages/dashboard/src/matrix-accounts.js
Normal file
180
frontend/packages/dashboard/src/matrix-accounts.js
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
// M4TR1X ACC0UNTS page entry (/matrix-accounts.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 (v1):
|
||||||
|
// GET /matrix-accounts?agent=<name>
|
||||||
|
// -> { accounts: [ { name, homeserver, token_present: bool } ] }
|
||||||
|
// POST /matrix-account-login (x-www-form-urlencoded, operator-auth)
|
||||||
|
// fields: agent, account, homeserver, mode=password|token,
|
||||||
|
// user_id?, password?, token?
|
||||||
|
// -> 2xx { ok: true, user_id } on success
|
||||||
|
// -> 4xx { error: "<msg>" } on failure
|
||||||
|
// The token is NEVER echoed back in any response, and this page never
|
||||||
|
// re-renders a submitted secret.
|
||||||
|
//
|
||||||
|
// Live up/down (a true green/red dot) needs the daemon's account
|
||||||
|
// registry; until that follow-up lands the dot only reflects whether a
|
||||||
|
// token is STORED, labelled "token stored" rather than "online".
|
||||||
|
//
|
||||||
|
// Hard dependency: per-account failure isolation on the matrix daemon.
|
||||||
|
// Until that lands a bad credential entered here can crash the agent's
|
||||||
|
// whole matrix session, so the form carries an explicit experimental
|
||||||
|
// notice in the markup.
|
||||||
|
|
||||||
|
import { $, el, esc, renderServerWarnings } from './common.js';
|
||||||
|
|
||||||
|
let agents = [];
|
||||||
|
|
||||||
|
async function loadState() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/state');
|
||||||
|
if (!resp.ok) return;
|
||||||
|
const s = await resp.json();
|
||||||
|
renderServerWarnings(s.server_warnings);
|
||||||
|
agents = (s.agents || [])
|
||||||
|
.map((a) => (typeof a === 'string' ? a : a && a.name))
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort();
|
||||||
|
} catch {
|
||||||
|
// best-effort: on a failed state read the picker renders empty
|
||||||
|
// ("— no agents —") and the submit guard blocks until an agent is
|
||||||
|
// selected, rather than guessing a roster.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAgentPicker() {
|
||||||
|
const sel = $('ma-agent');
|
||||||
|
sel.replaceChildren();
|
||||||
|
if (!agents.length) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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('/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 || [];
|
||||||
|
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;
|
||||||
|
ul.append(el('li', { class: 'ma-account' },
|
||||||
|
el('span', {
|
||||||
|
class: 'ma-dot ' + (present ? 'ok' : 'absent'),
|
||||||
|
title: present ? 'token stored' : 'no token yet',
|
||||||
|
}),
|
||||||
|
el('span', { class: 'ma-name' }, acc.name || '(unnamed)'),
|
||||||
|
el('span', { class: 'ma-hs' }, acc.homeserver || '—'),
|
||||||
|
el('span', { class: 'ma-status ' + (present ? 'ok' : 'absent') },
|
||||||
|
present ? 'token stored ✓' : 'no token'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
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"]')
|
||||||
|
.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 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('/matrix-account-login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams(fd),
|
||||||
|
});
|
||||||
|
let body = {};
|
||||||
|
try { body = await resp.json(); } catch { /* tolerate non-JSON error pages */ }
|
||||||
|
|
||||||
|
if (resp.ok && 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 = '✗ ' + (body.error || ('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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
await loadState();
|
||||||
|
renderAgentPicker();
|
||||||
|
$('ma-agent').addEventListener('change', (e) => loadAccounts(e.target.value));
|
||||||
|
document.querySelectorAll('input[name="mode"]')
|
||||||
|
.forEach((r) => r.addEventListener('change', toggleModeFields));
|
||||||
|
toggleModeFields();
|
||||||
|
$('ma-form').addEventListener('submit', submitLogin);
|
||||||
|
loadAccounts('');
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
Loading…
Reference in a new issue