dashboard: preserve <details open> across refresh via data-restore-key

generalises the focus-preservation pattern to expanded details
sections (journald viewer was collapsing on every 5s refresh; same
issue for approval diff blocks). before re-render we snapshot
which <details data-restore-key=...> are open; after render we
re-apply. setting .open = true programmatically also fires the
toggle event, so journald's lazy-fetch listener re-runs cleanly.

tagged: journal:<container>, approval-diff:<id>. anything else
that should survive a refresh just needs a stable data-restore-key
attribute.
This commit is contained in:
müde 2026-05-15 21:37:17 +02:00
parent fd0e493bf5
commit b374f39b0d

View file

@ -300,7 +300,10 @@
// operator expands; refresh re-fetches; unit toggle switches
// between the harness service and the full machine journal.
function buildJournalDetails(containerName, defaultUnit) {
const details = el('details', { class: 'journal' });
const details = el('details', {
class: 'journal',
'data-restore-key': 'journal:' + containerName,
});
const summary = el('summary', {}, '↳ logs · ' + containerName);
const body = el('div', { class: 'journal-body' });
const controls = el('div', { class: 'journal-controls' });
@ -576,7 +579,9 @@
);
li.append(row);
if (a.diff_html) {
const details = el('details');
const details = el('details', {
'data-restore-key': 'approval-diff:' + a.id,
});
details.append(el('summary', {}, 'diff vs applied'));
// diff_html is pre-rendered server-side (per-line class spans inside
// a <pre>); inject as innerHTML.
@ -601,6 +606,34 @@
'inbox-section',
'approvals-section',
];
// <details> sections that should survive a refresh need a stable
// `data-restore-key` attribute. snapshotOpenDetails walks managed
// sections and records which keys are currently open; restoreOpenDetails
// re-applies after the render. The `toggle` event fires on
// programmatic open changes too, so any onload-fetch wired up via
// a toggle listener (e.g. journald) re-fires cleanly.
function snapshotOpenDetails() {
const open = new Set();
for (const id of MANAGED_SECTION_IDS) {
const sect = document.getElementById(id);
if (!sect) continue;
for (const d of sect.querySelectorAll('details[data-restore-key]')) {
if (d.open) open.add(d.dataset.restoreKey);
}
}
return open;
}
function restoreOpenDetails(open) {
if (!open.size) return;
for (const id of MANAGED_SECTION_IDS) {
const sect = document.getElementById(id);
if (!sect) continue;
for (const d of sect.querySelectorAll('details[data-restore-key]')) {
if (open.has(d.dataset.restoreKey)) d.open = true;
}
}
}
function operatorIsTyping() {
const el_ = document.activeElement;
if (!el_ || el_ === document.body) return false;
@ -624,11 +657,13 @@
const resp = await fetch('/api/state');
if (!resp.ok) throw new Error('http ' + resp.status);
const s = await resp.json();
const openDetails = snapshotOpenDetails();
renderContainers(s);
renderTombstones(s);
renderQuestions(s);
renderInbox(s);
renderApprovals(s);
restoreOpenDetails(openDetails);
notifyDeltas(s);
// Auto-refresh: fast (2s) while a spawn or a per-container
// action is in flight, otherwise heartbeat (5s) so newly-queued