diff --git a/frontend/packages/swarm-ui/src/shell/Shell.tsx b/frontend/packages/swarm-ui/src/shell/Shell.tsx
index 07ca0712..cda6b4fe 100644
--- a/frontend/packages/swarm-ui/src/shell/Shell.tsx
+++ b/frontend/packages/swarm-ui/src/shell/Shell.tsx
@@ -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 `
` 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(null);
+
+ // Fetched once here, not per-page: every route mounts inside one
+ // ``, 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 (