diff --git a/docs/web-ui/README.md b/docs/web-ui/README.md
index d3b95f21..12201218 100644
--- a/docs/web-ui/README.md
+++ b/docs/web-ui/README.md
@@ -10,8 +10,9 @@ below; this page only covers what you actually do here.
Everything starts at the **H0M3 hub**, served at `/` — a grid of tiles
linking to every surface (Dashboard, Flow, Logs, Builds, Stats,
-Settings, Core, Credentials). Every page links back to H0M3, so you're
-never more than one click from the hub.
+Settings, Core, Credentials, plus Matrix/Forge when enabled). Every
+page links back to H0M3, so you're never more than one click from the
+hub.
The **dashboard** itself (`/dashboard.html`) is where you'll spend most
of your time. It's a single page with exactly four tabs:
diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md
index b0f84fb8..6cad964d 100644
--- a/docs/web-ui/dashboard.md
+++ b/docs/web-ui/dashboard.md
@@ -636,11 +636,9 @@ nginx container at `/matrix/` when
`HIVE_MATRIX_GUI_ENABLED` env var → `state.matrix_gui_enabled` in
`/api/state`; the gateway does the actual static serving.
-There's no tile for it on the H0M3 hub (removed along with Forge's —
-see the H0M3 section above). The operator reaches it from the swarm-ui
-LinksMenu instead (see `docs/swarm/ui.md::Quick links`), and logs in
-once with the in-host tuwunel homeserver URL (`http://localhost:8008` or whatever
-the matrix module exposes).
+The operator opens `/matrix/` from the Matrix tile on the H0M3 hub, logs
+in once with the in-host tuwunel homeserver URL
+(`http://localhost:8008` or whatever the matrix module exposes).
The unified nginx-front re-root to
`https://chat.${hyperhive.swarm.domain}` + `.well-known/matrix/client`
@@ -689,16 +687,25 @@ re-renders the terminal row. The root agent is addressed as `@root`.
## H0M3 page (`/`)
The H0M3 hub is the primary landing page (served at `/` by default). A
-responsive grid of link tiles — Dashboard, Flow, Logs, Builds, Stats,
-Settings, Core, Credentials, API — each pointing to their respective
-surfaces, all unconditionally shown (no gating). The page is a pure
-portal with no tab-bar or SSE subscriptions. Typography + colours inherit
-from the shared theme (Catppuccin Mocha via `common.css` + `theme.css`).
-`home.js` fills the swarm/hive identity line at the top. All dashboard
-sub-pages include a `← Home` back-link for navigation.
-
-No Matrix or Forge tile here — those surfaces (`/matrix/`, the forge's own
-public URL) are reached directly, not linked from the dashboard UI.
+responsive grid of link tiles — Dashboard, Flow, Logs, Matrix (when enabled),
+Forge (when enabled), among others (Builds, Stats, Settings, Core,
+Credentials, API) — each pointing to their respective surfaces. The **API**
+tile always shows (unlike Matrix/Forge, see below) and links to the
+Swagger UI at `/api/docs`. The page
+is a pure portal with no tab-bar or SSE subscriptions. Typography + colours
+inherit from the shared theme (Catppuccin Mocha via `common.css` + `theme.css`).
+Optional tiles are hidden until `home.js` confirms their availability:
+Matrix is hidden until `home.js` confirms `matrix_gui_enabled` (same gating as
+the dashboard's M4TR1X tab); Forge is hidden until `home.js` confirms both
+`state.forge_present` **and** `state.forge_public_url` (sourced from
+`services.hyperhive.forge.publicUrl`, which defaults to the gateway vhost URL
+when `behindGateway=true` and is `null` otherwise) and fills the href from the
+latter — never guessed from the operator's browser hostname + a container
+port, which is only right by accident off plain localhost. Operators without
+matrix or forge enabled — or with forge on but no public URL configured —
+never see a dead or wrong link. `home.js` also fills the swarm/hive identity
+line at the top. All dashboard sub-pages include a `← Home` back-link for
+navigation.
## L0GS page (`/logs.html`)
diff --git a/frontend/packages/dashboard/src/home.js b/frontend/packages/dashboard/src/home.js
index 0225f556..ce020a77 100644
--- a/frontend/packages/dashboard/src/home.js
+++ b/frontend/packages/dashboard/src/home.js
@@ -1,7 +1,9 @@
// H0M3 page script. The page is static markup; this only does
// a few small things off a single `/api/state` read:
-// 1. fill the swarm/hive identity line when configured;
-// 2. render the shared server-warnings banner (top of every page).
+// 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;
+// 3. render the shared server-warnings banner (top of every page).
// No SSE — a portal doesn't need live updates.
//
// It also drives the decorative matrix-rain backdrop (#matrix-rain) —
@@ -23,6 +25,25 @@ async function init() {
renderServerWarnings(state.server_warnings);
+ if (state.matrix_gui_enabled) {
+ const tile = $('home-tile-matrix');
+ if (tile) tile.hidden = false;
+ }
+
+ // Forge tile: reveal only when the hive-forge container is up AND
+ // the operator has stated a public URL for it
+ // (services.hyperhive.forge.publicUrl, surfaced as
+ // state.forge_public_url) — hidden rather than guessed from
+ // `location.hostname` + the direct :3000 port, which is only
+ // correct by accident on deployments that aren't plain localhost.
+ if (state.forge_present && state.forge_public_url) {
+ const tile = $('home-tile-forge');
+ if (tile) {
+ tile.href = state.forge_public_url;
+ 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);
diff --git a/frontend/packages/dashboard/src/index.html b/frontend/packages/dashboard/src/index.html
index 3aa97541..ad4d0428 100644
--- a/frontend/packages/dashboard/src/index.html
+++ b/frontend/packages/dashboard/src/index.html
@@ -100,9 +100,9 @@
provision per-agent matrix + github accounts
-
+
🧬
@@ -111,6 +111,29 @@
interactive OpenAPI spec (Swagger UI)
+
+
+
+ ⚒
+ Forge
+
+ issues · pull requests · agent-config repos
+
+
+
+
+
+ 💬
+ Matrix
+
+ matrix chat client
+
+
diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js
index 4be7010d..08fa8b0a 100644
--- a/frontend/packages/dashboard/src/tabs.js
+++ b/frontend/packages/dashboard/src/tabs.js
@@ -188,8 +188,8 @@ window.marked = marked;
// names from here instead of refetching on every keystroke).
window.__hyperhive_state = s;
renderServerWarnings(s.server_warnings);
- // (The M4TR1X surface is reachable directly at /matrix/ when enabled
- // — no link in the dashboard tab strip or the H0M3 hub.)
+ // (The M4TR1X surface is reachable from the H0M3 hub now, not the
+ // dashboard tab strip — home.js gates its tile on matrix_gui_enabled.)
// Hive identity: render the swarm/hive name as a headline at the
// top of the SW4RM pane + update the page title once the
// server-side display names are known. `hive_name` / `swarm_name`
diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix
index bc836acc..632643a7 100644
--- a/nix/host-modules/hive-matrix.nix
+++ b/nix/host-modules/hive-matrix.nix
@@ -513,12 +513,10 @@ in
# This swarm-ui quick-links entry. Gated on `gui.enable` too, not just
# `gatewayHost != null`: `/` on that vhost only serves fluffychat
- # (below) when the GUI is on — otherwise the link would 404. Same
- # `gatewayHost && gui.enable` condition the removed hive-dashboard
- # H0M3 tile used to check via `state.matrix_gui_enabled` before this
- # link became the only way in (docs/web-ui/dashboard.md::M4TR1X
- # page). See `services.hyperhive.swarm.controller.links`'s
- # description.
+ # (below) when the GUI is on — otherwise the link would 404, the same
+ # reason the old dashboard's H0M3 page hides its Matrix tile on
+ # `state.matrix_gui_enabled` rather than `gatewayHost` alone. See
+ # `services.hyperhive.swarm.controller.links`'s description.
services.hyperhive.swarm.controller.links =
lib.optional (cfg.gatewayHost != null && cfg.gui.enable)
{