dashboard: approval_added / approval_resolved mutation events + client derived state
This commit is contained in:
parent
291f1fce42
commit
56d615b51f
6 changed files with 244 additions and 11 deletions
|
|
@ -634,7 +634,51 @@
|
|||
}
|
||||
|
||||
const APPROVAL_TAB_KEY = 'hyperhive:approvals:tab';
|
||||
function renderApprovals(s) {
|
||||
// Derived approval state — cold-loaded from /api/state, then mutated
|
||||
// live by `approval_added` / `approval_resolved` dashboard events.
|
||||
// `pending` is the open queue (newest-first); `history` is the last
|
||||
// 30 resolved rows.
|
||||
const APPROVAL_HISTORY_LIMIT = 30;
|
||||
const approvalsState = { pending: [], history: [] };
|
||||
function syncApprovalsFromSnapshot(s) {
|
||||
approvalsState.pending = (s.approvals || []).slice();
|
||||
approvalsState.history = (s.approval_history || []).slice();
|
||||
}
|
||||
function applyApprovalAdded(ev) {
|
||||
// Upsert by id so a snapshot that already included the row (cold
|
||||
// load + event lands at the same tick) doesn't double it.
|
||||
const existing = approvalsState.pending.findIndex((a) => a.id === ev.id);
|
||||
const row = {
|
||||
id: ev.id,
|
||||
agent: ev.agent,
|
||||
kind: ev.approval_kind,
|
||||
sha_short: ev.sha_short || null,
|
||||
diff: ev.diff || null,
|
||||
description: ev.description || null,
|
||||
};
|
||||
if (existing >= 0) approvalsState.pending[existing] = row;
|
||||
else approvalsState.pending.push(row);
|
||||
renderApprovals();
|
||||
}
|
||||
function applyApprovalResolved(ev) {
|
||||
// Drop from pending; prepend to history (newest-first), cap at 30.
|
||||
approvalsState.pending = approvalsState.pending.filter((a) => a.id !== ev.id);
|
||||
approvalsState.history.unshift({
|
||||
id: ev.id,
|
||||
agent: ev.agent,
|
||||
kind: ev.approval_kind,
|
||||
sha_short: ev.sha_short || null,
|
||||
status: ev.status,
|
||||
resolved_at: ev.resolved_at,
|
||||
note: ev.note || null,
|
||||
description: ev.description || null,
|
||||
});
|
||||
if (approvalsState.history.length > APPROVAL_HISTORY_LIMIT) {
|
||||
approvalsState.history.length = APPROVAL_HISTORY_LIMIT;
|
||||
}
|
||||
renderApprovals();
|
||||
}
|
||||
function renderApprovals() {
|
||||
const root = $('approvals-section');
|
||||
root.innerHTML = '';
|
||||
|
||||
|
|
@ -655,7 +699,8 @@
|
|||
);
|
||||
root.append(spawn);
|
||||
|
||||
const history = s.approval_history || [];
|
||||
const pending = approvalsState.pending;
|
||||
const history = approvalsState.history;
|
||||
const active = localStorage.getItem(APPROVAL_TAB_KEY) || 'pending';
|
||||
const tabs = el('div', { class: 'approval-tabs' });
|
||||
const pendingTab = el(
|
||||
|
|
@ -664,7 +709,7 @@
|
|||
type: 'button',
|
||||
class: 'approval-tab' + (active === 'pending' ? ' active' : ''),
|
||||
},
|
||||
`pending · ${s.approvals.length}`,
|
||||
`pending · ${pending.length}`,
|
||||
);
|
||||
const historyTab = el(
|
||||
'button',
|
||||
|
|
@ -676,11 +721,11 @@
|
|||
);
|
||||
pendingTab.addEventListener('click', () => {
|
||||
localStorage.setItem(APPROVAL_TAB_KEY, 'pending');
|
||||
renderApprovals(s);
|
||||
renderApprovals();
|
||||
});
|
||||
historyTab.addEventListener('click', () => {
|
||||
localStorage.setItem(APPROVAL_TAB_KEY, 'history');
|
||||
renderApprovals(s);
|
||||
renderApprovals();
|
||||
});
|
||||
tabs.append(pendingTab, historyTab);
|
||||
root.append(tabs);
|
||||
|
|
@ -690,12 +735,12 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (!s.approvals.length) {
|
||||
if (!pending.length) {
|
||||
root.append(el('p', { class: 'empty' }, 'queue empty'));
|
||||
return;
|
||||
}
|
||||
const ul = el('ul', { class: 'approvals' });
|
||||
for (const a of s.approvals) {
|
||||
for (const a of pending) {
|
||||
const li = el('li');
|
||||
const row = el('div', { class: 'row' });
|
||||
if (a.kind === 'apply_commit') {
|
||||
|
|
@ -954,7 +999,12 @@
|
|||
renderTombstones(s);
|
||||
renderQuestions(s);
|
||||
renderInbox();
|
||||
renderApprovals(s);
|
||||
// Sync the derived approvals store from the snapshot, then
|
||||
// render. Live `approval_added` / `approval_resolved` events
|
||||
// mutate the store directly and call renderApprovals() without
|
||||
// a snapshot refetch.
|
||||
syncApprovalsFromSnapshot(s);
|
||||
renderApprovals();
|
||||
renderMetaInputs(s);
|
||||
restoreOpenDetails(openDetails);
|
||||
notifyDeltas(s);
|
||||
|
|
@ -1014,6 +1064,11 @@
|
|||
renderers: {
|
||||
sent: (ev, api) => renderMsg(ev, api, '→'),
|
||||
delivered: (ev, api) => renderMsg(ev, api, '✓'),
|
||||
// Mutation events update derived state and trigger a
|
||||
// section re-render — no terminal log row (the terminal is
|
||||
// for broker traffic, not state-change chatter).
|
||||
approval_added: (ev) => { applyApprovalAdded(ev); },
|
||||
approval_resolved: (ev) => { applyApprovalResolved(ev); },
|
||||
},
|
||||
// Both history backfill and live frames flow through here, so the
|
||||
// inbox section ends up populated correctly on first paint and
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue