fix(page): hung-load timeout surfaces error overlay (no more blank-white-page-forever)
User-reported regression with http://www.metikmusic.com/ — a server that
accepts the connection but never sends a response. Before this fix the
page-host's loading-timeout safety net silently force-cleared the
spinner after 30s and left the user staring at a blank white page with
no actionable feedback.
Root cause: `loadingLifecycle.startLoading()` in app/page/page.js had a
30s setTimeout whose only action was `this.stopLoading()`. did-fail-load
never fired (the server keeps the socket open) so the existing
`showLoadErrorOverlay` path was never taken.
Fix: the timeout callback now also calls `showLoadErrorOverlay` with
the in-flight URL, code 'TIMEOUT', and a "page took too long to load"
description. Same overlay shape as did-fail-load — Retry + Close
buttons, Peek-branded card.
URL attribution: the timeout reads `latestNavigationUrl` first
(updated by did-start-navigation, holds the URL we're trying to load),
falling back to `webview.getURL()` (which returns the previously-
displayed URL during a pending navigation — using it would attribute
the timeout to the wrong page) and finally the initial `targetUrl`.
The first-pass implementation got this wrong and the test caught it.
Test hook: `LOADING_TIMEOUT_MS` is now `getLoadingTimeoutMs()`, a
function that consults `window.__loadingTimeoutMs` first. Playwright
overrides to 1.5s so the test fires in <5s instead of >30s. Production
runs always use the 30s default (the override is unset).
Tests:
* `tests/desktop/page-load-failure.spec.ts` "Hung load: …" (NEW) —
test server gains a /hang endpoint that accepts the connection but
never responds; the spec opens page-host on /ok, waits for that to
finish, sets the timeout override to 1.5s, navigates to /hang,
asserts the overlay appears with the /hang URL + "took too long"
reason + Retry button, and the webview is no longer in 'loading'
state (the original "endless glow border" symptom).
* Suite total: 6/6 (was 5/5).
Tasks doc updated; bug entry marked done with the actual repro path
documented for future archaeology.