agent: meta-nav, favicon fallback, document.title

Ports the 3 remaining gaps argus's lost-functionality audit found beyond
login: the header meta-nav strip (stats/screen/forge/config +
hyperhive.dashboardLinks extras — the only on-page path to those, not
just polish), the header icon's /favicon.svg fallback on a broken
image, and the browser tab title update. All three are straight ports
of app.js's existing logic (refreshState's meta-links loop,
bindHeaderIconFallback, setHeader's document.title block), same
kind -> URL resolution rules, same dataset.fallback loop guard, same
qualified_label/hive_name fallback chain — no new backend fields
needed, hive_agent::web_ui::state::StateSnapshot already serves
links/forge_public_url and useAgentState's whole-payload cast already
threads them to Root.

New <MetaNav> component (reuses agent.css's existing .agent-nav/
.agent-nav-link rules verbatim, no new CSS) renders in Header's title
row via a new nav prop. Favicon fallback lives in Header itself
(onError handler on the .agent-icon <img>). document.title is a
useEffect in Root keyed on label/qualified_label/hive_name.

tsc --noEmit clean, build clean, both pre-push lints clean. Screenshot
verifies meta-nav renders 4 links (stats/forge/config/extra, forge
kind resolving against a mocked forge_public_url) and the favicon
fallback firing against a deliberately-404'd /icon.
This commit is contained in:
iris 2026-08-28 20:54:11 +02:00
commit df78236ce4
4 changed files with 102 additions and 4 deletions

View file

@ -0,0 +1,44 @@
// <MetaNav> — the header's meta-nav strip (`<nav id="meta-links">` in
// the old markup): stats/screen/forge/config + any
// `hyperhive.dashboardLinks` extras, sourced from the backend's
// `agent_links()` (the single source of truth — same list also feeds
// the dashboard card's icon strip). See
// docs/web-ui/agent.md::Header for the kind → URL resolution table
// this ports verbatim from app.js's `refreshState` (lines ~1216-1244):
// `container` → same-origin path (this page is itself container-local);
// `forge` → `forgePublicUrl + url`, and the link is dropped entirely
// when `forgePublicUrl` is unset (never guessed from `<host>:3000`);
// `external` → already absolute. Plain JSX text (not `innerHTML`), so
// this is XSS-safe by construction the same way app.js's `el()`-built
// anchors were — no new escaping to get right.
import type { AgentLink } from '../types.js';
export interface MetaNavProps {
links: AgentLink[];
forgePublicUrl: string | null;
}
export function MetaNav({ links, forgePublicUrl }: MetaNavProps) {
const visible = links.filter((lnk) => lnk.kind !== 'forge' || forgePublicUrl);
if (visible.length === 0) return null;
return (
<nav class="agent-nav" id="meta-links">
{visible.map((lnk) => {
const href = lnk.kind === 'forge' ? `${forgePublicUrl}${lnk.url}` : lnk.url;
return (
<a
key={lnk.url}
class="agent-nav-link"
href={href}
target="_blank"
rel="noopener"
title={lnk.label}
>
{`${lnk.icon} ${lnk.label}`.trim()}
</a>
);
})}
</nav>
);
}