dashboard: derive operator inbox from message stream (drop snapshot field + refetch workaround)
This commit is contained in:
parent
1340a654e7
commit
fb669c17c8
3 changed files with 42 additions and 30 deletions
|
|
@ -118,20 +118,20 @@
|
|||
// when the page reloads.
|
||||
const seenApprovals = new Set();
|
||||
const seenQuestions = new Set();
|
||||
const seenInboxIds = new Set();
|
||||
let seededNotify = false;
|
||||
|
||||
function notifyDeltas(s) {
|
||||
const approvals = s.approvals || [];
|
||||
const questions = s.questions || [];
|
||||
const inbox = s.operator_inbox || [];
|
||||
if (!seededNotify) {
|
||||
// First render after page load — fill the "seen" sets without
|
||||
// firing notifications. We only want to notify on NEW items
|
||||
// that arrived while the page is open.
|
||||
// that arrived while the page is open. The inbox no longer
|
||||
// needs seeding here: it's derived from the broker stream which
|
||||
// does its own per-event notification on live arrival, and
|
||||
// history-replayed events are silent by virtue of `fromHistory`.
|
||||
for (const a of approvals) seenApprovals.add(a.id);
|
||||
for (const q of questions) seenQuestions.add(q.id);
|
||||
for (const m of inbox) seenInboxIds.add(m.id);
|
||||
seededNotify = true;
|
||||
return;
|
||||
}
|
||||
|
|
@ -148,14 +148,6 @@
|
|||
NOTIF.show('◆ manager asks', q.question.slice(0, 120),
|
||||
'hyperhive:question:' + q.id);
|
||||
}
|
||||
// operator_inbox: only notify on truly new ids — sse already
|
||||
// handles single-message notifications, but if the operator
|
||||
// missed an SSE event (page reloaded), this catches up.
|
||||
for (const m of inbox) {
|
||||
if (seenInboxIds.has(m.id)) continue;
|
||||
seenInboxIds.add(m.id);
|
||||
// suppress here; SSE path handles the live notification.
|
||||
}
|
||||
}
|
||||
|
||||
// ─── async forms ────────────────────────────────────────────────────────
|
||||
|
|
@ -605,16 +597,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
function renderInbox(s) {
|
||||
// ─── operator inbox (derived from the broker message stream) ───────────
|
||||
// No longer shipped on `/api/state.operator_inbox`. The dashboard
|
||||
// terminal's HiveTerminal feeds this via `onAnyEvent` — backfill from
|
||||
// `/messages/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 });
|
||||
if (operatorInbox.length > INBOX_LIMIT) operatorInbox.length = INBOX_LIMIT;
|
||||
return true;
|
||||
}
|
||||
function renderInbox() {
|
||||
const root = $('inbox-section');
|
||||
if (!root) return;
|
||||
root.innerHTML = '';
|
||||
if (!s.operator_inbox || !s.operator_inbox.length) {
|
||||
if (!operatorInbox.length) {
|
||||
root.append(el('p', { class: 'empty' }, 'no messages'));
|
||||
return;
|
||||
}
|
||||
const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19);
|
||||
const ul = el('ul', { class: 'inbox' });
|
||||
for (const m of s.operator_inbox) {
|
||||
for (const m of operatorInbox) {
|
||||
const li = el('li');
|
||||
li.append(
|
||||
el('span', { class: 'msg-ts' }, fmt(m.at)), ' ',
|
||||
|
|
@ -932,7 +938,7 @@
|
|||
renderContainers(s);
|
||||
renderTombstones(s);
|
||||
renderQuestions(s);
|
||||
renderInbox(s);
|
||||
renderInbox();
|
||||
renderApprovals(s);
|
||||
renderMetaInputs(s);
|
||||
restoreOpenDetails(openDetails);
|
||||
|
|
@ -994,10 +1000,17 @@
|
|||
sent: (ev, api) => renderMsg(ev, api, '→'),
|
||||
delivered: (ev, api) => renderMsg(ev, api, '✓'),
|
||||
},
|
||||
// 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 (which used to be the workaround for the
|
||||
// double-render bug).
|
||||
onAnyEvent: (ev /* , { fromHistory } */) => {
|
||||
if (inboxAppendFromEvent(ev)) renderInbox();
|
||||
},
|
||||
onLiveEvent: (ev) => {
|
||||
pulseBanner();
|
||||
if (ev.kind === 'sent' && ev.to === 'operator') {
|
||||
refreshState();
|
||||
NOTIF.show(
|
||||
'◆ ' + ev.from + ' → operator',
|
||||
String(ev.body || '').slice(0, 200),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue