common.js + flow.js + index.html + flow.html: cookie scrub + doc pointers (#712 batch 13 cont.)
Final dashboard frontend cleanup. Four files at 0 cookies each. common.js (11 → 0): - #406 module-split history → drop, present-state intro - #448 SharedWorker SSE pipe (×3) → docs/web-ui.md SSE multiplexing paragraph pointer - #515 worker-death self-heal (×3) → docs pointer + brief inline - #453 bfcache argus-nit (×2) → drop framing, keep inline why - #451 drag-to-resize → docs/web-ui.md::Side panel pointer - #233 marked autolinks new-tab → drop cookie flow.js (8 → 0): - #406 module-split history → drop - #389 banner-in-footer → drop framing - #375 stacking context → docs/web-ui.md::Per-agent page Terminal-wrap - #408 server-side filter, #499 backend allow-list → drop cookies, keep inline why - #448 SharedWorker per-URL keying → docs pointer - #163 SSE catchup drift → drop cookie, keep inline why index.html (13 → 0): - #389 chrome history → drop - #459 schedules, #607 matrix, #609 nginx-front, #15 → drop cookies, keep functional comments + docs/web-ui.md::Tab strip + gateway.md pointers for matrix - #385 dropped C0NTAINERS heading → drop framing - #460 reminders-moved-here → drop framing - #444 SchedulesChanged SSE absence → drop cookie, keep inline why - #443 selection bar → docs/web-ui.md::Selection bar pointer - #564 fold-create-into-table → drop framing - #369#issuecomment-3437 → drop attribution - #406 step 3 bundle rename → drop history flow.html (3 → 0): - #389 slug + #362 pill pattern + #406 step 2 bundle → drop cookies, preserve functional descriptions Total this batch (across all 4 files): **35 cookies scrubbed**. Net effect: the dashboard SPA's HTML + JS comments all point at docs/web-ui.md for substantive design context now, with inline comments only retaining present-state operational descriptions.
This commit is contained in:
parent
6c9b28903f
commit
f7e38c0b42
4 changed files with 123 additions and 182 deletions
|
|
@ -1,13 +1,7 @@
|
||||||
// Shared dashboard helpers — extracted from the original monolithic
|
// Shared dashboard helpers used by both index.html (./tabs.js) and
|
||||||
// dashboard JS as step 1 of the #406 split. These bits are used by
|
// flow.html (./flow.js): pure DOM helpers, the side-panel singleton,
|
||||||
// both the tab dashboard (index.html) and the flow page (flow.html):
|
// the OS-notification module, and the path-link / file-preview
|
||||||
// pure DOM helpers, the side-panel singleton, the OS-notification
|
// infrastructure for the side panel.
|
||||||
// module, and the path-link / file-preview infrastructure for the
|
|
||||||
// side panel.
|
|
||||||
//
|
|
||||||
// Each page now has its own entry point — `./tabs.js` for index.html,
|
|
||||||
// `./flow.js` for flow.html — and both import from here directly
|
|
||||||
// (#406 steps 2 + 3 complete; #406 closed).
|
|
||||||
|
|
||||||
import { linkify as termLinkify } from '@hive/shared/terminal.js';
|
import { linkify as termLinkify } from '@hive/shared/terminal.js';
|
||||||
|
|
||||||
|
|
@ -57,31 +51,23 @@ export const form = (action, btnClass, btnLabel, confirmMsg, extra = {}, opts =
|
||||||
// tied to its caller, so they don't generalise cleanly. We can lift
|
// tied to its caller, so they don't generalise cleanly. We can lift
|
||||||
// them when a second consumer needs the same shape.
|
// them when a second consumer needs the same shape.
|
||||||
|
|
||||||
// ─── shared-worker SSE pipe (#448) ──────────────────────────────────────
|
// ─── shared-worker SSE pipe ─────────────────────────────────────────────
|
||||||
// Returns an EventSource-shaped object backed by a SharedWorker that
|
// Returns an EventSource-shaped facade backed by a SharedWorker that
|
||||||
// holds ONE upstream `new EventSource(url)` and fans events out to
|
// holds one upstream `new EventSource(url)` and fans events out to
|
||||||
// every connected tab. Replaces direct `new EventSource(url)` at the
|
// every connected tab. See docs/web-ui.md (SSE multiplexing paragraph)
|
||||||
// dashboard's two consumer sites (tabs.js inline + flow.js via
|
// for the design + Firefox throttling motivation; graceful fallback to
|
||||||
// terminal.js's `streamFactory` option) so N hyperhive tabs share
|
// direct EventSource on environments without SharedWorker.
|
||||||
// ONE backend connection — way under the browser's per-host
|
|
||||||
// connection cap, immune to per-tab throttling that drops the SSE
|
|
||||||
// when Firefox suspends background tabs.
|
|
||||||
//
|
//
|
||||||
// Graceful fallback to direct EventSource on environments without
|
// Consumer API: assign `onmessage` / `onopen` / `onerror`; `.close()`
|
||||||
// SharedWorker (some embedded browsers, some Safari versions). The
|
// drops the subscription (the worker closes the upstream when the last
|
||||||
// per-tab connection cost is the same as today — no regression.
|
// subscriber leaves).
|
||||||
//
|
|
||||||
// The page consumer uses the returned object like a regular
|
|
||||||
// EventSource: assign `onmessage` / `onopen` / `onerror`. `.close()`
|
|
||||||
// tells the worker to drop the subscription; the worker closes the
|
|
||||||
// upstream EventSource when the last subscriber leaves.
|
|
||||||
const SHARED_WORKER_PATH = '/static/stream-worker.js';
|
const SHARED_WORKER_PATH = '/static/stream-worker.js';
|
||||||
const SHARED_WORKER_NAME = 'hyperhive-stream';
|
const SHARED_WORKER_NAME = 'hyperhive-stream';
|
||||||
|
|
||||||
// One SharedWorker port per page, reused by all openStream calls on
|
// One SharedWorker port per page, reused by all openStream calls on
|
||||||
// that page. Invalidated on `pagehide` so a bfcache restore picks up
|
// that page. Invalidated on `pagehide` so a bfcache restore picks up
|
||||||
// a fresh port (the cached one may have been collected if all other
|
// a fresh port — the cached port may be dead if all other tabs
|
||||||
// tabs closed while this page was frozen — argus nit on #453).
|
// closed while this page was frozen.
|
||||||
let _sharedPort = null;
|
let _sharedPort = null;
|
||||||
function makeSharedPort() {
|
function makeSharedPort() {
|
||||||
if (typeof SharedWorker === 'undefined') return null;
|
if (typeof SharedWorker === 'undefined') return null;
|
||||||
|
|
@ -99,30 +85,11 @@ function getSharedPort() {
|
||||||
return _sharedPort;
|
return _sharedPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #515: detect when the SharedWorker has been killed. Firefox aggressively
|
// SharedWorker death detection: pings from the worker bump the
|
||||||
// reclaims "idle" SharedWorkers under memory pressure (or just on tab
|
// activity clock; a visibility-gated watchdog polls and re-subscribes
|
||||||
// lifecycle quirks we don't fully understand), and there's no native
|
// on a fresh port if the page has been silent past the threshold.
|
||||||
// signal to the client when that happens — `postMessage` on a dead
|
// See docs/web-ui.md (Worker-death self-heal paragraph) for the
|
||||||
// port silently no-ops, and the page just stops receiving events. The
|
// timing rationale + Firefox reclaim symptom.
|
||||||
// observable symptom is "dashboard never refreshes; F5 fixes it" (a
|
|
||||||
// fresh page creates a fresh worker), which is mara's report on #515.
|
|
||||||
//
|
|
||||||
// Pipeline:
|
|
||||||
// - The worker pings every connected port every WORKER_PING_INTERVAL_MS.
|
|
||||||
// - `route` (every message arrival, including pings) bumps
|
|
||||||
// `_lastWorkerActivityAt`.
|
|
||||||
// - `startWorkerWatchdog` polls every WORKER_WATCHDOG_INTERVAL_MS.
|
|
||||||
// If the page is visible AND we have active subs AND we haven't
|
|
||||||
// heard from the worker in > WORKER_DEAD_THRESHOLD_MS, we presume
|
|
||||||
// the worker is dead, log a warning, and re-subscribe on a fresh
|
|
||||||
// port.
|
|
||||||
//
|
|
||||||
// Numbers picked so a real Firefox tab-suspend / unsuspend cycle (which
|
|
||||||
// can pause the watchdog itself) doesn't false-positive: 90s without a
|
|
||||||
// 30s ping means at least three pings missed. Visibility-gated so a
|
|
||||||
// backgrounded tab — where Firefox throttles setInterval to 1Hz min and
|
|
||||||
// our healthcheck wouldn't trigger reliably anyway — doesn't try to
|
|
||||||
// reconnect uselessly.
|
|
||||||
const WORKER_DEAD_THRESHOLD_MS = 90_000;
|
const WORKER_DEAD_THRESHOLD_MS = 90_000;
|
||||||
const WORKER_WATCHDOG_INTERVAL_MS = 15_000;
|
const WORKER_WATCHDOG_INTERVAL_MS = 15_000;
|
||||||
let _lastWorkerActivityAt = 0;
|
let _lastWorkerActivityAt = 0;
|
||||||
|
|
@ -164,25 +131,19 @@ function rebindOnFreshPort() {
|
||||||
noteWorkerActivity();
|
noteWorkerActivity();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registry of live subscriptions on this page. Keyed by url so a
|
// Registry of live subscriptions on this page. Keyed by url; entries
|
||||||
// second openStream call for the same URL (would only happen on a
|
// cache the route function so bfcache-restore re-bind can re-attach
|
||||||
// hypothetical multi-consumer page) attaches to the existing route
|
// it to the fresh port. Today's pages only call openStream once with
|
||||||
// rather than overlapping. Each entry caches the route function so
|
// one URL; the registry shape just keeps the bfcache path correct
|
||||||
// bfcache-restore re-bind can re-attach it to the fresh port.
|
// if that changes.
|
||||||
//
|
|
||||||
// Today's pages only call openStream once with one URL; the registry
|
|
||||||
// shape just keeps the bfcache-restore path correct if that changes
|
|
||||||
// (e.g. /index.html later subscribing to two streams).
|
|
||||||
const _activeSubs = new Map();
|
const _activeSubs = new Map();
|
||||||
|
|
||||||
// One-shot wiring of the page-wide lifecycle hooks: on bfcache
|
// One-shot wiring of page-wide lifecycle hooks. On bfcache freeze
|
||||||
// freeze (`pagehide { persisted: true }`) we unsubscribe so the
|
// we unsubscribe so the worker can close the upstream when the last
|
||||||
// worker can close the upstream when the last live subscriber
|
// live subscriber leaves; on bfcache restore we invalidate the cached
|
||||||
// leaves; on bfcache restore (`pageshow { persisted: true }`) we
|
// port (may be dead after the freeze) and re-attach every active
|
||||||
// invalidate the cached port (it may be dead if all other tabs
|
// subscription to a fresh port. Without this, the consumer's
|
||||||
// closed during the freeze) and re-attach every active subscription
|
// onmessage stays bound but no events flow after restore.
|
||||||
// to a fresh port. argus nit on #453: without this, the consumer's
|
|
||||||
// onmessage stays bound but no events flow after a bfcache restore.
|
|
||||||
let _lifecycleBound = false;
|
let _lifecycleBound = false;
|
||||||
function bindLifecycleOnce() {
|
function bindLifecycleOnce() {
|
||||||
if (_lifecycleBound) return;
|
if (_lifecycleBound) return;
|
||||||
|
|
@ -245,7 +206,7 @@ export function openStream(url) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const route = (e) => {
|
const route = (e) => {
|
||||||
// #515: any message from the worker is proof of life — note it
|
// Any message from the worker is proof of life — note it
|
||||||
// before the URL filter, since heartbeat pings carry no URL.
|
// before the URL filter, since heartbeat pings carry no URL.
|
||||||
noteWorkerActivity();
|
noteWorkerActivity();
|
||||||
const m = e.data;
|
const m = e.data;
|
||||||
|
|
@ -272,8 +233,8 @@ export function openStream(url) {
|
||||||
_activeSubs.set(url, { target, route });
|
_activeSubs.set(url, { target, route });
|
||||||
port.addEventListener('message', route);
|
port.addEventListener('message', route);
|
||||||
port.postMessage({ kind: 'subscribe', url });
|
port.postMessage({ kind: 'subscribe', url });
|
||||||
// #515: seed the activity clock so the watchdog has a baseline; it
|
// Seed the activity clock so the watchdog has a baseline (would
|
||||||
// would otherwise compare against 0 (epoch) and trigger immediately.
|
// otherwise compare against 0 and trigger immediately).
|
||||||
noteWorkerActivity();
|
noteWorkerActivity();
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
@ -330,13 +291,10 @@ export const Panel = (() => {
|
||||||
root.classList.remove('open');
|
root.classList.remove('open');
|
||||||
root.setAttribute('aria-hidden', 'true');
|
root.setAttribute('aria-hidden', 'true');
|
||||||
}
|
}
|
||||||
// #451: drag-to-resize the drawer's width. Listens on a thin
|
// Drag-to-resize the drawer's width. See docs/web-ui.md::Side panel
|
||||||
// hit-strip glued to the drawer's left edge; mousedown captures
|
// for the hit-strip + pointer-capture + localStorage persistence
|
||||||
// pointermove + pointerup on the document so the drag continues
|
// model; CSS clamps the stored value to min 320px / max 96vw and
|
||||||
// even if the cursor strays outside the 6px handle band. Width
|
// out-of-range stored values are dropped silently.
|
||||||
// persists to localStorage so it survives page reload. The CSS
|
|
||||||
// clamps the value (min-width: 320px, max-width: 96vw) — drop
|
|
||||||
// unparseable / out-of-range stored values silently.
|
|
||||||
const WIDTH_KEY = 'hyperhive:side-panel-width';
|
const WIDTH_KEY = 'hyperhive:side-panel-width';
|
||||||
const WIDTH_MIN = 320;
|
const WIDTH_MIN = 320;
|
||||||
function clampWidth(w) {
|
function clampWidth(w) {
|
||||||
|
|
@ -476,7 +434,7 @@ function mdNode(text) {
|
||||||
window.marked.setOptions({ breaks: true, gfm: true });
|
window.marked.setOptions({ breaks: true, gfm: true });
|
||||||
div.innerHTML = window.marked.parse(text);
|
div.innerHTML = window.marked.parse(text);
|
||||||
// marked autolinks URLs but leaves them same-tab — open externally
|
// marked autolinks URLs but leaves them same-tab — open externally
|
||||||
// so a click never navigates away from the dashboard. (issue #233)
|
// so a click never navigates away from the dashboard.
|
||||||
div.querySelectorAll('a[href]').forEach((a) => {
|
div.querySelectorAll('a[href]').forEach((a) => {
|
||||||
a.target = '_blank';
|
a.target = '_blank';
|
||||||
a.rel = 'noopener noreferrer';
|
a.rel = 'noopener noreferrer';
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,11 @@
|
||||||
</head>
|
</head>
|
||||||
<body class="flow-shell">
|
<body class="flow-shell">
|
||||||
|
|
||||||
<!-- Fixed-overlay chrome — just the tab strip (#389 follow-up:
|
<!-- Fixed-overlay chrome — just the tab strip. The full-viewport
|
||||||
slug moved to the dashboard's page footer; the flow page is a
|
terminal has no normal-flow footer position so the dashboard's
|
||||||
full-viewport terminal with no normal-flow footer position, so
|
slug doesn't appear here. The operator can still switch tabs
|
||||||
the slug simply doesn't appear here). The operator can still
|
from the flow page without navigating back; FL0W is the current
|
||||||
switch tabs from the flow page without navigating back; FL0W is
|
page, SW4RM / Y3R C4LL / SYST3M / SCH3DUL3S cross-link to the
|
||||||
the current page, SW4RM / Y3R C4LL / SYST3M cross-link to the
|
|
||||||
dashboard with the matching hash. -->
|
dashboard with the matching hash. -->
|
||||||
<header class="dashboard-chrome flow-chrome" id="flow-header">
|
<header class="dashboard-chrome flow-chrome" id="flow-header">
|
||||||
<nav class="tabbar" id="tabbar" role="tablist">
|
<nav class="tabbar" id="tabbar" role="tablist">
|
||||||
|
|
@ -55,7 +54,7 @@
|
||||||
|
|
||||||
<!-- Operator inbox flyout trigger — count + click → side panel
|
<!-- Operator inbox flyout trigger — count + click → side panel
|
||||||
(singleton, declared below). Hidden until the inbox is non-
|
(singleton, declared below). Hidden until the inbox is non-
|
||||||
empty. Mirrors the agent page's pill pattern (#362). -->
|
empty. Mirrors the agent page's header pill pattern. -->
|
||||||
<button type="button" id="inbox-pill" class="flow-pill" hidden
|
<button type="button" id="inbox-pill" class="flow-pill" hidden
|
||||||
title="open operator inbox">
|
title="open operator inbox">
|
||||||
<span class="flow-pill-icon" aria-hidden="true">📬</span>
|
<span class="flow-pill-icon" aria-hidden="true">📬</span>
|
||||||
|
|
@ -111,10 +110,10 @@
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Flow-specific bundle (#406 step 2). Contains the broker
|
<!-- Flow-specific bundle. Contains the broker terminal init, the
|
||||||
terminal init, the operator-inbox derived store, the inbox
|
operator-inbox derived store, the inbox pill flyout, and the
|
||||||
pill flyout, and the @-mention composer. Tab renderers etc.
|
@-mention composer. Tab renderers etc. live in
|
||||||
live in `/static/tabs.js` which /flow.html doesn't load. -->
|
`/static/tabs.js` which /flow.html doesn't load. -->
|
||||||
<script type="module" src="/static/flow.js" defer></script>
|
<script type="module" src="/static/flow.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
// /flow.html entry point (#406 step 2 — flow-specific split from the
|
// /flow.html entry point. Owns the full-page broker terminal, the
|
||||||
// previous combined entry; #406 step 3 renamed that combined entry
|
// operator-inbox derived store (populated from the broker stream),
|
||||||
// from `app.js` to `tabs.js`).
|
// the inbox pill flyout, and the @-mention compose box. Pulls shared
|
||||||
//
|
// infrastructure (DOM helpers, side panel, OS notifications, path
|
||||||
// Owns the full-page broker terminal, the operator-inbox derived store
|
// linkification) from `./common.js`.
|
||||||
// (populated from the broker stream), the inbox pill flyout, and the
|
|
||||||
// @-mention compose box. Pulls shared infrastructure (DOM helpers, side
|
|
||||||
// panel, OS notifications, path linkification) from `./common.js`.
|
|
||||||
//
|
//
|
||||||
// Does NOT contain the dashboard's tab renderers, mutation-event
|
// Does NOT contain the dashboard's tab renderers, mutation-event
|
||||||
// dispatchers, or refreshState — that's `./tabs.js`, loaded only by
|
// dispatchers, or refreshState — that's `./tabs.js`, loaded only by
|
||||||
|
|
@ -106,11 +103,11 @@ import {
|
||||||
if (!flow) return;
|
if (!flow) return;
|
||||||
flow.innerHTML = '';
|
flow.innerHTML = '';
|
||||||
const tsFmt = (n) => new Date(n * 1000).toISOString().slice(11, 19);
|
const tsFmt = (n) => new Date(n * 1000).toISOString().slice(11, 19);
|
||||||
// Pulse the page banner whenever a broker event lands. (Note:
|
// Pulse the page banner whenever a broker event lands. The
|
||||||
// post-#389 the `.banner` lives in the dashboard's <footer>, not
|
// `.banner` element lives in the dashboard's <footer> rather than
|
||||||
// in the flow page chrome — `pulseBanner` no-ops on /flow.html
|
// in the flow chrome — `pulseBanner` no-ops on /flow.html since
|
||||||
// since there's no element to find. Kept for parity if a future
|
// there's no element to find. Kept for parity if a future chrome
|
||||||
// chrome change reintroduces a banner.)
|
// change reintroduces a banner.
|
||||||
const banner = document.querySelector('.banner');
|
const banner = document.querySelector('.banner');
|
||||||
let bannerOffTimer = null;
|
let bannerOffTimer = null;
|
||||||
function pulseBanner() {
|
function pulseBanner() {
|
||||||
|
|
@ -174,33 +171,28 @@ import {
|
||||||
// Register this row so future replies can reference it.
|
// Register this row so future replies can reference it.
|
||||||
if (ev.id != null && ev.id > 0) msgRowMap.set(ev.id, row);
|
if (ev.id != null && ev.id > 0) msgRowMap.set(ev.id, row);
|
||||||
}
|
}
|
||||||
// Anchor the `↓ N new` pill in `.flow-main` (NOT the default
|
// Anchor the `↓ N new` pill in `.flow-main` rather than the
|
||||||
// `log.parentElement` = `.terminal-wrap`). `.terminal-wrap`
|
// default `.terminal-wrap` parent — see docs/web-ui.md::Per-agent
|
||||||
// applies `backdrop-filter`, which creates a CSS stacking
|
// page (Terminal-wrap) for the backdrop-filter stacking-context
|
||||||
// context — the pill's z-index would otherwise be trapped
|
// gotcha (same shape on the flow page).
|
||||||
// inside and clipped under the fixed composer (issue #375).
|
|
||||||
// `.flow-main` has no backdrop-filter / stacking-context
|
|
||||||
// creators, so the pill's z-index reaches the root and floats
|
|
||||||
// above the composer.
|
|
||||||
const flowMain = document.querySelector('.flow-main');
|
const flowMain = document.querySelector('.flow-main');
|
||||||
termCreate({
|
termCreate({
|
||||||
logEl: flow,
|
logEl: flow,
|
||||||
pillAnchor: flowMain,
|
pillAnchor: flowMain,
|
||||||
historyUrl: '/dashboard/history',
|
historyUrl: '/dashboard/history',
|
||||||
// #408: server-side filter — only the kinds this page actually
|
// Server-side filter — only the kinds this page actually renders
|
||||||
// renders or routes (sent/delivered → broker terminal,
|
// or routes (sent/delivered → broker terminal,
|
||||||
// container_state_changed/_removed → local autocomplete cache).
|
// container_state_changed/_removed → local autocomplete cache).
|
||||||
// Backend (#499) pre-parses the allow-list at subscribe time so
|
// Backend pre-parses the allow-list at subscribe time so the
|
||||||
// the per-frame hot path is one HashSet::contains and the
|
// per-frame hot path is one HashSet::contains and the
|
||||||
// JSON-serialise is skipped entirely on irrelevant kinds. The
|
// JSON-serialise is skipped entirely on irrelevant kinds. The
|
||||||
// dashboard tabs page (tabs.js) keeps the unfiltered subscribe
|
// dashboard tabs page (tabs.js) keeps the unfiltered subscribe
|
||||||
// since it routes every mutation kind into its derived stores.
|
// since it routes every mutation kind into its derived stores.
|
||||||
streamUrl: '/dashboard/stream?kinds=sent,delivered,container_state_changed,container_removed',
|
streamUrl: '/dashboard/stream?kinds=sent,delivered,container_state_changed,container_removed',
|
||||||
// #448: route through the SharedWorker so this page's SSE shares
|
// Route through the SharedWorker — see docs/web-ui.md (SSE
|
||||||
// a single backend connection with /index.html (and any other
|
// multiplexing paragraph). Worker keys on the full URL incl.
|
||||||
// open hyperhive tab). Worker keys on the full URL (incl.
|
// query string, so this filtered subscribe is its own upstream
|
||||||
// query string), so the filtered subscribe is its own upstream
|
// and won't accidentally share with tabs.js's wider subscribe.
|
||||||
// — won't accidentally share with tabs.js's wider subscribe.
|
|
||||||
streamFactory: openStream,
|
streamFactory: openStream,
|
||||||
renderers: {
|
renderers: {
|
||||||
sent: (ev, api) => renderMsg(ev, api, '→'),
|
sent: (ev, api) => renderMsg(ev, api, '→'),
|
||||||
|
|
@ -232,10 +224,9 @@ import {
|
||||||
// Re-sync the local containers cache on every SSE (re)connect.
|
// Re-sync the local containers cache on every SSE (re)connect.
|
||||||
// Live mutation events that fired during a disconnect window
|
// Live mutation events that fired during a disconnect window
|
||||||
// are never replayed, so without this the compose autocomplete
|
// are never replayed, so without this the compose autocomplete
|
||||||
// could drift stale (issue #163). We don't try to recover
|
// could drift stale. We don't try to recover missed broker rows
|
||||||
// missed broker rows here — operator inbox briefly stales on
|
// here — operator inbox briefly stales on reconnect; the
|
||||||
// reconnect; HiveTerminal's history-replay covers the next
|
// history-replay covers the next page load.
|
||||||
// page load.
|
|
||||||
onStreamOpen: () => {
|
onStreamOpen: () => {
|
||||||
fetch('/api/state').then((r) => r.ok ? r.json() : null).then((s) => {
|
fetch('/api/state').then((r) => r.ok ? r.json() : null).then((s) => {
|
||||||
if (!s || !Array.isArray(s.containers)) return;
|
if (!s || !Array.isArray(s.containers)) return;
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,11 @@
|
||||||
</head>
|
</head>
|
||||||
<body class="dashboard-shell">
|
<body class="dashboard-shell">
|
||||||
|
|
||||||
<!-- Sticky chrome — just the tab strip now (#389 follow-up: the
|
<!-- Sticky chrome — just the tab strip. The "WE ARE THE WIRED"
|
||||||
"WE ARE THE WIRED" slug moved out of chrome entirely and lives
|
slug lives at the page footer below `<main>`; chrome is
|
||||||
at the page footer below `<main>`; chrome is navigation only).
|
navigation only. Tabs route via the URL hash so F5 / back-
|
||||||
Tabs route via the URL hash so F5 / back-button / shared links
|
button / shared links keep you on the same view. JS owns
|
||||||
keep you on the same view. JS owns the actual show/hide; this
|
the show/hide. -->
|
||||||
is just the menu. -->
|
|
||||||
<header class="dashboard-chrome">
|
<header class="dashboard-chrome">
|
||||||
<nav class="tabbar" id="tabbar" role="tablist">
|
<nav class="tabbar" id="tabbar" role="tablist">
|
||||||
<a class="tab" id="tab-swarm" href="#swarm" role="tab"
|
<a class="tab" id="tab-swarm" href="#swarm" role="tab"
|
||||||
|
|
@ -34,10 +33,10 @@
|
||||||
<span class="tab-label">◆ SYST3M ◆</span>
|
<span class="tab-label">◆ SYST3M ◆</span>
|
||||||
<span class="tab-count" id="tab-count-system" hidden></span>
|
<span class="tab-count" id="tab-count-system" hidden></span>
|
||||||
</a>
|
</a>
|
||||||
<!-- SCH3DUL3S (#459): scheduled-prompts surface. List of
|
<!-- SCH3DUL3S: scheduled-prompts surface. List of queued
|
||||||
queued schedules + an operator-direct creation form.
|
schedules + an operator-direct creation form. Count pill
|
||||||
Count pill mirrors the active (non-cancelled) schedule
|
mirrors the active (non-cancelled) schedule count; hidden
|
||||||
count; hidden when zero. -->
|
when zero. -->
|
||||||
<a class="tab" id="tab-schedules" href="#schedules" role="tab"
|
<a class="tab" id="tab-schedules" href="#schedules" role="tab"
|
||||||
aria-controls="tab-pane-schedules"
|
aria-controls="tab-pane-schedules"
|
||||||
data-tab="schedules">
|
data-tab="schedules">
|
||||||
|
|
@ -45,24 +44,25 @@
|
||||||
<span class="tab-count" id="tab-count-schedules" hidden></span>
|
<span class="tab-count" id="tab-count-schedules" hidden></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- M4TR1X (#607): optional matrix web client (fluffychat-web by
|
<!-- M4TR1X: optional matrix web client (fluffychat-web by
|
||||||
default) mounted at /matrix/ by hive-c0re's dashboard router
|
default) mounted at /matrix/ by the gateway when
|
||||||
when `hyperhive.matrix.gui.enable = true`. Same-origin same-
|
`hyperhive.matrix.gui.enable = true`. Same-origin
|
||||||
tab navigation (the static dist is its own SPA). Hidden in JS
|
navigation (the static dist is its own SPA). Hidden in
|
||||||
when `state.matrix_gui_enabled === false` so operators without
|
JS when `state.matrix_gui_enabled === false` so operators
|
||||||
matrix-gui on don't see a dead link (#609 covers the
|
without matrix-gui on don't see a dead link. See
|
||||||
post-#15 nginx-front re-root + .well-known auto-discovery). -->
|
docs/web-ui.md::Tab strip for the gating model and
|
||||||
|
docs/gateway.md for the matrix vhost + .well-known
|
||||||
|
auto-discovery. -->
|
||||||
<a class="tab tab-link" id="tab-matrix" href="/matrix/" hidden
|
<a class="tab tab-link" id="tab-matrix" href="/matrix/" hidden
|
||||||
title="open the matrix chat client (fluffychat-web)">
|
title="open the matrix chat client (fluffychat-web)">
|
||||||
<span class="tab-label">◆ M4TR1X ◆ →</span>
|
<span class="tab-label">◆ M4TR1X ◆ →</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- FL0W is its own page (`/flow.html`), not a tab — per
|
<!-- FL0W is its own page (`/flow.html`), not a tab. The link
|
||||||
operator @ #369#issuecomment-3437 ("yes terminal can be a
|
lives in the tab strip so it reads as a peer surface;
|
||||||
separate page"). The link lives in the tab strip so it
|
clicking navigates rather than swapping panes in place.
|
||||||
reads as a peer surface; clicking navigates rather than
|
Count pill mirrors the dashboard's operator-inbox length
|
||||||
swapping panes in place. Count pill mirrors the dashboard's
|
and is hidden when zero. See docs/web-ui.md::FL0W page. -->
|
||||||
operator-inbox length and is hidden when zero. -->
|
|
||||||
<a class="tab tab-link" id="tab-flow" href="/flow.html"
|
<a class="tab tab-link" id="tab-flow" href="/flow.html"
|
||||||
title="open the all-agents chat in a dedicated full-page terminal">
|
title="open the all-agents chat in a dedicated full-page terminal">
|
||||||
<span class="tab-label">◆ FL0W ◆ →</span>
|
<span class="tab-label">◆ FL0W ◆ →</span>
|
||||||
|
|
@ -86,9 +86,8 @@
|
||||||
|
|
||||||
<!-- SW4RM: the swarm itself. Container cards (the central thing
|
<!-- SW4RM: the swarm itself. Container cards (the central thing
|
||||||
the operator looks at) and rebuild queue / cascade visualisation
|
the operator looks at) and rebuild queue / cascade visualisation
|
||||||
that drives them. The tab label itself reads SW4RM, so the
|
that drives them. No inline `C0NTAINERS` h2 heading + divider
|
||||||
inline C0NTAINERS h2 heading + divider would be redundant —
|
— the tab label SW4RM already says it. -->
|
||||||
dropped per #385. -->
|
|
||||||
<section class="tab-pane" id="tab-pane-swarm"
|
<section class="tab-pane" id="tab-pane-swarm"
|
||||||
role="tabpanel" aria-labelledby="tab-swarm">
|
role="tabpanel" aria-labelledby="tab-swarm">
|
||||||
<div id="containers-section">
|
<div id="containers-section">
|
||||||
|
|
@ -117,11 +116,8 @@
|
||||||
|
|
||||||
<!-- SYST3M: passive / rare-interaction state. Meta inputs (lock
|
<!-- SYST3M: passive / rare-interaction state. Meta inputs (lock
|
||||||
bumps), rebuild queue (watch only), kept state from previous
|
bumps), rebuild queue (watch only), kept state from previous
|
||||||
tombstoned agents. Queued reminders moved to the SCH3DUL3S
|
tombstoned agents. Per-section content auto-compresses to a
|
||||||
tab in #460 — they're conceptually "fire X at time Y" too,
|
one-line summary when empty (JS toggle). -->
|
||||||
just self-scheduled by agents instead of operator-set.
|
|
||||||
Headings stay; the per-section content auto-compresses to a
|
|
||||||
one-line summary when empty (separate JS toggle). -->
|
|
||||||
<section class="tab-pane" id="tab-pane-system"
|
<section class="tab-pane" id="tab-pane-system"
|
||||||
role="tabpanel" aria-labelledby="tab-system">
|
role="tabpanel" aria-labelledby="tab-system">
|
||||||
<h2>◆ M3T4 1NPUTS ◆</h2>
|
<h2>◆ M3T4 1NPUTS ◆</h2>
|
||||||
|
|
@ -145,16 +141,15 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- SCH3DUL3S (#459, #564): scheduled prompts. Creation + edit
|
<!-- SCH3DUL3S: scheduled prompts. Creation + edit are folded
|
||||||
are folded into the same table now (#564) — empty bottom row
|
into the same table — empty bottom row is the create form
|
||||||
is the create form (fill cells, click +), inline-edit-row
|
(fill cells, click +), inline-edit-row expands on the `✎`
|
||||||
expands on the `✎` toggle for existing schedules. Live
|
toggle for existing schedules. Schedules list driven by
|
||||||
schedules list driven by GET /api/schedules; POST /api/schedules
|
GET /api/schedules; POST /api/schedules to create, PATCH
|
||||||
to create, PATCH /api/schedules/{id} to edit, POST
|
/api/schedules/{id} to edit, POST /api/schedules/{id}/cancel
|
||||||
/api/schedules/{id}/cancel for per-target / whole-row cancel.
|
for per-target / whole-row cancel. No SchedulesChanged SSE
|
||||||
#444 backend doesn't emit SchedulesChanged dashboard event yet,
|
event yet, so the list re-fetches on tab activation + after
|
||||||
so the list re-fetches on tab activation + after each submit /
|
each submit / cancel. See docs/web-ui.md::SCH3DUL3S tab. -->
|
||||||
cancel. Live SSE wiring is the future PR C. -->
|
|
||||||
<section class="tab-pane" id="tab-pane-schedules"
|
<section class="tab-pane" id="tab-pane-schedules"
|
||||||
role="tabpanel" aria-labelledby="tab-schedules">
|
role="tabpanel" aria-labelledby="tab-schedules">
|
||||||
<h2>◆ SCH3DUL3S ◆</h2>
|
<h2>◆ SCH3DUL3S ◆</h2>
|
||||||
|
|
@ -164,12 +159,12 @@
|
||||||
<p class="meta">loading…</p>
|
<p class="meta">loading…</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- QU3U3D R3M1ND3RS (#460): self-scheduled agent reminders.
|
<!-- QU3U3D R3M1ND3RS: self-scheduled agent reminders. Lives
|
||||||
Moved here from the SYST3M tab so the operator has one
|
on this tab so the operator has one place for everything
|
||||||
place for everything that fires at a future time —
|
that fires at a future time — operator-set schedules
|
||||||
operator-set schedules above, agent-self reminders here.
|
above, agent-self reminders here. Backed by GET
|
||||||
Backed by GET /api/reminders; refresh handled by
|
/api/reminders; refresh handled by refreshReminders()
|
||||||
refreshReminders() (called from refreshState). -->
|
(called from refreshState). -->
|
||||||
<h2>◆ QU3U3D R3M1ND3RS ◆</h2>
|
<h2>◆ QU3U3D R3M1ND3RS ◆</h2>
|
||||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||||
<p class="meta">reminders agents have queued for themselves but not yet delivered. cancel to drop a stuck or unwanted entry.</p>
|
<p class="meta">reminders agents have queued for themselves but not yet delivered. cancel to drop a stuck or unwanted entry.</p>
|
||||||
|
|
@ -209,11 +204,10 @@
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Selection action bar (#443). Sticky-bottom strip that slides
|
<!-- Selection action bar. Sticky-bottom strip that slides into
|
||||||
into view when one or more agent cards is selected (click the
|
view when one or more agent cards is selected (click the icon
|
||||||
icon to toggle). Shows the selection count + actions that
|
to toggle). See docs/web-ui.md::Selection bar for the bulk
|
||||||
apply to ALL selected; disabled-with-tooltip for actions that
|
action gating + clear semantics. -->
|
||||||
don't (mara picked option B). Hidden when selection is empty. -->
|
|
||||||
<div id="selection-bar" class="selection-bar" hidden role="toolbar"
|
<div id="selection-bar" class="selection-bar" hidden role="toolbar"
|
||||||
aria-label="bulk agent actions">
|
aria-label="bulk agent actions">
|
||||||
<span class="selection-count" id="selection-count"></span>
|
<span class="selection-count" id="selection-count"></span>
|
||||||
|
|
@ -223,11 +217,10 @@
|
||||||
title="clear selection (esc)">✕ clear</button>
|
title="clear selection (esc)">✕ clear</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Single bundled entry (#406 step 3 — renamed from app.js to
|
<!-- Single bundled entry — tabs.js is the dashboard tabs surface;
|
||||||
tabs.js since this bundle is the dashboard *tabs* surface only;
|
flow.html has its own flow.js bundle. esbuild folds
|
||||||
flow.html has its own flow.js bundle). esbuild folds
|
|
||||||
@hive/shared/terminal.js and the marked npm package into
|
@hive/shared/terminal.js and the marked npm package into
|
||||||
tabs.js; load order is preserved by the module bundler. -->
|
tabs.js. -->
|
||||||
<script type="module" src="/static/tabs.js" defer></script>
|
<script type="module" src="/static/tabs.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue