dashboard: dedent call.js moved blocks to module top level

Cosmetic follow-up to the Y3R C4LL extraction (argus nit on #1705): the
approvals + questions sections kept their original 2-space IIFE indentation
from tabs.js, which read as if they were inside a block and was inconsistent
with the unindented operator-inbox section + the other domain modules.
Uniform dedent — no logic change, build unaffected.
This commit is contained in:
iris 2026-06-16 09:56:02 +02:00 committed by mara
commit c187366961

View file

@ -99,19 +99,19 @@ export function operatorInboxAppendFromEvent(ev) {
}
// ─── approvals — the operator config-change / spawn approval queue ────────
const APPROVAL_TAB_KEY = 'hyperhive:approvals:tab';
// 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: [] };
export function activeApprovalCount() { return approvalsState.pending.length; }
export function syncApprovalsFromSnapshot(s) {
const APPROVAL_TAB_KEY = 'hyperhive:approvals:tab';
// 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: [] };
export function activeApprovalCount() { return approvalsState.pending.length; }
export function syncApprovalsFromSnapshot(s) {
approvalsState.pending = (s.approvals || []).slice();
approvalsState.history = (s.approval_history || []).slice();
}
export function applyApprovalAdded(ev) {
}
export 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);
@ -132,8 +132,8 @@ export function operatorInboxAppendFromEvent(ev) {
if (existing >= 0) approvalsState.pending[existing] = row;
else approvalsState.pending.push(row);
renderApprovals();
}
export function applyApprovalResolved(ev) {
}
export function applyApprovalResolved(ev) {
// Drop from pending; prepend to history (newest-first), cap at 30.
approvalsState.pending = approvalsState.pending.filter((a) => a.id !== ev.id);
// Idempotent: a snapshot re-sync (post-disconnect SSE catchup) can
@ -156,12 +156,12 @@ export function operatorInboxAppendFromEvent(ev) {
}
}
renderApprovals();
}
// Classify each unified-diff line by its leading char so
// `.diff-add` / `.diff-del` / `.diff-hunk` / `.diff-file` /
// `.diff-ctx` colour the output. Built as text-only spans (no
// innerHTML) so there's no HTML-escape surface.
function buildDiffPre(text) {
}
// Classify each unified-diff line by its leading char so
// `.diff-add` / `.diff-del` / `.diff-hunk` / `.diff-file` /
// `.diff-ctx` colour the output. Built as text-only spans (no
// innerHTML) so there's no HTML-escape surface.
function buildDiffPre(text) {
const pre = el('pre', { class: 'diff' });
for (const raw of String(text).split('\n')) {
let cls = 'diff-ctx';
@ -175,13 +175,13 @@ export function operatorInboxAppendFromEvent(ev) {
pre.appendChild(span);
}
return pre;
}
}
// Open an approval's diff in the side panel with a 3-way base
// toggle: vs applied (running tree), vs last-approved, vs previous
// proposal. `applied` uses the diff already shipped on the approval
// for instant paint; the other two fetch /api/approval-diff.
function openDiffPanel(a) {
// Open an approval's diff in the side panel with a 3-way base
// toggle: vs applied (running tree), vs last-approved, vs previous
// proposal. `applied` uses the diff already shipped on the approval
// for instant paint; the other two fetch /api/approval-diff.
function openDiffPanel(a) {
const bases = [
['applied', 'vs applied'],
['approved', 'vs last-approved'],
@ -217,9 +217,9 @@ export function operatorInboxAppendFromEvent(ev) {
const wrap = el('div', { class: 'diff-panel' }, tabs, host);
Panel.open('diff · ' + a.agent + ' #' + a.id, wrap);
selectBase('applied');
}
}
export function renderApprovals() {
export function renderApprovals() {
const root = $('approvals-section');
// #approvals-section only lives on /dashboard.html (Y3R C4LL tab);
// no-op elsewhere — `approval_added` / `approval_resolved` SSE
@ -375,9 +375,9 @@ export function operatorInboxAppendFromEvent(ev) {
ul.append(li);
}
root.append(ul);
}
}
function renderApprovalHistory(root, history) {
function renderApprovalHistory(root, history) {
if (!history.length) {
root.append(el('p', { class: 'empty' }, 'no resolved approvals yet'));
return;
@ -408,22 +408,22 @@ export function operatorInboxAppendFromEvent(ev) {
ul.append(li);
}
root.append(ul);
}
}
// ─── questions — the operator question queue (Y3R C4LL) ───────────────────
// questionsState + QUESTION_HISTORY_LIMIT are imported from state.js (shared
// with the SW4RM per-agent question-count badges).
// Keyed row cache: question id → {el, fingerprint}. Allows renderQuestions
// to reuse <li> elements whose state hasn't changed. The main benefit is
// preserving textarea draft text and radio/checkbox selections when an
// unrelated question arrives while the operator is composing a reply.
const questionRowCache = new Map();
export function activeQuestionCount() { return questionsState.pending.length; }
export function syncQuestionsFromSnapshot(s) {
// questionsState + QUESTION_HISTORY_LIMIT are imported from state.js (shared
// with the SW4RM per-agent question-count badges).
// Keyed row cache: question id → {el, fingerprint}. Allows renderQuestions
// to reuse <li> elements whose state hasn't changed. The main benefit is
// preserving textarea draft text and radio/checkbox selections when an
// unrelated question arrives while the operator is composing a reply.
const questionRowCache = new Map();
export function activeQuestionCount() { return questionsState.pending.length; }
export function syncQuestionsFromSnapshot(s) {
questionsState.pending = (s.questions || []).slice();
questionsState.history = (s.question_history || []).slice();
}
export function applyQuestionAdded(ev) {
}
export function applyQuestionAdded(ev) {
if (questionsState.pending.some((q) => q.id === ev.id)) return;
questionsState.pending.push({
id: ev.id,
@ -438,8 +438,8 @@ export function operatorInboxAppendFromEvent(ev) {
});
renderQuestions();
renderContainersFromState();
}
export function applyQuestionResolved(ev) {
}
export function applyQuestionResolved(ev) {
const idx = questionsState.pending.findIndex((q) => q.id === ev.id);
const existing = idx >= 0 ? questionsState.pending[idx] : null;
if (idx >= 0) questionsState.pending.splice(idx, 1);
@ -468,19 +468,19 @@ export function operatorInboxAppendFromEvent(ev) {
}
renderQuestions();
renderContainersFromState();
}
// Filter selection for the questions section. Persisted so the
// operator's preferred view (all / operator-targeted / peer)
// survives a reload.
const QUESTIONS_FILTER_KEY = 'hyperhive:questions:filter';
function getQuestionsFilter() {
}
// Filter selection for the questions section. Persisted so the
// operator's preferred view (all / operator-targeted / peer)
// survives a reload.
const QUESTIONS_FILTER_KEY = 'hyperhive:questions:filter';
function getQuestionsFilter() {
return localStorage.getItem(QUESTIONS_FILTER_KEY) || 'all';
}
function setQuestionsFilter(v) {
}
function setQuestionsFilter(v) {
localStorage.setItem(QUESTIONS_FILTER_KEY, v);
renderQuestions();
}
function questionMatchesFilter(q, filter) {
}
function questionMatchesFilter(q, filter) {
if (filter === 'all') return true;
if (filter === 'operator') return !q.target;
if (filter === 'peer') return !!q.target;
@ -490,24 +490,24 @@ export function operatorInboxAppendFromEvent(ev) {
return q.asker === name || q.target === name;
}
return true;
}
// Serialise the fields that determine a pending-question <li>'s DOM
// structure. Used by questionRowCache to skip rebuilds when nothing
// visible has changed. deadline_at controls whether the TTL chip node
// exists at all (its text is kept current by the global 1s ticker).
function questionRowFingerprint(q) {
}
// Serialise the fields that determine a pending-question <li>'s DOM
// structure. Used by questionRowCache to skip rebuilds when nothing
// visible has changed. deadline_at controls whether the TTL chip node
// exists at all (its text is kept current by the global 1s ticker).
function questionRowFingerprint(q) {
return JSON.stringify({
asker: q.asker, target: q.target, asked_at: q.asked_at,
deadline_at: q.deadline_at, question: q.question,
question_refs: q.question_refs, options: q.options, multi: q.multi,
});
}
}
// Build a single pending-question <li>. Extracted from renderQuestions so
// questionRowCache can reuse unchanged nodes without re-running this body.
// Event listeners attached here (keydown on textarea, submit on form) are
// preserved in the reused node — no re-attachment needed.
function buildQuestionLi(q) {
// Build a single pending-question <li>. Extracted from renderQuestions so
// questionRowCache can reuse unchanged nodes without re-running this body.
// Event listeners attached here (keydown on textarea, submit on form) are
// preserved in the reused node — no re-attachment needed.
function buildQuestionLi(q) {
const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19);
const targetLabel = q.target || 'operator';
const li = el('li', { class: 'question' + (q.target ? ' question-peer' : '') });
@ -612,9 +612,9 @@ export function operatorInboxAppendFromEvent(ev) {
);
li.append(cancelForm);
return li;
}
}
export function renderQuestions() {
export function renderQuestions() {
const root = $('questions-section');
// #questions-section only lives on /dashboard.html (Y3R C4LL tab);
// no-op when the section is missing. `question_added` /
@ -750,13 +750,13 @@ export function operatorInboxAppendFromEvent(ev) {
if (histEl) histEl.open = true;
}
restoreOpenDetails(openDetails);
}
}
// Format a remaining-seconds count as the `⏳ …` TTL chip text on a
// question card. Bucketed at minutes / hours so a long deadline stays
// readable; "expiring…" once the deadline has passed (the host-side
// ttl-watchdog will fire shortly).
function formatTtl(remaining) {
// Format a remaining-seconds count as the `⏳ …` TTL chip text on a
// question card. Bucketed at minutes / hours so a long deadline stays
// readable; "expiring…" once the deadline has passed (the host-side
// ttl-watchdog will fire shortly).
function formatTtl(remaining) {
if (remaining <= 0) return 'expiring…';
if (remaining < 60) return '⏳ ' + remaining + 's';
if (remaining < 3600) {
@ -765,18 +765,18 @@ export function operatorInboxAppendFromEvent(ev) {
}
return '⏳ ' + Math.floor(remaining / 3600) + 'h '
+ Math.floor((remaining % 3600) / 60) + 'm';
}
}
// Single page-wide ticker that refreshes every TTL chip in place
// each second. Renderers stamp `data-deadline` on the
// chip; this just updates `textContent`, no re-render of the
// questions section. No-op when no chips are on screen, so the
// cost is negligible.
setInterval(() => {
// Single page-wide ticker that refreshes every TTL chip in place
// each second. Renderers stamp `data-deadline` on the
// chip; this just updates `textContent`, no re-render of the
// questions section. No-op when no chips are on screen, so the
// cost is negligible.
setInterval(() => {
const now = Math.floor(Date.now() / 1000);
document.querySelectorAll('.q-ttl[data-deadline]').forEach((node) => {
const deadline = Number(node.getAttribute('data-deadline'));
if (!Number.isFinite(deadline)) return;
node.textContent = formatTtl(deadline - now);
});
}, 1000);
}, 1000);