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:
parent
a0fc7e1a9f
commit
c187366961
1 changed files with 632 additions and 632 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue