Y3R C4LL is the operator inbox alone: no P3NDING APPR0VALS section, no approval cards or history, no approval_resolved subscription, no approval browser notifications, and the tab count and title prefix count unread operator messages only. The H0M3 tile text drops "approvals". The deny form was the only data-prompt form, so bindAsyncForms' prompt branch, themedPrompt and its dialog styles go too, along with the approval-only CSS (.approval*, .approvals-history, .btn-deny, the status/glyph colours and .history-note).
114 lines
3.8 KiB
JavaScript
114 lines
3.8 KiB
JavaScript
// Y3R C4LL domain — the dashboard pane for things blocked on an operator
|
|
// decision: the operator inbox (messages agents sent to
|
|
// `to="operator"`). Lifted out of the dashboard entry (tabs.js) into its
|
|
// own module so the entry stays a thin coordinator; mirrors the
|
|
// permissions.js / schedules.js extractions.
|
|
//
|
|
// Tab-count wiring: like schedules.js this module does not call the
|
|
// coordinator's `refreshTabCounts` directly (that would be a circular
|
|
// import). Instead it exposes count getters the coordinator pulls, and the
|
|
// live-mutation paths call an injected `onCountsChanged` callback the entry
|
|
// registers once via `initCall`.
|
|
|
|
import { $, appendLinkified } from "./common.js";
|
|
import { el } from "@hive/shared/dom.js";
|
|
|
|
// Registered by the dashboard entry at boot; defaults to a no-op so the
|
|
// module is safe to call before wiring.
|
|
let onCountsChanged = () => {};
|
|
|
|
export function initCall(opts = {}) {
|
|
if (typeof opts.onCountsChanged === "function")
|
|
onCountsChanged = opts.onCountsChanged;
|
|
}
|
|
|
|
// ─── operator inbox — unread agent→operator messages ────────────
|
|
// The Y3R C4LL tab surfaces messages agents `send(to: "operator")` so
|
|
// the operator stops missing them. Unread = broker rows to "operator"
|
|
// with `acked_at IS NULL`; cold-loaded from `/api/operator-inbox`,
|
|
// appended live from the broker `sent` stream, and cleared via the
|
|
// existing per-recipient ack (`POST /api/agent/operator/mark-all-read`).
|
|
// Count folds into the Y3R C4LL pill + browser-title prefix.
|
|
let operatorInbox = []; // [{ id, from, body, at, file_refs }], newest-first
|
|
|
|
export function operatorInboxCount() {
|
|
return operatorInbox.length;
|
|
}
|
|
|
|
export async function refreshOperatorInbox() {
|
|
try {
|
|
const r = await fetch("/api/operator-inbox");
|
|
if (r.ok) {
|
|
const data = await r.json();
|
|
operatorInbox = Array.isArray(data.messages) ? data.messages : [];
|
|
}
|
|
} catch {
|
|
/* keep prior list on transient failure */
|
|
}
|
|
renderOperatorInbox();
|
|
onCountsChanged();
|
|
}
|
|
|
|
function renderOperatorInbox() {
|
|
const root = $("operator-inbox-section");
|
|
if (!root) return;
|
|
root.replaceChildren();
|
|
if (!operatorInbox.length) {
|
|
root.append(el("p", { class: "meta" }, "no unread messages"));
|
|
return;
|
|
}
|
|
const mark = el(
|
|
"button",
|
|
{ type: "button", class: "btn", id: "op-inbox-mark-read" },
|
|
`✓ mark all read (${operatorInbox.length})`,
|
|
);
|
|
mark.addEventListener("click", markOperatorInboxRead);
|
|
root.append(el("div", { class: "inbox-toolbar" }, mark));
|
|
const fmt = (ts) => new Date(ts).toISOString().replace("T", " ").slice(0, 19);
|
|
const ul = el("ul", { class: "inbox" });
|
|
for (const m of operatorInbox) {
|
|
const body = el("span", { class: "msg-body" });
|
|
appendLinkified(body, m.body, m.file_refs);
|
|
ul.append(
|
|
el(
|
|
"li",
|
|
{},
|
|
el("span", { class: "msg-ts" }, fmt(m.at)),
|
|
" ",
|
|
el("span", { class: "msg-from" }, m.from),
|
|
" ",
|
|
el("span", { class: "msg-sep" }, "→ "),
|
|
body,
|
|
),
|
|
);
|
|
}
|
|
root.append(ul);
|
|
}
|
|
|
|
async function markOperatorInboxRead() {
|
|
try {
|
|
await fetch("/api/agent/operator/mark-all-read", { method: "POST" });
|
|
} catch {
|
|
/* best-effort; the next refresh reconciles */
|
|
}
|
|
operatorInbox = [];
|
|
renderOperatorInbox();
|
|
onCountsChanged();
|
|
}
|
|
|
|
// Live append from the broker stream — a `sent` frame addressed to
|
|
// "operator". De-dupes on broker row id so a history/live overlap or
|
|
// a refresh racing the stream doesn't double-list.
|
|
export function operatorInboxAppendFromEvent(ev) {
|
|
if (ev.id != null && operatorInbox.some((m) => m.id === ev.id)) return;
|
|
operatorInbox.unshift({
|
|
id: ev.id,
|
|
from: ev.from,
|
|
body: ev.body,
|
|
at: ev.at,
|
|
file_refs: ev.file_refs || [],
|
|
});
|
|
if (operatorInbox.length > 100) operatorInbox.length = 100;
|
|
renderOperatorInbox();
|
|
onCountsChanged();
|
|
}
|