swarm-ui: show the swarm's name as the page title and top-left brand

New GET /api/swarm on swarm-controller, backed by
services.hyperhive.swarm.name (SWARM_CONTROLLER_NAME env var, same
optionalAttrs-gated-on-option-resolving shape queueEnv/forgeEnv/etc.
already use). swarm-ui's <Shell> fetches it once and sets both
document.title and the header's brand text; falls back to the
existing static "hyperhive swarm" label when the operator never set
a name or the fetch fails.

Extracted the swarm-queue connect block out of main() into its own
connect_status_reader() fn to keep main() under clippy's line-count
lint after adding the new field wiring — no behavior change, same
comments moved as-is.
This commit is contained in:
iris 2026-08-18 17:58:30 +02:00 committed by mara
commit 3643eccf22
3 changed files with 161 additions and 44 deletions

View file

@ -12,6 +12,7 @@
// has exactly one place that needs to know its own nav, and this is
// it. Grows additively as real routes land (a swarm-wide agent roster
// is next); no speculative entries.
import { useEffect, useState } from 'preact/hooks';
import type { ComponentChildren } from 'preact';
import { Link, useRoute } from 'wouter-preact';
import { LinksMenu } from './LinksMenu.js';
@ -24,6 +25,12 @@ const NAV_ITEMS: { href: string; label: string }[] = [
{ href: '/components', label: 'components' },
];
// Static fallback — matches `index.html`'s `<title>` default, so a page
// never flashes something else before the fetch below resolves, and an
// operator who never set `services.hyperhive.swarm.name` sees the exact
// same generic label the pre-fetch page already showed, not a blank.
const DEFAULT_BRAND = 'hyperhive swarm';
function NavLink({ href, label }: { href: string; label: string }) {
const [active] = useRoute(href);
return (
@ -34,10 +41,34 @@ function NavLink({ href, label }: { href: string; label: string }) {
}
export function Shell({ children }: { children: ComponentChildren }) {
const [swarmName, setSwarmName] = useState<string | null>(null);
// Fetched once here, not per-page: every route mounts inside one
// `<Shell>`, and the swarm's name doesn't change within a page
// visit. A fetch failure is silently ignored — `swarmName` just stays
// `null` and the header/title fall back to `DEFAULT_BRAND`, same as
// an operator who never configured a name; this label is cosmetic,
// not worth an `ApiErrorPanel`.
useEffect(() => {
(async () => {
const r = await fetch('/api/swarm');
if (!r.ok) return;
const data = (await r.json()) as { name: string | null };
if (data.name) setSwarmName(data.name);
})().catch(() => {
/* cosmetic only — see comment above */
});
}, []);
const brand = swarmName ?? DEFAULT_BRAND;
useEffect(() => {
document.title = brand;
}, [brand]);
return (
<div class="shell">
<header class="shell-header">
<span class="shell-brand">hyperhive swarm</span>
<span class="shell-brand">{brand}</span>
<nav class="shell-nav">
{NAV_ITEMS.map((item) => (
<NavLink key={item.href} href={item.href} label={item.label} />