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 ────────
|
// ─── 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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue