matrix-accounts: 3-state live status dot + user_id + snapshot freshness
Frontend for the BE-4 snapshot (#1702): GET /api/matrix-accounts now returns per-account `live` + `user_id` and a top-level `as_of_unix`. Render a 3-state dot instead of the v1 token-present-only one: - green (live + container running) — online - amber (live + container DOWN) — stale: the page already loads /api/state containers, so cross-reference running state; a container that's down means the daemon is down, so a "live" snapshot there is stale - amber (token_present + !live) — provisioned but offline - grey (no token) — not provisioned The daemon rewrites its snapshot only on (re)start, so `as_of_unix` is "live as of", not a heartbeat — surfaced as a tooltip. We deliberately do NOT dim a green purely on snapshot age (an old as_of is ambiguous: stable uptime vs dead daemon); the container cross-ref catches the definitive down case, and true daemon-up-but-client-dead detection is the daemon-heartbeat follow-up. user_id is shown next to the account name. When `live` is absent (v1 backend not yet deployed) the dot falls back to the token-present rendering, so this is safe to ship independent of the backend deploy. FE half of #1702 BE-4; pairs with the backend PR.
This commit is contained in:
parent
ad6b39b425
commit
06b2bd8b72
2 changed files with 87 additions and 23 deletions
|
|
@ -57,12 +57,22 @@
|
|||
border-radius: 50%;
|
||||
flex: none;
|
||||
}
|
||||
.ma-dot.ok { background: var(--green); }
|
||||
/* `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 { background: var(--green); }
|
||||
.ma-dot.offline,
|
||||
.ma-dot.stale { background: var(--amber); }
|
||||
.ma-dot.absent { background: var(--muted); }
|
||||
.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; }
|
||||
.ma-status { margin-left: auto; font-size: 0.8rem; }
|
||||
.ma-status.ok { color: var(--green); }
|
||||
.ma-status.ok,
|
||||
.ma-status.live { color: var(--green); }
|
||||
.ma-status.offline,
|
||||
.ma-status.stale { color: var(--amber); }
|
||||
.ma-status.absent { color: var(--muted); }
|
||||
|
||||
.ma-result {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@
|
|||
// account and store its access token, without editing the agent's config
|
||||
// repo. Companion to the multi-account harness support.
|
||||
//
|
||||
// Backend contract (v1):
|
||||
// Backend contract (v2 — BE-4 adds live/user_id/as_of from the daemon snapshot):
|
||||
// GET /api/matrix-accounts?agent=<name>
|
||||
// -> { accounts: [ { name, homeserver, token_present: bool } ] }
|
||||
// POST /matrix-account-login (x-www-form-urlencoded, operator-auth)
|
||||
// -> { accounts: [ { name, homeserver: string|null, token_present: bool,
|
||||
// live: bool, user_id: string|null } ],
|
||||
// as_of_unix: int|null }
|
||||
// POST /api/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
|
||||
|
|
@ -15,15 +17,30 @@
|
|||
// 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". The
|
||||
// account list shows what is provisioned (has a stored token), so a
|
||||
// config-declared-but-unprovisioned account appears only once provisioned.
|
||||
// Live status dot (coordinated with the BE-4 snapshot): the daemon publishes a
|
||||
// host-visible snapshot rewritten on each (re)start, so `live` means "restored
|
||||
// as of `as_of_unix`", not a real-time heartbeat. We render 3 states —
|
||||
// green (live + container running) = online
|
||||
// amber (live + container DOWN) = stale (container down ⟹ daemon down)
|
||||
// amber (token_present + !live) = provisioned but offline
|
||||
// grey (no token) = not provisioned
|
||||
// `as_of_unix` is tooltipped ("live as of N ago") so a green isn't read as a
|
||||
// real-time guarantee. We deliberately do NOT dim a green purely on snapshot
|
||||
// age: an old `as_of` is ambiguous (stable long uptime vs dead daemon), and the
|
||||
// container cross-ref already catches the definitive "down" case; true
|
||||
// daemon-up-but-client-dead detection is a daemon-heartbeat follow-up. When
|
||||
// `live` is absent (v1 backend not yet deployed) the dot falls back to the
|
||||
// token-present rendering.
|
||||
|
||||
import { $, el, esc, renderServerWarnings } from './common.js';
|
||||
import { $, el, esc, fmtAgeSecs, renderServerWarnings } from './common.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 {
|
||||
|
|
@ -32,12 +49,14 @@ async function loadState() {
|
|||
const s = await resp.json();
|
||||
renderServerWarnings(s.server_warnings);
|
||||
// `/api/state` exposes the live roster under `containers` (each entry an
|
||||
// object carrying `.name`); there is no top-level `agents` field, so the
|
||||
// picker stays compatible with both string + object shapes defensively.
|
||||
agents = (s.containers || [])
|
||||
.map((a) => (typeof a === 'string' ? a : a && a.name))
|
||||
.filter(Boolean)
|
||||
.sort();
|
||||
// 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))
|
||||
.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,6 +93,18 @@ async function loadAccounts(agent) {
|
|||
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 rewrites its snapshot only on (re)start, so this is "live as of"
|
||||
// the last publish, not a heartbeat — surface it so a green isn't read as a
|
||||
// real-time guarantee.
|
||||
const asOfText = asOf != null
|
||||
? 'matrix snapshot · live as of '
|
||||
+ fmtAgeSecs(Math.max(0, Math.floor(Date.now() / 1000) - asOf)) + ' ago'
|
||||
: 'no daemon snapshot yet';
|
||||
list.replaceChildren();
|
||||
if (!accounts.length) {
|
||||
list.append(el('p', { class: 'meta' }, 'no matrix accounts configured for this agent.'));
|
||||
|
|
@ -82,15 +113,38 @@ async function loadAccounts(agent) {
|
|||
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) {
|
||||
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 ' + (present ? 'ok' : 'absent'),
|
||||
title: present ? 'token stored' : 'no token yet',
|
||||
}),
|
||||
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 ' + (present ? 'ok' : 'absent') },
|
||||
present ? 'token stored ✓' : 'no token'),
|
||||
el('span', { class: 'ma-status ' + cls, title: asOfText }, statusText),
|
||||
));
|
||||
}
|
||||
list.append(ul);
|
||||
|
|
|
|||
Loading…
Reference in a new issue