Skip to content

Say why the app did not start, instead of showing a dead one - #619

Merged
thcp merged 1 commit into
mainfrom
fix/issue-617-unresponsive-ui
Sep 12, 2026
Merged

thcp merged 1 commit into
mainfrom
fix/issue-617-unresponsive-ui

Conversation

@thcp

@thcp thcp commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Closes #618

The problem

main.js wires the UI in a synchronous run of fifteen wireX() calls (lines 207-243) and registers its own error and unhandledrejection handlers at lines 904-907, near the end of the module.

A throw anywhere in that run does two damaging things at once:

  1. Every listener after it never attaches, including the delegated click handler at line 873.
  2. 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 anywhere in the frontend, and CSP script-src 'self' forbids adding an inline one to the HTML, so there was no way to add it there.

configure_logging() attaches the file handler to the stemdeck logger only, so stemdeck.log never sees frontend problems at all. A reporter checking the obvious log finds nothing unusual, which is exactly what #617 describes.

The fix

static/js/boot-diagnostics.js, loaded before main.js, shows the failure in the page.

Three constraints shaped it, since it has to work in the circumstances where main.js does not:

  • It imports nothing. An import is a second thing that can fail, and it would fail in exactly the situation this exists to report.
  • It styles itself inline. It cannot assume a stylesheet loaded. CSP allows inline style and forbids inline script, which is also why this is a file rather than a <script> block.
  • It listens in the capture phase. A module that 404s fires its error on the element and never bubbles to window, so a normal listener would miss one of the two causes.

One deliberate i18n exception

Its strings are English and do not go through t(). This is the one place in the UI where that 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. The reasoning is recorded in the file so it does not read as an oversight.

One honest limit

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.

That is asserted in the tests rather than papered over. Which file is missing is answerable, just not from the page: it is a uvicorn access line in backend.log, which is why the banner points at /api/logs/backend instead of pretending to know.

Test plan

  • tests/e2e/boot-failure.spec.mjs, four cases: a module that fails to load, a throw during wiring, the detail being selectable and the banner dismissable, and a healthy page showing nothing
  • The first three fail without the fix. Verified by removing the script tag from index.html and re-running: 3 failed, 1 passed. The healthy-page case passes either way by design, since it guards the opposite regression
  • Full e2e suite: 152 passed
  • node --check on every static/js/*.js
  • tests/js/*.test.mjs all pass
  • i18n coverage clean
  • git diff --stat uv.lock empty, so the desktop in-app updater is unaffected
  • ruff check and ruff format --check clean

Not fixed here

This does not close #617. It makes that class of failure reportable; the reporter's specific trigger is still unknown, and I have asked them for backend.log to tell the two causes apart.

Pre-existing, unrelated

8 pytest failures in test_click_render.py and test_transpose_export.py on this machine. The identical 8 fail on clean main; they are ffmpeg/rubberband environment issues, not this change. No Python was touched.

How to try it by hand

Run the app, then in devtools:

await fetch('/js/catalog.js')  // fine

Or simulate the real thing: rename static/js/catalog.js, reload, and the banner should name the failure and point at the backend log. Rename it back afterwards.

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.
@thcp
thcp merged commit ddc0b43 into main Sep 12, 2026
10 checks passed
@thcp
thcp deleted the fix/issue-617-unresponsive-ui branch September 12, 2026 02:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A frontend boot failure is invisible: the app cannot report why nothing responds [Bug]: Nothing responds on boot

1 participant