Add vpinball: a browser Visual Pinball player

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
This commit is contained in:
2026-08-23 05:25:15 +02:00
co-authored by Claude Sonnet 5
commit e493a12e68
51 changed files with 5570 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# nginx only sees its own container-internal port (80) and has no idea
# docker-compose maps that to some other host port — an absolute redirect
# (e.g. nginx's own trailing-slash directory redirect) would silently
# drop that external port from the Location header. A relative Location
# lets the browser resolve it against whatever host:port it's already on.
absolute_redirect off;
# The stock /etc/nginx/mime.types (included by the base image's own
# nginx.conf) already maps .wasm/.woff2/.html correctly. .webmanifest
# isn't in it, so it's set per-location below rather than via a
# server-level types{} block, which would replace that whole map instead
# of extending it. .data has no meaningful type of its own and correctly
# falls through to the base config's default_type (application/octet-stream).
location = /manifest.webmanifest {
default_type application/manifest+json;
add_header Cache-Control "no-cache";
}
location = /sw.js {
add_header Cache-Control "no-cache";
}
# Large, effectively-immutable engine binaries — keep gzip off so nginx
# can still serve byte-range/If-Range requests (on-the-fly gzip disables
# range serving), and cache them aggressively.
location /vendor/vpinball-wasm/ {
gzip off;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# error_page preserves the 404 status while serving the prerendered
# not-found page's content — a bare `try_files ... /404.html;` fallback
# would serve that same content with a 200, since try_files treats an
# existing fallback file as a normal successful response.
error_page 404 /404.html;
location = /404.html {
internal;
}
location / {
try_files $uri $uri/ $uri.html =404;
}
}