chore(ui): temporary Android WebView boot diagnostic (test contour only) #176

Merged
developer merged 7 commits from feature/webview-boot-diagnostic into development 2026-07-04 20:36:05 +00:00
Owner

What

Temporary on-device boot diagnostic to chase the Android white-screen: the SPA loads a progress bar and then stays blank inside the Telegram/VK in-app WebView, while Firefox/Gecko on the same device renders it fine — pointing at a stale Android System WebView that cannot parse the es2022 bundle.

A classic ES5 <script> is injected into index.html before the module bundle, so it runs and paints even when the bundle fails to parse. The one index.html is served under /app/, /telegram/ and /vk/, so it covers all three entries at once.

It reports, on screen:

  • ENGINE — full userAgent, parsed Chromium major, wv WebView marker, viewport/dpr, cookies/online/lang.
  • JS SYNTAX?., ??, ??=, ||=, private fields, static blocks, object spread, numeric separators, dynamic import() — each parsed via new Function (never crashes the probe) and dated by the Chrome version that shipped it, so the first NO dates the engine.
  • JS / WEB APISstructuredClone, Array.at/findLast, Object.hasOwn, Proxy (Svelte 5 runes), ResizeObserver, Intl.Segmenter, localStorage, … same dating.
  • VERDICT — flags the likely cause (missing ES2020 ?./?? ⇒ the bundle cannot parse).
  • SPA MOUNT CHECK — at DOMContentLoaded + t+3s + t+8s: <html>.app-shell (did main.ts run?) and #app child count (did Svelte mount?), so it distinguishes module never ran (parse fail) / ran but Svelte crashed / all good.
  • ERRORSwindow.onerror, capture-phase error (module SyntaxError / load failure), unhandledrejection.

A Copy report button exports everything; Hide / continue reveals the app that booted underneath.

Not for production

vite.config.ts strip-boot-diag removes the whole BOOT-DIAG block from every non-production build, so the mock e2e (a full-screen overlay would intercept its taps) and the dev server stay clear — only the production build shipped to the test contour carries it. Temporary: the block + the plugin get reverted after the diagnosis; must never reach master.

Verified locally

  • pnpm check — 0 errors.
  • pnpm build (production) — the panel is in dist/index.html; rendered it via headless Chromium (all probes OK, mount check reads SPA mounted).
  • pnpm build --mode mock — the panel is absent (gate works).
  • pnpm test:e2e188 passed (Chromium + WebKit); the overlay does not affect the smoke.
## What Temporary on-device **boot diagnostic** to chase the Android white-screen: the SPA loads a progress bar and then stays blank inside the Telegram/VK in-app WebView, while **Firefox/Gecko on the same device renders it fine** — pointing at a stale Android System WebView that cannot parse the `es2022` bundle. A classic ES5 `<script>` is injected into `index.html` **before** the module bundle, so it runs and paints even when the bundle fails to parse. The one `index.html` is served under `/app/`, `/telegram/` and `/vk/`, so it covers all three entries at once. It reports, on screen: - **ENGINE** — full `userAgent`, parsed Chromium major, `wv` WebView marker, viewport/dpr, cookies/online/lang. - **JS SYNTAX** — `?.`, `??`, `??=`, `||=`, private fields, static blocks, object spread, numeric separators, dynamic `import()` — each parsed via `new Function` (never crashes the probe) and dated by the Chrome version that shipped it, so the first `NO` dates the engine. - **JS / WEB APIS** — `structuredClone`, `Array.at`/`findLast`, `Object.hasOwn`, `Proxy` (Svelte 5 runes), `ResizeObserver`, `Intl.Segmenter`, `localStorage`, … same dating. - **VERDICT** — flags the likely cause (missing ES2020 `?.`/`??` ⇒ the bundle cannot parse). - **SPA MOUNT CHECK** — at DOMContentLoaded + t+3s + t+8s: `<html>.app-shell` (did `main.ts` run?) and `#app` child count (did Svelte mount?), so it distinguishes *module never ran (parse fail)* / *ran but Svelte crashed* / *all good*. - **ERRORS** — `window.onerror`, capture-phase `error` (module SyntaxError / load failure), `unhandledrejection`. A **Copy report** button exports everything; **Hide / continue** reveals the app that booted underneath. ## Not for production `vite.config.ts` `strip-boot-diag` removes the whole `BOOT-DIAG` block from every **non-production** build, so the mock e2e (a full-screen overlay would intercept its taps) and the dev server stay clear — only the production build shipped to the **test contour** carries it. **Temporary**: the block + the plugin get reverted after the diagnosis; must never reach `master`. ## Verified locally - `pnpm check` — 0 errors. - `pnpm build` (production) — the panel is in `dist/index.html`; rendered it via headless Chromium (all probes OK, mount check reads *SPA mounted*). - `pnpm build --mode mock` — the panel is **absent** (gate works). - `pnpm test:e2e` — **188 passed** (Chromium + WebKit); the overlay does not affect the smoke.
developer added 1 commit 2026-07-04 10:31:21 +00:00
chore(ui): temporary on-device boot diagnostic for the Android WebView white screen
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m4s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m48s
8c55b2d239
Add a classic ES5 <script> to index.html, before the ES2022 module bundle, that
renders an on-screen diagnostic overlay. It runs and paints even when the bundle
fails to parse on an old Android System WebView (the Telegram/VK in-app case we are
chasing: Firefox/Gecko renders the SPA on the same device, the in-app WebView shows
only a white screen).

The overlay installs error capture first (so a module SyntaxError / load failure /
unhandled rejection is printed), then reports the engine (userAgent + Chromium
version), the JS syntax and Web API support the es2022 bundle needs — each row dated
by the Chrome version that shipped it, so the first failing row dates the engine —
and whether the module ran (<html> gets .app-shell) and Svelte mounted (#app has
children). A verdict flags the likely cause; a Copy button exports the report.

vite.config.ts strip-boot-diag removes the whole BOOT-DIAG block from every
non-production build, so the mock e2e (whose first taps a full-screen overlay would
intercept) and the dev server stay clear; only the production build shipped to the
test contour carries it.

Temporary: revert this commit (the index.html block and the plugin) after the
diagnosis. It must never reach master / production.
developer added 1 commit 2026-07-04 10:58:58 +00:00
fix(ui): down-level build target to es2019 for old Android WebViews
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m34s
6dfd5203d7
The es2022 build target shipped optional chaining / nullish coalescing verbatim
(esbuild does not down-level syntax below the configured target), so an old Android
System WebView — captured as Chromium 66 in the Telegram/VK in-app WebView on Android
9, via the boot diagnostic — rejected the bundle with "Uncaught SyntaxError:
Unexpected token ?" at main-*.js:2, the module never ran (no .app-shell, #app empty)
and the SPA showed a white screen. Firefox/Gecko on the same device, which ships its
own current engine, rendered the SPA fine.

Lower build.target to es2019 so esbuild down-levels the es2020+ syntax the old engine
cannot parse (?., ??, private fields, static blocks, numeric separators). Verified the
emitted main bundle no longer contains ?./?? and the mock e2e (Chromium + WebKit)
still passes. The boot-diagnostic JS-SYNTAX header is relabelled accordingly.

esbuild lowers syntax only, not runtime APIs; the app's own production source uses
none beyond this floor (mock-only helpers are tree-shaken). A graceful "update your
WebView" fallback and any runtime-API polyfills follow once the on-device re-test
confirms the parse fix (and @vitejs/plugin-legacy stays in reserve for even older
engines).
developer added 1 commit 2026-07-04 15:03:49 +00:00
fix(ui): conditionally load core-js polyfills for old Android WebViews
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
4a0c296bbb
The es2019 down-level fixed the parse error, but the Chromium 66 in-app WebView
(Android 9) then threw "Uncaught ReferenceError: globalThis is not defined" — esbuild
lowers syntax, not the es2020+ runtime globals the bundle and its deps reference
(globalThis, structuredClone, Array.at, Object.hasOwn, Promise.allSettled, …), so the
module still failed to evaluate and the SPA stayed a white screen.

Load core-js only where it is actually missing. A permanent ES5 gate in index.html
feature-detects the engine and, on an old one, document.writes a parser-blocking
<script src=polyfills.js> before the deferred module runs; a modern engine matches
none of the checks and downloads nothing. document.write is used deliberately — it is
the only way to inject a script guaranteed to run before a deferred module — with a
static literal argument (no interpolation, no injection surface). vite.config
emitPolyfills writes dist/polyfills.js from the prebuilt core-js-bundle and keeps it
out of the module graph, so the bundle-size budget is unaffected (app entry still
108.8 / 110 KB gzip).

pnpm-workspace denies core-js-bundle's install script (the package ships a prebuilt
minified.js we only read at build time; its script is a funding banner). svelte-check
and the mock e2e (Chromium + WebKit) are green and the size gate still passes. The
on-device re-test on the Chromium 66 emulator is pending.
developer added 1 commit 2026-07-04 15:40:33 +00:00
chore(ui): surface caught boot errors on the diagnostic panel
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m57s
de2f1432be
The es2019 + conditional core-js fixes let the module mount on the Chromium 66 in-app
WebView, but the lobby then shows a white screen and a generic "something went wrong"
toast — a caught error that never reaches window.onerror, so the diagnostic ERRORS
panel stayed empty and could not name the cause.

Mirror console.error / console.warn into the panel, and add a temporary, mock-gated
console.error of the raw error (with stack) in handleError, so the on-device panel
shows exactly what threw and whether it is one error or several. Expected: a
ReferenceError from the 64-bit FlatBuffers timestamp decode (BigInt, absent on Chrome
66 and not polyfillable by core-js) — to be confirmed on-device before deciding the
support floor.

Temporary — both hunks revert together with the index.html BOOT-DIAG block.
developer added 1 commit 2026-07-04 16:43:54 +00:00
fix(ui): draw board tile glyphs on Chrome < 105 (container-query-unit fallback)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m51s
c353c036ba
The board sizes its tile letter/value glyphs (and the blank/bonus marks) in container-
query units (cqw), which are Chrome 105+. On an older engine — e.g. the Chrome 74
Android 10 System WebView — the cqw declaration is invalid and dropped, so the glyph
inherits the .cell `font-size: 0` reset and renders at 0px: the board tiles show empty
while the rack and stats tiles (not under that reset) render fine.

Add a viewport-relative fallback before each cqw font-size — calc(<n>vmin * var(--z, 1))
— which approximates the board-relative size and tracks the zoom (--z), so old WebViews
draw the glyphs; Chrome 105+ still takes the exact cqw line after it. Additive only, no
change on modern engines (verified: mock e2e incl. the board/zoom specs still green).

The landscape board-container sizing (.scaler.land min(100cqw,100cqh)) still relies on
container-query units and is not covered here — portrait (the mobile default under test)
is unaffected by that.
developer added 1 commit 2026-07-04 17:08:49 +00:00
fix(ui): pan the zoomed board via window listeners, not pointer capture
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m47s
8499f388af
The mouse drag-to-pan of the zoomed board captured the pointer on the scroll container
(vp.setPointerCapture) and wrote that same element's scrollLeft/Top. On an older
Chromium — the Chrome 74 Android 10 System WebView — capturing the pointer on the very
element being scrolled drops or displaces the capture as the scroll offset changes
mid-drag, so the board juddered under the cursor and would not pan (modern engines
cope).

Track the drag with window pointermove/up/cancel listeners, added on pointerdown and
removed on release, instead of capturing — the same "keep receiving moves past the
element edge" without holding the scroll container. The path is mouse-only (touch
scrolls the overflow viewport natively) and its behaviour on modern engines is
unchanged; svelte-check and the mock e2e (incl. the zoom specs) are green.

Speculative, not locally reproducible (needs Chrome 74): pending the owner's on-device
confirmation. The unrelated auto-zoom-on-placement gap on the same engine is left as-is
(minor).
developer added 1 commit 2026-07-04 18:23:22 +00:00
feat(ui): unsupported-engine boot screen; drop the temporary diagnostic
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m6s
CI / conformance (pull_request) Successful in 8s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m55s
2729fe9467
Replace the temporary on-device boot diagnostic (the always-on overlay, the
console.error mirror and the handleError console hook) with a permanent boot-capability
guard in index.html. Before the deferred module, in plain ES5, it:
  - hard-gates on the unpolyfillable essentials the app cannot run without — BigInt (the
    64-bit FlatBuffers wire decode) and Proxy (Svelte 5 runes) — and, when one is
    missing, shows a friendly full-screen "this device's OS/browser can't run the app"
    screen instead of a white screen;
  - soft-gates the polyfillable es2020+ globals, pulling core-js (polyfills.js) only
    when needed (the previous behaviour, folded in);
  - keeps a reactive net: an uncaught error during boot with no window.__booted signal
    within 8s raises the same screen with the captured cause.
Inside a Telegram/VK Mini App the screen also points to the web version
(https://<host>/app/). A "Diagnostic information" button reveals the engine, the
feature table, the reason and the app version, with a Copy button.

App.svelte sets window.__booted once bootstrap resolves. vite.config drops the
now-unneeded strip-boot-diag plugin and adds inject-boot-version (stamps the guard's
diagnostic with VITE_APP_VERSION). The es2019 target, the core-js loader and the board
cqw->vmin glyph fallback stay. The speculative Chrome-74 pan change is reverted — it did
not fix the judder and added nothing on modern engines.

Verified: svelte-check clean, unit + mock e2e green (the guard is inert on a capable
engine, so the size budget and the smoke are untouched), and the screen + diagnostic +
stamped version render on a forced hard-gate (BigInt removed via addInitScript).

The unsupported-engine telemetry beacon (dedup + edge rate-limit + a Prometheus counter)
is a separate follow-up PR, per the agreed split.
owner approved these changes 2026-07-04 20:32:56 +00:00
developer merged commit 4dfedd02a3 into development 2026-07-04 20:36:05 +00:00
developer deleted branch feature/webview-boot-diagnostic 2026-07-04 20:36:06 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: developer/scrabble-game#176