agent: wire Header/StatusChips to real /api/state polling

- useAgentState hook: polls GET /api/state (4s interval for now — see
  its file comment for why this isn't yet the SSE-triggered + login-
  only-timer cadence the old page used; that lands with the live
  stream + term-input commit, which is when clobbering the operator's
  in-progress input actually becomes a risk).
- format.ts: fmtTokens/fmtAge, same output shapes as app.js's.
- modelEffort.ts: POST /api/model + /api/effort (same-origin, plain
  fetch).
- dashboardBase.ts + pauseAction.ts: pause/resume POST to the
  *dashboard's* origin via a real <form> submit, kept unchanged from
  app.js — a cross-origin fetch needs CORS headers hive-c0re doesn't
  send, a form submit sidesteps that same as it already did.
- Root.tsx: wires it all together, including app.js's "any non-online
  status forces the turn-state badge to offline" behavior.

Screenshot-verified against a mock GET /api/state (real fetch, not
hardcoded props) + the real agent.css/theme.css/colors.css.

Builds + tsc --noEmit clean.
This commit is contained in:
iris 2026-08-28 02:07:17 +02:00
commit 908f479372
6 changed files with 239 additions and 26 deletions

View file

@ -0,0 +1,11 @@
// Resolves the host dashboard's origin, ported unchanged from app.js's
// `setHeader` — pause/resume are host-side (hive-c0re) actions, not
// this agent's own `/api/*`, so they need the *dashboard's* origin, not
// a relative path. Behind the gateway this page lives at
// `/agent/<name>/` on the dashboard's own origin (`location.origin`);
// accessed directly, it's the same host on `dashboardPort`.
export function resolveDashboardBase(dashboardPort: number): string {
return location.pathname.startsWith('/agent/')
? location.origin + '/'
: `${location.protocol}//${location.hostname}:${dashboardPort}/`;
}

View file

@ -0,0 +1,17 @@
// Formatting helpers ported from app.js's `fmtTokens`/`fmtAge` (same
// output shapes — this page's operators are used to reading them).
export function fmtTokens(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
if (n >= 1_000) return Math.round(n / 1000) + 'k';
return String(n);
}
export function fmtAge(ms: number): string {
const s = Math.floor(ms / 1000);
if (s < 60) return s + 's';
const m = Math.floor(s / 60);
if (m < 60) return m + 'm ' + (s % 60) + 's';
const h = Math.floor(m / 60);
return h + 'h ' + (m % 60) + 'm';
}

View file

@ -0,0 +1,25 @@
// POST `/api/model` / `/api/effort` — same-origin (this agent's own
// backend), so a plain `fetch` is safe unlike pause/resume (see
// pauseAction.ts). `redirect: 'manual'` + the broad "ok" check below is
// ported from app.js unchanged — the endpoints don't actually redirect
// today, but treating an opaque redirect as success costs nothing and
// matches the existing contract exactly.
async function post(path: string, field: string, value: string): Promise<{ ok: boolean; detail?: string }> {
try {
const resp = await fetch(path, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ [field]: value }),
redirect: 'manual',
});
const ok = resp.ok || resp.type === 'opaqueredirect' || (resp.status >= 200 && resp.status < 400);
if (ok) return { ok: true };
const detail = await resp.text().catch(() => '');
return { ok: false, detail: `http ${resp.status}${detail ? ' — ' + detail : ''}` };
} catch (err) {
return { ok: false, detail: err instanceof Error ? err.message : String(err) };
}
}
export const postModel = (name: string) => post('api/model', 'model', name);
export const postEffort = (level: string) => post('api/effort', 'effort', level);

View file

@ -0,0 +1,16 @@
// Pause/resume POST to the *dashboard's* origin (hive-c0re, not this
// agent's own `/api/*`) — see dashboardBase.ts. A real `<form>` submit
// rather than `fetch`, kept unchanged from app.js: accessed directly
// (not through the gateway proxy), the dashboard is a different origin/
// port, and a cross-origin `fetch` POST needs the response readable
// under CORS to report success/failure — a full navigation form submit
// sidesteps that (the browser reloads to whatever the endpoint
// returns) without needing hive-c0re to grow CORS headers for what's
// otherwise a same-origin action behind the gateway.
export function submitPauseResume(dashboardBase: string, label: string, verb: 'pause' | 'resume'): void {
const form = document.createElement('form');
form.method = 'POST';
form.action = `${dashboardBase}api/${verb}/${label}`;
document.body.appendChild(form);
form.submit();
}