refactor(flow): drop operator-inbox UI, now lives on Y3R C4LL (#1483)
The operator inbox moved to the dashboard's Y3R C4LL tab in #1469 (◆ 1NB0X ◆ section, with per-message + mark-all read). FL0W goes back to being the pure event firehose, so remove its now-redundant inbox UI: - flow.js: drop the operatorInbox store, inboxAppendFromEvent, buildInboxListNode, renderInbox, the inbox-pill click wiring, and the onAnyEvent hook that fed them. The side panel was only used for the inbox flyout on this page, so drop Panel.bind() + the Panel import too. - flow.html: remove the inbox pill, the offscreen inbox-section div, and the side-panel markup. - flow.css: remove the .flow-pill* and .flow-inbox-headless rules. - home.html: Flow tile desc → "live all-agents message firehose". - docs/web-ui/dashboard.md: drop the FL0W 0PER4T0R 1NB0X section, point at Y3R C4LL, and fix the count-pill + /op-send descriptions. The agent filter, sent→delivered collapse, compose box, and OS notifications on operator-bound traffic are unaffected.
This commit is contained in:
parent
f9fe3280f9
commit
e949129e75
5 changed files with 27 additions and 183 deletions
|
|
@ -18,9 +18,9 @@ swap.
|
||||||
`◆ P33RS ◆` (hidden when `swarm.peers` is empty),
|
`◆ P33RS ◆` (hidden when `swarm.peers` is empty),
|
||||||
`◆ M4TR1X ◆ →` (optional page link, see below), `◆ FL0W ◆ →`
|
`◆ M4TR1X ◆ →` (optional page link, see below), `◆ FL0W ◆ →`
|
||||||
(page link), and `◆ S3TT1NGS ◆`. Count pills on SW4RM (container
|
(page link), and `◆ S3TT1NGS ◆`. Count pills on SW4RM (container
|
||||||
count), Y3R C4LL (pending approvals + questions), and SCH3DUL3S
|
count), Y3R C4LL (pending approvals + questions + unread operator
|
||||||
(active schedules); FL0W pill mirrors the operator inbox length
|
messages), and SCH3DUL3S (active schedules); P33RS and S3TT1NGS
|
||||||
(hidden when zero); P33RS and S3TT1NGS have no count. The M4TR1X →
|
have no count. The M4TR1X →
|
||||||
entry is hidden when `services.hyperhive.matrix.gui.enable` is off
|
entry is hidden when `services.hyperhive.matrix.gui.enable` is off
|
||||||
(defaults to `matrix.enable`) so operators without the matrix GUI
|
(defaults to `matrix.enable`) so operators without the matrix GUI
|
||||||
on don't see a dead link — tabs.js gates the `hidden` attribute on
|
on don't see a dead link — tabs.js gates the `hidden` attribute on
|
||||||
|
|
@ -368,7 +368,7 @@ Operator-local preferences. State lives in the browser's
|
||||||
`localStorage` — preferences do NOT sync between devices and
|
`localStorage` — preferences do NOT sync between devices and
|
||||||
do NOT survive a profile wipe. Today the tab holds one section
|
do NOT survive a profile wipe. Today the tab holds one section
|
||||||
(browser notifications); future preferences (theme, density,
|
(browser notifications); future preferences (theme, density,
|
||||||
inbox-pill threshold, etc.) land here as sibling `<h3>` blocks
|
etc.) land here as sibling `<h3>` blocks
|
||||||
under the same `<section id="tab-pane-settings">`.
|
under the same `<section id="tab-pane-settings">`.
|
||||||
|
|
||||||
**◇ browser notifications** — `🔔 enable notifications` button when
|
**◇ browser notifications** — `🔔 enable notifications` button when
|
||||||
|
|
@ -415,11 +415,9 @@ as the dashboard so the tab strip remains visible; SW4RM / Y3R
|
||||||
C4LL / SYST3M / SCH3DUL3S are cross-page links back to `/#<tab>`,
|
C4LL / SYST3M / SCH3DUL3S are cross-page links back to `/#<tab>`,
|
||||||
and the FL0W entry is marked active (`aria-current="page"`).
|
and the FL0W entry is marked active (`aria-current="page"`).
|
||||||
|
|
||||||
**0PER4T0R 1NB0X** — recent messages addressed to `operator`,
|
The operator inbox is **not** on this page — it lives on the
|
||||||
derived client-side from the dashboard event stream. Cold load
|
dashboard's Y3R C4LL tab (◆ 1NB0X ◆ section, with per-message and
|
||||||
seeds from `/dashboard/history`'s 200-message backfill; subsequent
|
mark-all read). FL0W stays the pure event firehose.
|
||||||
`sent` events with `to == "operator"` are appended live. Cap 50,
|
|
||||||
newest-first.
|
|
||||||
|
|
||||||
**MESS4GE FL0W** — live broker tail wrapped in a `.terminal-wrap`.
|
**MESS4GE FL0W** — live broker tail wrapped in a `.terminal-wrap`.
|
||||||
Cold load backfills the last ~200 messages from `/dashboard/history`;
|
Cold load backfills the last ~200 messages from `/dashboard/history`;
|
||||||
|
|
@ -437,8 +435,7 @@ terminal-style compose box: `@name` picks the recipient (sticky via
|
||||||
localStorage; auto-complete from the live container list, Tab/Enter
|
localStorage; auto-complete from the live container list, Tab/Enter
|
||||||
to confirm; `@*` broadcasts). `POST /op-send` drops
|
to confirm; `@*` broadcasts). `POST /op-send` drops
|
||||||
`{from:"operator", to, body}` into the broker; the resulting SSE
|
`{from:"operator", to, body}` into the broker; the resulting SSE
|
||||||
frame re-renders both the terminal row and the inbox section.
|
frame re-renders the terminal row. Manager is addressed as `@root`.
|
||||||
Manager is addressed as `@root`.
|
|
||||||
|
|
||||||
## L0GS page (`/logs.html`)
|
## L0GS page (`/logs.html`)
|
||||||
|
|
||||||
|
|
@ -805,9 +802,8 @@ that's a browser-level decision, not ours.
|
||||||
- `POST /op-send` (`to=<name>`, `body=<text>`) — drop an
|
- `POST /op-send` (`to=<name>`, `body=<text>`) — drop an
|
||||||
operator-authored message into `<name>`'s inbox. `to=*` fans
|
operator-authored message into `<name>`'s inbox. `to=*` fans
|
||||||
out to every registered agent. Returns 200; the broker
|
out to every registered agent. Returns 200; the broker
|
||||||
`Sent` event re-renders both the message-flow terminal and
|
`Sent` event re-renders the message-flow terminal without a
|
||||||
the operator inbox without a snapshot refetch. Used by the
|
snapshot refetch. Used by the compose textbox under MESS4GE FL0W.
|
||||||
compose textbox under MESS4GE FL0W.
|
|
||||||
- `GET /api/build-logs/{agent}?limit=N` — most-recent build log
|
- `GET /api/build-logs/{agent}?limit=N` — most-recent build log
|
||||||
headers for one agent, newest first. Returns
|
headers for one agent, newest first. Returns
|
||||||
`Vec<BuildLogHeader>` (JSON): `id`, `agent`, `kind`, `cmdline`,
|
`Vec<BuildLogHeader>` (JSON): `id`, `agent`, `kind`, `cmdline`,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
/* ─── /flow.html — full-page chat ─────────────────────────────────
|
/* ─── /flow.html — full-page chat ─────────────────────────────────
|
||||||
See docs/web-ui.md::FL0W page for the page-vs-pane rationale
|
See docs/web-ui.md::FL0W page for the page-vs-pane rationale.
|
||||||
and the inbox-flyout-as-pill ergonomics. Shape mirrors the
|
Shape mirrors the per-agent live page (frosted-glass header +
|
||||||
per-agent live page (frosted-glass header + composer, full-
|
composer, full-viewport terminal). */
|
||||||
viewport terminal). */
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Approximate height of the flow chrome (tabbar + dashboard-chrome
|
/* Approximate height of the flow chrome (tabbar + dashboard-chrome
|
||||||
|
|
@ -59,47 +58,6 @@ body.flow-shell .tabbar .tab.active.tab-link {
|
||||||
border-color: var(--purple-dim);
|
border-color: var(--purple-dim);
|
||||||
box-shadow: 0 -2px 12px -4px rgba(203, 166, 247, 0.4);
|
box-shadow: 0 -2px 12px -4px rgba(203, 166, 247, 0.4);
|
||||||
}
|
}
|
||||||
/* Inbox pill — operator inbox flyout trigger. Sits right under the
|
|
||||||
header so it stays in the operator's gaze without crowding the
|
|
||||||
chat. Same shape as the agent page's header pills. */
|
|
||||||
.flow-pill {
|
|
||||||
position: fixed;
|
|
||||||
top: calc(var(--flow-header-h) + 0.8em);
|
|
||||||
right: 1em;
|
|
||||||
z-index: 25;
|
|
||||||
background: var(--bg-elev);
|
|
||||||
border: 1px solid var(--purple-dim);
|
|
||||||
color: var(--fg);
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.85em;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.3em 0.8em;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5em;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
|
|
||||||
transition: border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease;
|
|
||||||
}
|
|
||||||
.flow-pill:hover {
|
|
||||||
border-color: var(--purple);
|
|
||||||
color: var(--purple);
|
|
||||||
box-shadow: 0 0 12px -2px var(--purple);
|
|
||||||
}
|
|
||||||
.flow-pill-icon { font-size: 1.05em; line-height: 1; }
|
|
||||||
.flow-pill-label { color: var(--muted); }
|
|
||||||
.flow-pill-count {
|
|
||||||
background: rgba(250, 179, 135, 0.18);
|
|
||||||
color: var(--amber);
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0 0.5em;
|
|
||||||
min-width: 1.6em;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flow-main {
|
.flow-main {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -159,11 +117,6 @@ body.flow-shell .flow-main-slim {
|
||||||
padding: 0.4em 1em;
|
padding: 0.4em 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hidden inbox section on the flow page — the renderInbox path
|
|
||||||
wants `#inbox-section` in the DOM (legacy contract), but we
|
|
||||||
surface the messages via the pill/flyout instead. */
|
|
||||||
.flow-inbox-headless { display: none !important; }
|
|
||||||
|
|
||||||
/* Agent filter (#1473): a select in the FL0W header narrows the
|
/* Agent filter (#1473): a select in the FL0W header narrows the
|
||||||
timeline to messages involving one agent. Non-matching rows get
|
timeline to messages involving one agent. Non-matching rows get
|
||||||
`.flow-hidden`; the select reuses the journal-unit control look. */
|
`.flow-hidden`; the select reuses the journal-unit control look. */
|
||||||
|
|
|
||||||
|
|
@ -23,16 +23,6 @@
|
||||||
</select>
|
</select>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Operator inbox flyout trigger — count + click → side panel
|
|
||||||
(singleton, declared below). Hidden until the inbox is non-
|
|
||||||
empty. Mirrors the agent page's header pill pattern. -->
|
|
||||||
<button type="button" id="inbox-pill" class="flow-pill" hidden
|
|
||||||
title="open operator inbox">
|
|
||||||
<span class="flow-pill-icon" aria-hidden="true">📬</span>
|
|
||||||
<span class="flow-pill-label">inbox</span>
|
|
||||||
<span class="flow-pill-count" id="inbox-pill-count">0</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Main content: the full-viewport terminal. Padded for the
|
<!-- Main content: the full-viewport terminal. Padded for the
|
||||||
overlay header + composer so the first/last rows stay
|
overlay header + composer so the first/last rows stay
|
||||||
reachable. -->
|
reachable. -->
|
||||||
|
|
@ -55,34 +45,7 @@
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- Inbox rendered offscreen — kept in the DOM so flow.js's
|
<!-- Flow-specific bundle. Contains the broker terminal init + the
|
||||||
renderInbox keeps working unchanged. The pill click handler
|
|
||||||
opens the side panel which displays a clone of the list. The
|
|
||||||
legacy section heading would otherwise be visible; hidden
|
|
||||||
here. -->
|
|
||||||
<div id="inbox-section" class="flow-inbox-headless" hidden>
|
|
||||||
<p class="meta">loading…</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Slide-in side panel. Singleton — JS swaps the title + body
|
|
||||||
and toggles `.open`. On this page only used to surface the
|
|
||||||
operator inbox flyout; the dashboard's other panel uses
|
|
||||||
(approval diffs, file previews, logs) don't apply here. -->
|
|
||||||
<div id="side-panel" class="side-panel" aria-hidden="true">
|
|
||||||
<div class="side-panel-backdrop" id="side-panel-backdrop"></div>
|
|
||||||
<aside class="side-panel-drawer" role="dialog" aria-modal="true"
|
|
||||||
aria-labelledby="side-panel-title">
|
|
||||||
<header class="side-panel-head">
|
|
||||||
<span class="side-panel-title" id="side-panel-title"></span>
|
|
||||||
<button type="button" class="side-panel-close" id="side-panel-close"
|
|
||||||
title="close (esc)">✕</button>
|
|
||||||
</header>
|
|
||||||
<div class="side-panel-body" id="side-panel-body"></div>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Flow-specific bundle. Contains the broker terminal init, the
|
|
||||||
operator-inbox derived store, the inbox pill flyout, and the
|
|
||||||
@-mention composer. Tab renderers etc. live in
|
@-mention composer. Tab renderers etc. 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>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
// /flow.html entry point. Owns the full-page broker terminal, the
|
// /flow.html entry point. Owns the full-page broker terminal and the
|
||||||
// operator-inbox derived store (populated from the broker stream),
|
// @-mention compose box. Pulls shared infrastructure (DOM helpers,
|
||||||
// the inbox pill flyout, and the @-mention compose box. Pulls shared
|
// side panel, OS notifications, path linkification) from `./common.js`.
|
||||||
// infrastructure (DOM helpers, side panel, OS notifications, path
|
//
|
||||||
// linkification) from `./common.js`.
|
// The operator inbox lives on the dashboard's Y3R C4LL tab now; FL0W
|
||||||
|
// stays the pure event firehose.
|
||||||
//
|
//
|
||||||
// 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
|
||||||
|
|
@ -13,74 +14,14 @@
|
||||||
import { create as termCreate } from '@hive/shared/terminal.js';
|
import { create as termCreate } from '@hive/shared/terminal.js';
|
||||||
import {
|
import {
|
||||||
$, el,
|
$, el,
|
||||||
Panel, NOTIF,
|
NOTIF,
|
||||||
appendLinkified,
|
appendLinkified,
|
||||||
openStream,
|
openStream,
|
||||||
} from './common.js';
|
} from './common.js';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
Panel.bind();
|
|
||||||
NOTIF.bind();
|
NOTIF.bind();
|
||||||
|
|
||||||
// ─── operator inbox (derived from the broker message stream) ───────────
|
|
||||||
// No longer shipped on `/api/state.operator_inbox`. The broker
|
|
||||||
// terminal feeds this via `onAnyEvent` — backfill from
|
|
||||||
// `/dashboard/history` populates on load, live SSE keeps it current.
|
|
||||||
// Newest-first to match the previous behaviour.
|
|
||||||
const INBOX_LIMIT = 50;
|
|
||||||
const operatorInbox = [];
|
|
||||||
function inboxAppendFromEvent(ev) {
|
|
||||||
if (ev.kind !== 'sent' || ev.to !== 'operator') return false;
|
|
||||||
operatorInbox.unshift({
|
|
||||||
from: ev.from,
|
|
||||||
body: ev.body,
|
|
||||||
at: ev.at,
|
|
||||||
file_refs: ev.file_refs || [],
|
|
||||||
});
|
|
||||||
if (operatorInbox.length > INBOX_LIMIT) operatorInbox.length = INBOX_LIMIT;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
function buildInboxListNode() {
|
|
||||||
if (!operatorInbox.length) return el('p', { class: 'empty' }, 'no messages');
|
|
||||||
const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19);
|
|
||||||
const ul = el('ul', { class: 'inbox' });
|
|
||||||
for (const m of operatorInbox) {
|
|
||||||
const li = el('li');
|
|
||||||
const body = el('span', { class: 'msg-body' });
|
|
||||||
appendLinkified(body, m.body, m.file_refs);
|
|
||||||
li.append(
|
|
||||||
el('span', { class: 'msg-ts' }, fmt(m.at)), ' ',
|
|
||||||
el('span', { class: 'msg-from' }, m.from), ' ',
|
|
||||||
el('span', { class: 'msg-sep' }, '→ '),
|
|
||||||
body,
|
|
||||||
);
|
|
||||||
ul.append(li);
|
|
||||||
}
|
|
||||||
return ul;
|
|
||||||
}
|
|
||||||
function renderInbox() {
|
|
||||||
// Flow page surfaces inbox as a pill that opens the side-panel
|
|
||||||
// flyout. Pill is hidden when empty; click handler below opens
|
|
||||||
// the panel with the freshest list. If the panel is already
|
|
||||||
// showing the inbox view, refresh its body in place so live
|
|
||||||
// messages land without a re-open.
|
|
||||||
const pill = $('inbox-pill');
|
|
||||||
const pillCount = $('inbox-pill-count');
|
|
||||||
if (pillCount) pillCount.textContent = String(operatorInbox.length);
|
|
||||||
if (pill) pill.hidden = operatorInbox.length === 0;
|
|
||||||
Panel.refresh('inbox', 'inbox · ' + operatorInbox.length, buildInboxListNode());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wire the inbox pill to open the side-panel flyout with the
|
|
||||||
// operator inbox.
|
|
||||||
const inboxPill = $('inbox-pill');
|
|
||||||
if (inboxPill) {
|
|
||||||
inboxPill.addEventListener('click', () => {
|
|
||||||
Panel.openNamed('inbox', 'inbox · ' + operatorInbox.length,
|
|
||||||
buildInboxListNode());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── local containers cache (for compose autocomplete) ──────────────────
|
// ─── local containers cache (for compose autocomplete) ──────────────────
|
||||||
// The compose box's @-mention completion suggests known agent names.
|
// The compose box's @-mention completion suggests known agent names.
|
||||||
// /index.html (tabs.js) maintains the canonical `containersState`
|
// /index.html (tabs.js) maintains the canonical `containersState`
|
||||||
|
|
@ -144,8 +85,8 @@ import {
|
||||||
// ─── message flow: shared terminal pane ────────────────────────────────
|
// ─── message flow: shared terminal pane ────────────────────────────────
|
||||||
// Scroll, pill, backfill + SSE plumbing live in @hive/shared/terminal.
|
// Scroll, pill, backfill + SSE plumbing live in @hive/shared/terminal.
|
||||||
// What stays here is the broker-message renderer + the page-local
|
// What stays here is the broker-message renderer + the page-local
|
||||||
// side effects (banner pulse, inbox refresh on operator-bound
|
// side effects (banner pulse, OS notifications on operator-bound
|
||||||
// traffic, OS notifications).
|
// traffic).
|
||||||
(() => {
|
(() => {
|
||||||
const flow = $('msgflow');
|
const flow = $('msgflow');
|
||||||
if (!flow) return;
|
if (!flow) return;
|
||||||
|
|
@ -310,19 +251,10 @@ import {
|
||||||
// tabs handle these on /index.html via tabs.js.
|
// tabs handle these on /index.html via tabs.js.
|
||||||
_default: () => {},
|
_default: () => {},
|
||||||
},
|
},
|
||||||
// Both history backfill and live frames flow through here, so the
|
|
||||||
// inbox section ends up populated correctly on first paint and
|
|
||||||
// updated thereafter — no /api/state refetch needed for inbox
|
|
||||||
// freshness.
|
|
||||||
onAnyEvent: (ev /* , { fromHistory } */) => {
|
|
||||||
if (inboxAppendFromEvent(ev)) renderInbox();
|
|
||||||
},
|
|
||||||
// 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. We don't try to recover missed broker rows
|
// could drift stale.
|
||||||
// here — operator inbox briefly stales on reconnect; the
|
|
||||||
// history-replay covers the next 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;
|
||||||
|
|
@ -448,8 +380,8 @@ import {
|
||||||
input.disabled = true;
|
input.disabled = true;
|
||||||
try {
|
try {
|
||||||
// /op-send returns 200. The SSE channel carries the resulting
|
// /op-send returns 200. The SSE channel carries the resulting
|
||||||
// MessageEvent → the terminal renders the sent row + the
|
// MessageEvent → the terminal renders the sent row on its own;
|
||||||
// inbox updates on its own; no /api/state refetch needed.
|
// no /api/state refetch needed.
|
||||||
const resp = await fetch('/op-send', {
|
const resp = await fetch('/op-send', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: new URLSearchParams(fd),
|
body: new URLSearchParams(fd),
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
<a class="home-tile" href="/flow.html">
|
<a class="home-tile" href="/flow.html">
|
||||||
<span class="home-tile-label">Flow</span>
|
<span class="home-tile-label">Flow</span>
|
||||||
<span class="home-tile-desc">all-agents chat + operator inbox</span>
|
<span class="home-tile-desc">live all-agents message firehose</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="home-tile" href="/logs.html">
|
<a class="home-tile" href="/logs.html">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue