feat(#1292): add GET /api/dashboard-state to hive-ag3nt

hive-ag3nt now exposes a lean DashboardState endpoint that returns the
agent-owned fields the dashboard card needs:

  - status_text / status_set_at  (from hyperhive-status on disk)
  - ctx_tokens / context_window_tokens  (from Bus)
  - rate_limited  (from Bus)
  - links  (from agent_links — includes screen link for GUI agents,
            which c0re's disk-based fallback cannot determine)

The dashboard fetches `${containerBase}/api/dashboard-state` instead of
the previous c0re proxy `/api/agent/{name}/links`. For gateway deployments
this is a same-origin call to the agent via the gateway's unix-socket
upstream; for direct TCP it hits the agent port directly. Both paths fail
gracefully (empty strip) when the agent is starting up.

The main behaviour fix: GUI agents now show the screen link in the dashboard
nav strip. c0re's build_nav_links reads /etc/hyperhive/gui.json from outside
the container (not possible), so it always omitted the screen link.
This commit is contained in:
iris 2026-06-04 19:16:40 +02:00
commit 40845c21cd
2 changed files with 92 additions and 11 deletions

View file

@ -724,11 +724,12 @@ window.marked = marked;
head.append(
el('a', { class: 'name', href: url, target: '_blank', rel: 'noopener' }, c.name),
);
// Icon-only nav strip — populated async from `/api/agent/{name}/links`,
// a same-origin proxy that forwards the agent backend's own link list
// (stats / screen-if-gui / forge profile / agent-configs / extras).
// The agent backend is the single source of truth; no hardcoded link
// list here. DOM-built — link strings come from the agent's process
// Icon-only nav strip — populated async from the agent's own
// `GET /api/dashboard-state` (via gateway when enabled, direct
// TCP otherwise). The agent is the single source of truth for its
// link list: stats / screen (GUI agents only — c0re's disk-based
// fallback cannot detect this) / forge profile / agent-configs /
// extras. DOM-built — link strings come from the agent's process
// and must never reach the HTML parser.
const navStrip = el('span', { class: 'nav-strip' });
head.append(navStrip);
@ -742,11 +743,14 @@ window.marked = marked;
? `/agent/${encodeURIComponent(c.name)}`
: `http://${hostname}:${c.port}`;
if (c.running) {
fetch(`/api/agent/${encodeURIComponent(c.name)}/links`)
.then((r) => (r.ok ? r.json() : []))
.then((links) => {
if (!Array.isArray(links)) return;
for (const lnk of links) {
// Fetch the lean dashboard-state snapshot from the agent directly.
// Fails gracefully (empty strip) when the agent is starting up
// or the gateway is not yet routing to it.
fetch(`${containerBase}/api/dashboard-state`)
.then((r) => (r.ok ? r.json() : null))
.then((ds) => {
if (!ds || !Array.isArray(ds.links)) return;
for (const lnk of ds.links) {
const href = lnk.kind === 'forge' ? forgeBase + (lnk.url || '')
: lnk.kind === 'external' ? (lnk.url || '')
: /* container */ containerBase + '/' + (lnk.url || '');
@ -762,7 +766,7 @@ window.marked = marked;
navStrip.append(a);
}
})
.catch(() => { /* graceful: agent down → no strip */ });
.catch(() => { /* graceful: agent starting / gateway miss → no strip */ });
}
// Status / runtime badges. Pending transients always win
// (start / stop / restart / rebuild is in progress). Otherwise,