feat(frontend): add the H0M3 menu-hub page (#1464 step 1)

First step of the dashboard-tab consolidation (#1464): a static menu
page linking to every top-level surface, so the tab strip can later
shed its `→` page-links and the dashboard can graduate Stats/Settings
to their own pages.

- dashboard/src/home.{html,css,js}: a responsive grid of link tiles
  (D4SHB04RD, FL0W, L0GS, M4TR1X). Pure portal — no tabbar/SSE. Colours
  from the shared theme.css, base typography from common.css. The
  Matrix tile is hidden until home.js confirms `matrix_gui_enabled`
  (same gating as the dashboard's M4TR1X tab); home.js also fills the
  swarm/hive identity line.
- build.mjs: emit home.{html,css,js}.

Served at `/home.html` for now (additive — reachable via the existing
ServeDir, links to surfaces at their current routes). Promoting it to
`/` (and relocating the dashboard to `/dashboard` + wiring `← home`
back-links) is the next step — a route swap that touches hive-c0re's
static router, coordinating with damocles. Deliberately decoupled so
this page ships standalone without conflicting with the in-flight
tabs.js change (#1449/#1451).

Part of #1464.
This commit is contained in:
iris 2026-06-06 11:06:39 +02:00 committed by mara
commit b0eb824cfa
4 changed files with 150 additions and 3 deletions

View file

@ -43,7 +43,7 @@ mkdirSync(staticDir(''), { recursive: true });
// follow-up once asset sizes warrant it). esbuild writes each entry
// to `static/<name>.js` based on the entryPoint basename.
await build({
entryPoints: [src('tabs.js'), src('flow.js'), src('logs.js')],
entryPoints: [src('tabs.js'), src('flow.js'), src('logs.js'), src('home.js')],
outdir: staticDir(''),
bundle: true,
format: 'esm',
@ -81,7 +81,7 @@ await build({
// (the standalone Catppuccin palette — kept its own file so a theme
// swap replaces only it) + common.css (shared typography, badges,
// buttons, inbox, side panel) plus its own page-specific bundle.
for (const entry of ['theme.css', 'common.css', 'dashboard.css', 'flow.css', 'logs.css']) {
for (const entry of ['theme.css', 'common.css', 'dashboard.css', 'flow.css', 'logs.css', 'home.css']) {
await build({
entryPoints: [src(entry)],
outfile: staticDir(entry),
@ -91,7 +91,7 @@ for (const entry of ['theme.css', 'common.css', 'dashboard.css', 'flow.css', 'lo
});
}
for (const html of ['index.html', 'flow.html', 'logs.html']) {
for (const html of ['index.html', 'flow.html', 'logs.html', 'home.html']) {
copyFileSync(src(html), dist(html));
}

View file

@ -0,0 +1,59 @@
/* H0M3 the menu-hub page (#1464). Minimal portal styling: a banner
+ a responsive grid of link tiles. Colours come from theme.css (the
shared palette); base typography from common.css. When the shared
page-chrome component lands (#1464 phase 2 / #1451) the banner + tile
styling can fold into it. */
body.home-shell {
margin: 0;
padding: 0 1.5em 2em;
min-height: 100vh;
}
.home-header {
text-align: center;
padding: 1.5em 0 0.5em;
}
.home-header .banner {
font-size: 1.05em;
}
.home-main {
max-width: 60em;
margin: 1.5em auto 0;
}
.home-menu {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16em, 1fr));
gap: 1em;
}
.home-tile {
display: flex;
flex-direction: column;
gap: 0.35em;
padding: 1.1em 1.2em;
border: 1px solid var(--purple-dim);
border-radius: 6px;
background: rgba(24, 24, 37, 0.55);
text-decoration: none;
color: var(--fg);
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease,
box-shadow 0.15s ease;
}
.home-tile:hover {
border-color: var(--purple);
background: rgba(203, 166, 247, 0.06);
box-shadow: 0 -2px 14px -6px rgba(203, 166, 247, 0.5);
}
.home-tile-label {
font-weight: bold;
letter-spacing: 0.08em;
color: var(--purple);
}
.home-tile-desc {
font-size: 0.85em;
color: var(--muted);
}

View file

@ -0,0 +1,55 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>hyperhive // h0m3</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/static/theme.css">
<link rel="stylesheet" href="/static/common.css">
<link rel="stylesheet" href="/static/home.css">
</head>
<body class="home-shell">
<!-- H0M3: the menu hub. A plain grid of links to every top-level
surface. Per #1464 this becomes the page served at `/` once the
route swap lands (dashboard relocates to /dashboard); for now it
lives at /home.html and links to the surfaces at their current
routes. No tabbar / SSE — it's a static portal. -->
<header class="home-header">
<p class="banner-thin" id="hive-identity" hidden></p>
<pre class="banner">░▒▓█▓▒░ ░▒▓█▓▒░ H Y P E R H I V E · H0M3 ░▒▓█▓▒░ ░▒▓█▓▒░</pre>
</header>
<main class="home-main">
<nav class="home-menu" aria-label="hyperhive surfaces">
<a class="home-tile" href="/">
<span class="home-tile-label">◆ D4SHB04RD ◆</span>
<span class="home-tile-desc">containers · approvals · system · stats</span>
</a>
<a class="home-tile" href="/flow.html">
<span class="home-tile-label">◆ FL0W ◆</span>
<span class="home-tile-desc">all-agents chat + operator inbox</span>
</a>
<a class="home-tile" href="/logs.html">
<span class="home-tile-label">◆ L0GS ◆</span>
<span class="home-tile-desc">build · agent · system logs</span>
</a>
<!-- Matrix tile: hidden until home.js confirms the matrix GUI is
enabled (state.matrix_gui_enabled), mirroring the dashboard
tab gating so operators without it don't see a dead link. -->
<a class="home-tile" id="home-tile-matrix" href="/matrix/" hidden>
<span class="home-tile-label">◆ M4TR1X ◆</span>
<span class="home-tile-desc">matrix chat client</span>
</a>
</nav>
</main>
<script type="module" src="/static/home.js" defer></script>
</body>
</html>

View file

@ -0,0 +1,33 @@
// H0M3 page script (#1464). The page is static markup; this only does
// two small things off a single `/api/state` read:
// 1. reveal the Matrix tile when the matrix GUI is enabled (same
// gating as the dashboard's M4TR1X tab — no dead link otherwise);
// 2. fill the swarm/hive identity line when configured.
// No SSE, no shared deps — a portal doesn't need live updates.
const $ = (id) => document.getElementById(id);
async function init() {
let state;
try {
const resp = await fetch('/api/state');
if (!resp.ok) return;
state = await resp.json();
} catch {
return; // best-effort: the tiles still work without it
}
if (state.matrix_gui_enabled) {
const tile = $('home-tile-matrix');
if (tile) tile.hidden = false;
}
const ident = $('hive-identity');
if (ident && (state.swarm_name || state.hive_name)) {
const parts = [state.swarm_name, state.hive_name].filter(Boolean);
ident.textContent = parts.join(' / ');
ident.hidden = false;
}
}
init();