Compare commits

..

View file

@ -41,28 +41,6 @@ window.marked = marked;
// Helpers ($, el, esc, form, fmtAgeSecs) moved to ./common.js (#406).
// #464 — atomic-swap render helper. Each managed section's render
// function used to do `root.innerHTML = ''; root.append(...);` in
// sequence; even though both operations sit in the same JS turn,
// operators could still see a "blink" on every poll cycle because
// (a) on async paths the await yield gave the browser a paint
// opportunity, and (b) complex builds with many `el()` allocations
// can blow the browser's per-task budget enough for layout to flash
// empty before the new children land.
//
// The fix: build the new content off-DOM into a `DocumentFragment`,
// then move it into the live root in a single `replaceChildren`
// call. The browser never sees an intermediate empty state. Builder
// callbacks receive the fragment as their `root` argument, so each
// renderer's existing `root.append(...)` code carries over with
// zero internal changes. Early-return inside the builder is fine —
// the commit still happens with whatever the builder appended.
function paintAtomic(liveRoot, build) {
const buf = document.createDocumentFragment();
build(buf);
liveRoot.replaceChildren(buf);
}
// Side panel singleton (Panel) moved to ./common.js (#406).
// Path linkification + file-preview side panel (openFilePanel,
@ -1849,28 +1827,26 @@ window.marked = marked;
// agents themselves and the operator already sees them next time
// they interact with the page.
async function refreshReminders() {
const liveRoot = $('reminders-section');
if (!liveRoot) return;
const root = $('reminders-section');
if (!root) return;
try {
const resp = await fetch('/api/reminders');
if (!resp.ok) {
paintAtomic(liveRoot, (root) => {
root.innerHTML = '';
root.append(el('p', { class: 'empty' }, 'reminders unavailable: http ' + resp.status));
});
return;
}
const rows = await resp.json();
renderReminders(rows);
} catch (err) {
paintAtomic(liveRoot, (root) => {
root.innerHTML = '';
root.append(el('p', { class: 'empty' }, 'reminders fetch failed: ' + err));
});
}
}
function renderReminders(rows) {
const liveRoot = $('reminders-section');
if (!liveRoot) return;
paintAtomic(liveRoot, (root) => {
const root = $('reminders-section');
if (!root) return;
root.innerHTML = '';
if (!rows.length) {
root.append(el('p', { class: 'empty' }, 'no queued reminders'));
return;
@ -1934,7 +1910,6 @@ window.marked = marked;
ul.append(li);
}
root.append(ul);
});
}
function fmtDuration(secs) {
if (secs < 60) return secs + 's';
@ -1964,16 +1939,14 @@ window.marked = marked;
try {
const resp = await fetch('/api/schedules');
if (!resp.ok) {
paintAtomic(listRoot, (root) => {
root.append(el('p', { class: 'empty' }, 'schedules unavailable: http ' + resp.status));
});
listRoot.innerHTML = '';
listRoot.append(el('p', { class: 'empty' }, 'schedules unavailable: http ' + resp.status));
return;
}
schedulesState = await resp.json();
} catch (err) {
paintAtomic(listRoot, (root) => {
root.append(el('p', { class: 'empty' }, 'schedules fetch failed: ' + err));
});
listRoot.innerHTML = '';
listRoot.append(el('p', { class: 'empty' }, 'schedules fetch failed: ' + err));
return;
}
renderScheduleNewForm();
@ -2090,15 +2063,13 @@ window.marked = marked;
}
function renderScheduleNewForm() {
const liveRoot = $('schedule-new-section');
if (!liveRoot) return;
const root = $('schedule-new-section');
if (!root) return;
// Preserve any field the operator was mid-typing in by reading
// current values before we re-render. Only `body` + `description`
// are big enough to feel — the others are toggle/datetime/number.
// Read from the live root BEFORE the swap so we see current
// values (the fragment we build into is fresh).
const carry = readScheduleFormCarry(liveRoot);
paintAtomic(liveRoot, (root) => {
const carry = readScheduleFormCarry(root);
root.innerHTML = '';
// Targets multi-select pulls from the live containers list. The
// backend accepts any string (forward-compat for richer recipients),
@ -2194,7 +2165,6 @@ window.marked = marked;
form_.append(actions);
root.append(form_);
}); // paintAtomic
}
function readScheduleFormCarry(root) {
return {
@ -2279,9 +2249,9 @@ window.marked = marked;
}
}
function renderSchedulesList() {
const liveRoot = $('schedules-section');
if (!liveRoot) return;
paintAtomic(liveRoot, (root) => {
const root = $('schedules-section');
if (!root) return;
root.innerHTML = '';
if (!schedulesState.length) {
root.append(el('p', { class: 'empty' }, 'no schedules queued'));
return;
@ -2296,7 +2266,6 @@ window.marked = marked;
});
for (const s of sorted) ul.append(renderScheduleCard(s));
root.append(ul);
});
}
function renderScheduleCard(s) {
const cancelled = !!s.cancelled_at_unix;