diff --git a/docs/web-ui/README.md b/docs/web-ui/README.md
index d3b95f21..265171b6 100644
--- a/docs/web-ui/README.md
+++ b/docs/web-ui/README.md
@@ -26,11 +26,12 @@ of your time. It's a single page with exactly four tabs:
Everything else lives on its own page instead, all reachable from the
H0M3 hub: **Flow** (`/flow.html`, the raw live message stream across
the whole swarm), **Logs** (`/logs.html`, per-agent and host
-journals), **Stats** (`/stats.html`, swarm-wide usage stats), **Settings**
-(`/settings.html`, your local browser preferences), **Builds** (`/builds.html`, the rebuild
-queue and build history), **Core** (`/core.html`, tombstones and
-container resource use), and **Credentials** (`/credentials.html`,
-provisioning matrix/GitHub/forge accounts per agent).
+journals), **Stats** (`/stats.html`, swarm-wide usage stats),
+**Builds** (`/builds.html`, the rebuild queue and build history),
+**Core** (`/core.html`, tombstones and container resource use), and
+**Credentials** (`/credentials.html`, provisioning matrix/GitHub/forge
+accounts per agent). Your local browser preferences (notifications)
+live in the dashboard's Y3R C4LL tab now, not a separate page.
Each agent also has its own page — a full terminal view of that
agent's session, reachable by clicking its name anywhere in the
diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md
index b0f84fb8..bae45de0 100644
--- a/docs/web-ui/dashboard.md
+++ b/docs/web-ui/dashboard.md
@@ -7,7 +7,7 @@
The dashboard is served at `/dashboard.html` (with the home page at `/`).
It has a fixed chrome header at the top and a `` that shows exactly
one tab pane at a time. The URL hash (`#swarm`, `#call`, `#system`,
-`#permissions`, `#schedules`, `#settings`) drives which pane is
+`#permissions`, `#schedules`) drives which pane is
active; hash changes don't reload the page. FL0W, L0GS, and the optional
M4TR1X client are separate pages reachable from the H0M3 hub at `/`, not
from the dashboard tab strip.
@@ -18,16 +18,15 @@ from the dashboard tab strip.
- **Tab strip**: `◆ SW4RM ◆`, `◆ Y3R C4LL ◆`, `◆ P3RM1SS10NS ◆`,
`◆ SCH3DUL3S ◆`. In-page tabs only — the SYST3M panels moved to the
standalone **C0R3** page (`/core.html`), and FL0W / L0GS / ST4TS /
- S3TT1NGS / M4TR1X live on their own pages too, all reachable from the
+ M4TR1X live on their own pages too, all reachable from the
H0M3 hub (not the tab strip). Count pills on SW4RM
(container count), Y3R C4LL (pending approvals + unread
- operator messages), and SCH3DUL3S (active schedules); S3TT1NGS has no
- count.
+ operator messages), and SCH3DUL3S (active schedules).
- **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`)
— sits below the tab strip.
- **Server-warnings banner** — a generic, sticky top-of-page strip shown
on **every** page (dashboard + every stand-alone page — FL0W, L0GS,
- H0M3, C0R3, BU1LDS, CR3D3NTIALS, ST4TS, S3TT1NGS), injected at the top
+ H0M3, C0R3, BU1LDS, CR3D3NTIALS, ST4TS), injected at the top
of `` by `renderServerWarnings` in `common.js`. Driven by
`state.server_warnings` — a list of `{ kind, level, message }` — and
coloured by `level` (`warn` amber / `crit` red). The backend owns the
@@ -100,6 +99,27 @@ messages are visible from any tab even while inactive. Backed by
`GET /api/operator-inbox` → `{ messages: [...] }` (id, from, body, at,
in_reply_to, file_refs).
+**◆ PR3F3R3NC3S ◆** — operator-local preferences. State lives in the
+browser's `localStorage` — preferences do NOT sync between devices and
+do NOT survive a profile wipe. One section today (browser
+notifications); future preferences land here as sibling `
` blocks
+in `dashboard.html`. Was its own S3TT1NGS page/tile (`/settings.html`)
+until mara moved it in here — a standalone page for one toggle didn't
+earn its own tile.
+
+**◇ browser notifications** — `🔔 enable notifications` button when
+permission ungranted; `🔕 mute / 🔔 unmute` toggle once granted
+(mute silences the dispatch without revoking the OS-level
+permission). On unsupported origins (non-secure context, or
+browsers without the `Notification` API) the controls hide and a
+single status line explains why. See `### Browser notifications`
+below for the dispatch model + the three signals the dashboard
+emits OS notifications on.
+
+The "expand tool output panels" toggle lives on the per-agent page's
+own settings popover (`SettingsMenu`, `@hive/shared/settings-menu.js`)
+instead — it's agent-terminal-only, unrelated to this section.
+
## C0R3 page (`/core.html`)
Passive / rare-interaction state. No longer a dashboard tab — it's a
@@ -595,32 +615,10 @@ the model id, longest match wins) mapping to
`{ input, output, cache_read, cache_write }` USD-per-million-token
prices. Models not covered fall back to hive-c0re's built-in estimate.
-## S3TT1NGS tab
-
-Operator-local preferences. State lives in the browser's
-`localStorage` — preferences do NOT sync between devices and
-do NOT survive a profile wipe. One section today (browser
-notifications); future preferences land here as sibling `
`
-blocks in `settings.html`.
-
-**◇ browser notifications** — `🔔 enable notifications` button when
-permission ungranted; `🔕 mute / 🔔 unmute` toggle once granted
-(mute silences the dispatch without revoking the OS-level
-permission). On unsupported origins (non-secure context, or
-browsers without the `Notification` API) the controls hide and a
-single status line explains why. See `### Browser notifications`
-below for the dispatch model + the three signals the dashboard
-emits OS notifications on.
-
-The "expand tool output panels" toggle used to live here too; it
-moved into the per-agent page's own settings popover (`SettingsMenu`,
-`@hive/shared/settings-menu.js`) since it's agent-terminal-only and
-this tab is shared with swarm-ui — see that component's file comment.
-
-The FL0W page does NOT host this pane — settings live only on the
-dashboard's S3TT1NGS tab (reach it via the FL0W page's `← home`
+The FL0W page does NOT host the PR3F3R3NC3S pane — it lives only on the
+dashboard's Y3R C4LL tab (reach it via the FL0W page's `← home`
back-link → Dashboard). Notifications still fire on the FL0W page when
-they're enabled here, because `NOTIF.show()` in
+they're enabled there, because `NOTIF.show()` in
`common.js` depends on `Notification.permission` + the
`hyperhive.notify.muted` localStorage key, not on the buttons
existing in the page DOM.
@@ -1091,9 +1089,9 @@ Pure frontend (`Notification` API). Two signals trigger them:
- new pending approval (per id, delta on `/api/state`)
- new broker message sent `to: "operator"` (live via SSE)
-The toggle controls live in the `S3TT1NGS` tab (`#settings`); see
-that section above for the user-facing shape. Dispatch logic lives
-in `common.js::NOTIF`.
+The toggle controls live in the Y3R C4LL tab's ◆ PR3F3R3NC3S ◆
+section; see that section above for the user-facing shape. Dispatch
+logic lives in `common.js::NOTIF`.
First `/api/state` after page load seeds "seen" sets without
firing — only items that arrive while the page is open count.
diff --git a/docs/web-ui/shape.md b/docs/web-ui/shape.md
index 3a12b496..c1f3d409 100644
--- a/docs/web-ui/shape.md
+++ b/docs/web-ui/shape.md
@@ -20,7 +20,7 @@
CSS bundle per page (`colors.css` + `theme.css` + `common.css`,
loaded by every page, plus a page-specific bundle —
`dashboard.css` / `flow.css` / `logs.css` / `home.css` /
- `settings.css` / `stats.css` / `core.css` / `builds.css` /
+ `stats.css` / `core.css` / `builds.css` /
`credentials.css`); `common.css` inlines `@hive/shared`'s
`base.css` + `terminal.css` + `tabs.css` + `chrome.css` +
`pill.css` via esbuild's `@import` resolution.
diff --git a/frontend/packages/dashboard/build.mjs b/frontend/packages/dashboard/build.mjs
index f4e16c3b..d2844aa5 100644
--- a/frontend/packages/dashboard/build.mjs
+++ b/frontend/packages/dashboard/build.mjs
@@ -66,7 +66,7 @@ mkdirSync(staticDir(''), { recursive: true });
// follow-up once asset sizes warrant it). esbuild writes each entry
// to `static/.js` based on the entryPoint basename.
await build({
- entryPoints: [src('tabs.js'), src('flow.js'), src('logs.js'), src('home.js'), src('settings.js'), src('stats.js'), src('core.js'), src('builds.js'), src('credentials.js')],
+ entryPoints: [src('tabs.js'), src('flow.js'), src('logs.js'), src('home.js'), src('stats.js'), src('core.js'), src('builds.js'), src('credentials.js')],
outdir: staticDir(''),
bundle: true,
format: 'esm',
@@ -122,7 +122,7 @@ await build({
// so a swap replaces only it) + theme.css (the semantic derivation
// layer) + common.css (shared typography, badges, buttons, inbox, side
// panel) plus its own page-specific bundle.
-for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', 'flow.css', 'logs.css', 'home.css', 'settings.css', 'stats.css', 'core.css', 'builds.css', 'credentials.css']) {
+for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', 'flow.css', 'logs.css', 'home.css', 'stats.css', 'core.css', 'builds.css', 'credentials.css']) {
await build({
entryPoints: [src(entry)],
outfile: staticDir(entry),
@@ -132,7 +132,7 @@ for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', '
});
}
-for (const html of ['index.html', 'dashboard.html', 'flow.html', 'logs.html', 'settings.html', 'stats.html', 'core.html', 'builds.html', 'credentials.html']) {
+for (const html of ['index.html', 'dashboard.html', 'flow.html', 'logs.html', 'stats.html', 'core.html', 'builds.html', 'credentials.html']) {
copyFileSync(src(html), dist(html));
}
diff --git a/frontend/packages/dashboard/src/common.css b/frontend/packages/dashboard/src/common.css
index f3c5df99..a5db50a0 100644
--- a/frontend/packages/dashboard/src/common.css
+++ b/frontend/packages/dashboard/src/common.css
@@ -434,7 +434,7 @@ hive-side-panel .md td {
server-warnings banner, the footer) then spans edge-to-edge for free,
with no negative-margin breakout hacks. Only matches on pages that opt
in by adding the wrapper (dashboard.html, index.html); FL0W / L0GS /
- ST4TS / S3TT1NGS are full-bleed with their own `.-main` padding. */
+ ST4TS are full-bleed with their own `.-main` padding. */
.page-content { padding: 0 1.5em; }
/* ─── sticky top region ───────────────────────────────────────────────
diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css
index 476663dd..49c880f3 100644
--- a/frontend/packages/dashboard/src/dashboard.css
+++ b/frontend/packages/dashboard/src/dashboard.css
@@ -461,8 +461,32 @@ hive-agent-menu {
`JobqRollup` Preact component (`.jqr-*` classes, imported above) —
mounted by swarm.js::initJobqRollup into #jobq-rollup-section. */
-/* .notif-row / .btn-notif moved to settings.css with the S3TT1NGS
- page. */
+/* Browser-notification toggle row, now living in the Y3R C4LL tab's
+ ◆ PR3F3R3NC3S ◆ section (was its own S3TT1NGS page/tile; mara: a
+ standalone page for one toggle didn't earn its own tile). Buttons
+ wired by NOTIF.bind() in common.js. */
+.notif-row {
+ display: flex;
+ gap: 0.5em;
+ align-items: center;
+ margin: 0.5em 0;
+ font-size: 0.85em;
+}
+.btn-notif {
+ font-family: inherit;
+ font-size: 0.85em;
+ background: transparent;
+ color: var(--cyan);
+ border: 1px solid var(--cyan);
+ padding: 0.2em 0.7em;
+ border-radius: 999px;
+ cursor: pointer;
+ text-shadow: 0 0 4px currentColor;
+}
+.btn-notif:hover {
+ background: color-mix(in srgb, var(--cyan) 10%, transparent);
+ box-shadow: 0 0 10px -2px currentColor;
+}
.pending-state {
color: var(--amber);
diff --git a/frontend/packages/dashboard/src/dashboard.html b/frontend/packages/dashboard/src/dashboard.html
index d8a6112f..990dc9c6 100644
--- a/frontend/packages/dashboard/src/dashboard.html
+++ b/frontend/packages/dashboard/src/dashboard.html
@@ -72,9 +72,6 @@
into the log viewer (an agent's logs, a build entry's log)
still live inside the relevant dashboard content. -->
-
@@ -129,6 +126,24 @@
operator-local preferences. these live in the browser's localStorage — they do not sync between devices and do not survive a profile wipe.
+
◇ browser notifications
+
desktop notifications for new approvals and broker messages addressed to you. requires a secure context (https or localhost). mute silences the notifications without revoking the OS-level permission.
operator-local preferences. these live in the browser's localStorage — they do not sync between devices and do not survive a profile wipe.
-
-
◇ browser notifications
-
desktop notifications for new approvals and broker messages addressed to you. requires a secure context (https or localhost). mute silences the notifications without revoking the OS-level permission.
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/packages/dashboard/src/settings.js b/frontend/packages/dashboard/src/settings.js
deleted file mode 100644
index 298378d1..00000000
--- a/frontend/packages/dashboard/src/settings.js
+++ /dev/null
@@ -1,15 +0,0 @@
-// /settings.html — operator-local preferences page.
-//
-// Extracted from the dashboard S3TT1NGS tab. The browser-notification
-// toggle: NOTIF.bind() wires the enable / mute / unmute buttons and
-// persists state to localStorage. The dashboard's NOTIF.show() calls
-// (approvals) read that same localStorage state + the
-// browser-level permission, so firing still works from the dashboard
-// even though the toggle UI now lives on its own page.
-//
-// initServerWarnings() renders the shared top-of-page warnings banner,
-// matching the other stand-alone pages (FL0W / L0GS / H0M3).
-import { NOTIF, initServerWarnings } from './common.js';
-
-initServerWarnings();
-NOTIF.bind();
diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js
index 4be7010d..3d6cd2a9 100644
--- a/frontend/packages/dashboard/src/tabs.js
+++ b/frontend/packages/dashboard/src/tabs.js
@@ -257,12 +257,14 @@ window.marked = marked;
// title reflect unread agent->operator messages immediately, before the
// operator opens the tab. Live updates arrive via the broker stream.
refreshOperatorInbox();
- // NOTIF.bind() (the enable/mute/unmute toggle UI) moved to /settings.html
- // with the S3TT1NGS page. The dashboard keeps NOTIF.show() for approval
- // notifications — it reads the browser permission + localStorage
- // mute flag the settings page sets, so no bind() is needed here. The
- // side panel () wires its own internals in
- // connectedCallback — no bind() step needed either.
+ // NOTIF.bind() wires the enable/mute/unmute toggle buttons that now
+ // live in the Y3R C4LL tab's ◆ PR3F3R3NC3S ◆ section. NOTIF.show()
+ // (approval/inbox notifications) reads the same browser permission +
+ // localStorage mute flag regardless of which page fires it — bind()
+ // only needs to run once, here, where the buttons live. The side
+ // panel () wires its own internals in
+ // connectedCallback — no bind() step needed for it.
+ NOTIF.bind();
// ─── live updates: dashboard event stream ──────────────────────────────
// The dashboard subscribes to /dashboard/stream for live mutation
@@ -349,9 +351,9 @@ window.marked = marked;
// ─── tab routing ───────────────────────────────────────────────────────
// Hash-based: `#swarm` / `#call` / `#system` / `#permissions` /
// `#schedules` activate the matching pane on the dashboard. Empty
- // hash defaults to SW4RM. FL0W, S3TT1NGS, and ST4TS are NOT tabs —
- // they're separate pages (`/flow.html`, `/settings.html`,
- // `/stats.html`) reached from the H0M3 hub. Tab routing only
+ // hash defaults to SW4RM. FL0W and ST4TS are NOT tabs —
+ // they're separate pages (`/flow.html`, `/stats.html`) reached from
+ // the H0M3 hub. Tab routing only
// applies when the tab DOM is present (e.g. not on the flow page
// itself, where these elements don't exist and the loop no-ops).
// The shared hash-routed tab strip (@hive/shared/tabs.js) owns the
diff --git a/frontend/packages/shared/src/prefs.ts b/frontend/packages/shared/src/prefs.ts
index de4fa5bf..ebfb5369 100644
--- a/frontend/packages/shared/src/prefs.ts
+++ b/frontend/packages/shared/src/prefs.ts
@@ -1,10 +1,9 @@
-// Tiny browser-local (localStorage) preference helpers shared by the
-// dashboard's /settings.html and the per-agent page — both need the
-// exact same key name to actually talk to each other via the browser's
-// shared per-origin storage (settings.html is where the operator sets
-// the preference; every per-agent page's terminal reads it), so the
-// get/set pair — and the key itself — live here once rather than as
-// independent copies in each package that could drift out of sync.
+// Tiny browser-local (localStorage) preference helper, in `@hive/shared`
+// so a future second consumer (e.g. swarm-ui's own terminal) gets the
+// exact same key name for free via the browser's shared per-origin
+// storage, rather than an independent copy that could drift.
+// `ExpandDetailsSetting` (the per-agent page's own settings popover) is
+// the only writer today; `Row.tsx` is the only reader.
const EXPAND_DETAILS_KEY = 'hive-agent-expand-details';
diff --git a/nix/packages/frontend.nix b/nix/packages/frontend.nix
index 406065bd..a8e9b41d 100644
--- a/nix/packages/frontend.nix
+++ b/nix/packages/frontend.nix
@@ -16,10 +16,10 @@
# $out/dashboard/ the hive-c0re dashboard assets (full layout in
# frontend/packages/dashboard/build.mjs):
# index.html (H0M3 hub, served at /) dashboard.html (operator SPA,
-# served at /dashboard.html) flow.html logs.html settings.html
+# served at /dashboard.html) flow.html logs.html
# stats.html favicon.svg
-# static/{home,tabs,flow,logs,settings,stats,stream-worker}.js{,.map}
-# static/{colors,theme,common,home,dashboard,flow,logs,settings,stats}.css
+# static/{home,tabs,flow,logs,stats,stream-worker}.js{,.map}
+# static/{colors,theme,common,home,dashboard,flow,logs,stats}.css
# $out/agent/ the per-agent default UI (layered with
# hyperhive.frontend.extraFiles at activation time)
# index.html stats.html screen.html