The service worker's engine cache key relied on a manual CACHE_VERSION
bump that was forgotten across the last two @valknar/vpinball-wasm
version bumps, so returning visitors kept getting served old cached
vpinball.js/.wasm/.data via cache-first with no revalidation — the origin
server itself was serving a correct, matching 0.3.4 set the whole time
(verified via headers/sizes against the local build).
Split CACHE_VERSION into a manually-bumped SHELL_VERSION and an
ENGINE_VERSION that copy-engine-assets.mjs now writes automatically from
the installed engine's package.json on every build, so the engine cache
always busts in lockstep with the dependency version — no step to forget.
0.3.4 skips SDL_SetWindowIcon on the Emscripten target, so the engine no
longer hijacks the browser tab's favicon (and title) via a blob: URL
when a table loads — verified live, no app-side workaround needed.
vpinball.js sets a table-supplied image as the page favicon via a blob:
URL as a table loads. Revert any blob: href on the icon <link> the
moment it appears, via a MutationObserver, instead of patching the
compiled engine.
Swaps the unicode glyphs (◀ ▶ ⬆) for hand-drawn stroke/fill SVG icons
(chevrons, an eject-style plunger glyph, a coin glyph, a play triangle),
and positions the coin/start/plunger cluster with absolute + translate
so it sits on the bar's true center regardless of the flipper buttons.
The engine's default attachTouchControls() overlay was generic floating
circles; this swaps in an app-themed bottom bar (left/right flipper,
plunger, insert coin, start game) anchored to the screen edges instead.
nginx serves /404.html as internal-only (see docker/nginx.conf) so it
preserves the real 404 status via error_page - any direct client fetch,
including the service worker's install-time addAll, gets a 403. Since
addAll is atomic, that one bad URL was failing precache of the entire
app shell.
Bump @valknar/vpinball-wasm to 0.3.3, which fixes a genuine infinite
busy-wait in the PinMAME plugin's Controller::Stop() under Emscripten
(no OS thread ever clears the flag it spins on) — this fired from
dispose()'s synchronous script Exit event on any ROM-based table,
hanging the tab permanently.
Sequence unmount cleanup as stop() then two animation frames then
dispose(), instead of dispose() alone, matching the consumer pattern
the engine fix expects (stop() only takes effect on its next internal
step, so disposing immediately after can still race it).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
instance.start() runs synchronously and blocks the main thread for a
noticeable moment on heavy WASM init, so pressing "Tap to Start" would
freeze on the old screen with no feedback until the HUD suddenly
appeared. Add a "starting" status between "ready" and "running", and
defer the actual start() call by a nested rAF so the browser gets a
chance to paint the spinner before the blocking call runs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Recolor a supplied ball-and-flippers glyph in the app's cabinet palette
(amber ball, chrome flippers) and place it on the ink rounded-square
background used elsewhere in the icon set. Regenerate every derived
asset (192/512/maskable PNGs, favicon.ico, app/icon.png) from it.
icon.svg/icon-source.svg now embed the composited PNG directly, since no
vector-trace tool was available to rebuild it as real vector paths.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the host-port-published nginx service with one that joins an
external Docker network and routes entirely through Traefik labels
(HTTP->HTTPS redirect, TLS router, security-headers middleware), matching
the pattern used by other Coolify-deployed projects. Add .env.example for
TRAEFIK_ENABLED/TRAEFIK_HOST/NETWORK_NAME and document the new deploy flow
and local-preview alternative in the README.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Shorten CTA labels to "Play Demo" / "Load Table"
- Fix mismatched corner radii on the hero and table-staging panels
- Add horizontal padding to the in-game "insert coin" hint on mobile
- Match the 404 page's "TILT" eyebrow label size to the rest of the site
- Only register the service worker in production — it precaches
/404.html, which doesn't exist as a static file under `next dev` and
was failing cache.addAll() on every load
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Bump @valknar/vpinball-wasm to 0.3.2 (PinMAME ROM support, MsgBox fix)
- Add optional PinMAME ROM .zip upload alongside .vpx tables
- Persist uploaded tables/ROMs in IndexedDB so users don't have to
re-upload on later visits, with a library list on the landing page
- Fix Insert Coin key mismatch with the engine's real coin-door gating
(now bound to 4, matching the shared table scripts) and surface it
in the controls legend and "Tap to Start" hint
- Make the landing page, staging panel, and in-game HUD responsive for
portrait phone widths; drop the landscape hint since portrait plays fine
- Replace the separate STATS HUD toggle with an FPS row in the info panel
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9JmzRBZenapVkFWjw9jmG
Next's fallback-metrics table has no entry for this (newer, merged
variable-font) family name, so the CLS-reducing fallback override it
tries to generate always fails with a build warning. Correctly
suppresses it under webpack; Turbopack (this project's default
builder) doesn't yet honor the flag the same way, so the warning
still prints there today, but the setting is still the semantically
correct declaration of intent and forward-compatible.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hQoM3jJT1Lx7CMTciMzvD
Next.js app that plays real .vpx tables via @valknar/vpinball-wasm
(WebAssembly Visual Pinball). Demo mode with the bundled default
table, drag-and-drop custom table upload, a cabinet-styled UI with a
coin-door HUD (fullscreen/stats/info/eject), a custom 404 page, PWA
support (hand-rolled service worker), and a static Docker Compose
deployment behind nginx.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hQoM3jJT1Lx7CMTciMzvD