From c53086b06d2a005a1d78b73652b409338277e91b Mon Sep 17 00:00:00 2001 From: Thales <> Date: Sat, 12 Sep 2026 02:47:18 +0100 Subject: [PATCH] Say why the app did not start, instead of showing a dead one main.js wires the UI in a synchronous run of fifteen wireX() calls and only registers its own error handlers at the end of the module. A throw anywhere in that run does two things at once: every listener after it never attaches, and the handlers that would have reported it never register either. What the user gets is an app that hovers and lets them type, because the engine does that itself, but whose buttons and file drop do nothing because nothing is listening. Nothing is written anywhere. There was no earlier error capture in the frontend, and CSP script-src 'self' forbids adding an inline one to the HTML. boot-diagnostics.js is loaded before main.js and shows the failure in the page. It follows three rules so it still works when main.js does not: it imports nothing, since an import is a second thing that can fail in exactly these circumstances; it styles itself inline, since it cannot assume a stylesheet loaded; and it listens in the capture phase, because a module that 404s fires its error on the element and never bubbles to window. Its strings are English and do not go through i18n. That is deliberate and is the one place in the UI where it is correct: i18n.js is itself a module that can fail to load, and a banner that renders nothing when translation is broken defeats the point of having it. One honest limit, asserted in the tests rather than papered over: when a dependency 404s, the banner names main.js and not the missing file. The module loader fetches dependencies itself, so no per-file error event exists to catch. Which file is missing is a uvicorn access line in backend.log, which is why the banner points at /api/logs/backend rather than pretending to know. Four browser tests cover a module that fails to load, a throw during wiring, that the detail is selectable and the banner dismissable, and that a healthy page shows nothing. The first three fail without the script tag; verified by removing it. Refs #618, and the investigation behind #617. --- static/index.html | 3 + static/js/boot-diagnostics.js | 130 ++++++++++++++++++++++++++++++++ tests/e2e/boot-failure.spec.mjs | 83 ++++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 static/js/boot-diagnostics.js create mode 100644 tests/e2e/boot-failure.spec.mjs diff --git a/static/index.html b/static/index.html index d7862198..5db0a9ab 100644 --- a/static/index.html +++ b/static/index.html @@ -1087,6 +1087,9 @@

Something failed

+ + diff --git a/static/js/boot-diagnostics.js b/static/js/boot-diagnostics.js new file mode 100644 index 00000000..8a3e885e --- /dev/null +++ b/static/js/boot-diagnostics.js @@ -0,0 +1,130 @@ +// Catch and show a failure that happens while the app is wiring itself up. +// +// main.js wires the UI in a long synchronous run of wireX() calls and only +// registers its own error handlers near the end of the module. A throw anywhere +// in that run therefore does two things at once: every listener after it never +// attaches, and the handlers that would have reported it never register either. +// What the user gets is an app that hovers and lets them type, because that is +// the engine's own behaviour, but whose buttons and file drop do nothing, +// because nothing is listening. Nothing is written anywhere (#618, found via +// #617). +// +// This module exists to be the thing that still works when that happens, so it +// follows three rules: +// +// 1. It is loaded BEFORE main.js in index.html. Module scripts execute in +// document order, so its handlers are installed before main.js is +// evaluated and are in place for anything main.js does, including a +// failure to parse or to resolve an import. +// 2. It imports nothing. An import is a second thing that can fail, and it +// would fail in exactly the circumstances this module needs to survive. +// 3. It styles itself inline. It cannot rely on a stylesheet having loaded, +// and CSP allows inline style (`style-src 'self' 'unsafe-inline'`) while +// forbidding inline script, which is also why this is a file rather than a +//