Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui, hive dashboard, agent page: one profile menu with the signed-in user, avatar and authelia links

The swarm UI's user menu moves to @hive/shared and takes the whoami URL and
authelia's URL as props; the swarm UI keeps a thin wrapper, and the hive
dashboard and the per-agent page mount the same component. The dashboard's
nginx vhost proxies /api/whoami to authelia (the agent page is served from
the same vhost), and hive-c0re's state carries authelia_public_url, set from
services.hyperhive.swarm.authelia.url. The agent page shows the menu only
behind the gateway.
This commit is contained in:
iris 2026-10-25 15:05:35 +01:00 • committed by mara
commit cd41e1d510
19 changed files with 417 additions and 154 deletions

View file

@ -11,6 +11,7 @@ import { useApplyThemeOverride } from "@hive/shared/theme-apply.js";
import { useApplyMotionOverride } from "@hive/shared/motion-apply.js";
import { Header } from "./components/Header.js";
import { MetaNav } from "./components/MetaNav.js";
import { AgentUserMenu } from "./components/AgentUserMenu.js";
import { ExpandDetailsSetting } from "./components/ExpandDetailsSetting.js";
import { HideDebugSetting } from "@hive/shared/hide-debug-setting.js";
import { StatusChips } from "./components/StatusChips.js";
@ -176,6 +177,11 @@ export function Root() {
<ExpandDetailsSetting />
<HideDebugSetting />
</SettingsMenu>
{state ? (
<AgentUserMenu
dashboardBase={resolveDashboardBase(state.dashboard_port)}
/>
) : null}
</>
);
// Kept mounted regardless of `openPanel` (open/closed toggles just the

View file

@ -0,0 +1,44 @@
// <AgentUserMenu> — the profile menu (`@hive/shared/user-menu.js`) in this
// page's header. Only behind the gateway: the page is then on the dashboard's
// own origin, where the session cookie is valid and nginx proxies
// `/api/whoami` to authelia. Opened directly on the agent's own port there is
// no sign-in to show, so the page has no menu.
//
// Authelia's URL, for the settings and sign-out links, comes from the
// dashboard's `/api/state` (`authelia_public_url`), the one place hive-c0re
// states it.
import { useEffect, useState } from "preact/hooks";
import { UserMenu } from "@hive/shared/user-menu.js";
import "@hive/shared/user-menu.css";
import { isBehindGateway } from "../lib/dashboardBase.js";
export function AgentUserMenu({ dashboardBase }: { dashboardBase: string }) {
const behind = isBehindGateway();
const [autheliaUrl, setAutheliaUrl] = useState<string | null>(null);
useEffect(() => {
if (!behind) return;
let cancelled = false;
fetch(`${dashboardBase}api/state`)
.then((r) =>
r.ok ? r.json() : Promise.reject(new Error(`http ${r.status}`)),
)
.then((state: { authelia_public_url?: string | null }) => {
if (!cancelled) setAutheliaUrl(state.authelia_public_url ?? null);
})
.catch(() => {
/* cosmetic only: the menu then shows the name without the links */
});
return () => {
cancelled = true;
};
}, [behind, dashboardBase]);
if (!behind) return null;
return (
<UserMenu
whoamiUrl={`${dashboardBase}api/whoami`}
autheliaUrl={autheliaUrl}
/>
);
}

View file

@ -4,8 +4,13 @@
// 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`.
/** Whether this page is served through the gateway, on the dashboard's origin. */
export function isBehindGateway(): boolean {
return location.pathname.startsWith("/agent/");
}
export function resolveDashboardBase(dashboardPort: number): string {
return location.pathname.startsWith("/agent/")
return isBehindGateway()
? location.origin + "/"
: `${location.protocol}//${location.hostname}:${dashboardPort}/`;
}