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

7 Commits

Author SHA1 Message Date
Ilia Denisov 2729fe9467 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
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.
2026-07-04 20:23:19 +02:00
Ilia Denisov 8499f388af 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
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).
2026-07-04 19:08:48 +02:00
Ilia Denisov c353c036ba 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
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.
2026-07-04 18:43:52 +02:00
Ilia Denisov de2f1432be 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
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.
2026-07-04 17:40:31 +02:00
Ilia Denisov 4a0c296bbb 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
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.
2026-07-04 17:03:49 +02:00
Ilia Denisov 6dfd5203d7 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
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).
2026-07-04 12:58:57 +02:00
Ilia Denisov 8c55b2d239 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
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.
2026-07-04 12:30:47 +02:00