dashboard: guard renderers against missing-root on /flow.html (#399)

Mara hit:

  TypeError: can't access property "innerHTML", root is null
      renderContainers app.js:666
      applyContainerStateChanged app.js:484
      container_state_changed app.js:2306

…on /flow.html. The same bundled `app.js` runs on both /index.html
(dashboard, has the tab panes) and /flow.html (flow page, has only
the broker terminal). SSE events arrive on every page —
`container_state_changed` / `tombstones_changed` / `approval_*` /
`question_*` route through their corresponding renderers, which
then `root.innerHTML = ''` on `$('section-id')` and crash when the
section isn't in the DOM.

The convention is already "no-op when the target DOM doesn't
exist" — `renderMetaInputs`, `renderRebuildQueue`, `renderReminders`
all guard with `if (!root) return;` at the top. Bring the rest in
line:

- `renderContainers`
- `renderTombstones`
- `renderQuestions`
- `renderApprovals`

`renderInbox` already handles the absent case via its
`if (root && !root.hidden)` branch — no change.

No behaviour change on /index.html. On /flow.html the failing
events silently no-op as intended (terminal renderers still
re-render the broker tail normally).
This commit is contained in:
iris 2026-05-25 01:22:11 +02:00 committed by Mara
parent 92becdd951
commit 4559a56e2e

View file

@ -704,6 +704,15 @@ window.marked = marked;
function renderContainers(s) {
const root = $('containers-section');
// #containers-section only exists on /index.html. The same
// bundled app.js runs on /flow.html (which has the broker
// terminal but no container list) and on any future pages —
// `container_state_changed` SSE events arrive on every page and
// route through `applyContainerStateChanged → renderContainersFromState`,
// so without this guard the renderer throws `root is null` on
// every event (#399). Matches the no-op-when-target-absent
// convention the other renderers (renderTombstones, etc.) follow.
if (!root) return;
root.innerHTML = '';
// Containers come from the derived map (event-driven) rather than
@ -1042,6 +1051,10 @@ window.marked = marked;
function renderTombstones(s) {
const root = $('tombstones-section');
// #tombstones-section only lives on /index.html (SYST3M tab);
// no-op on /flow.html and any other page that loads the shared
// bundle without the dashboard's tab panes (#399).
if (!root) return;
root.innerHTML = '';
if (!s.tombstones || !s.tombstones.length) {
root.append(el('p', { class: 'empty' }, 'no kept state — clean'));
@ -1176,6 +1189,11 @@ window.marked = marked;
}
function renderQuestions() {
const root = $('questions-section');
// #questions-section only lives on /index.html (Y3R C4LL tab);
// no-op on /flow.html etc. — the bundled app.js runs everywhere
// and `question_added` / `question_resolved` SSE events route
// through here (#399).
if (!root) return;
root.innerHTML = '';
const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19);
const allPending = questionsState.pending;
@ -1563,6 +1581,11 @@ window.marked = marked;
function renderApprovals() {
const root = $('approvals-section');
// #approvals-section only lives on /index.html (Y3R C4LL tab);
// no-op elsewhere — `approval_added` / `approval_resolved` SSE
// events route through here on every page that loads the bundle
// (#399).
if (!root) return;
root.innerHTML = '';
// Spawn request form: submitting it queues a Spawn approval that