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