feat(pwa): installable app + fix research tree pip overflow

Research tree UI:
- The level pips shared the header row with the upgrade name and fought
  for width; long names like "Festungsmauern" (5 pips) pushed the pips
  out of the upgrade card. Pips now sit in the bottom row next to the
  buy button (space-between), where width is plentiful, and carry a
  "level / max" tooltip
- Tighter modal padding on phones (max-width 560px), scrollable at 92vh

PWA (installable as app in mobile browsers):
- public/manifest.webmanifest: standalone display, any orientation,
  TRXTD theme colors, German lang, start_url "/"
- public/sw.js: network-first service worker with runtime caching.
  index.html is always fetched fresh so new deploys are picked up
  immediately; the cache only serves as offline fallback. API calls
  and cross-origin requests are never intercepted
- Icon set generated by scripts/generate-icons.mjs (pure Node PNG
  encoder, zero image dependencies): gold tower on the game-themed
  dark gradient in 192px, 512px, maskable 512px (motif inside the safe
  zone) and 180px apple-touch-icon
- index.html: manifest link, favicon, apple-touch-icon, theme-color,
  mobile-web-app-capable, apple-mobile-web-app meta tags
- src/main.ts: service worker registration in production builds only
  (dev HMR stays untouched)
- server.mjs: serve .webmanifest as application/manifest+json (Chrome
  requires the correct MIME type for installability)

Verified against the production server: manifest (application/
manifest+json), icons (image/png) and sw.js (text/javascript) respond
with 200, index.html references all PWA tags; npm test 48/48 green.
This commit is contained in:
Tronax 2026-08-17 14:53:47 +02:00
parent 914af42210
commit 31cb594cb0
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
12 changed files with 296 additions and 13 deletions

47
public/sw.js Normal file
View file

@ -0,0 +1,47 @@
/**
* TRXTD service worker makes the game installable as an app (PWA) and
* keeps it playable offline.
*
* Strategy: network-first with runtime caching. The deployed build uses
* hashed asset names, so index.html must always come from the network to
* pick up new deploys; the cache is only a fallback when offline.
* API calls and WebSocket upgrades are never intercepted.
*/
const CACHE = 'trxtd-v1'
self.addEventListener('install', () => {
self.skipWaiting()
})
self.addEventListener('activate', (event) => {
event.waitUntil(
caches
.keys()
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
.then(() => self.clients.claim()),
)
})
self.addEventListener('fetch', (event) => {
const req = event.request
if (req.method !== 'GET') return
const url = new URL(req.url)
if (url.origin !== self.location.origin) return // CDN/external resources
if (url.pathname.startsWith('/api/')) return // auth & game API: network only
if (url.pathname.startsWith('/ws')) return
event.respondWith(
fetch(req)
.then((res) => {
if (res && res.status === 200 && res.type === 'basic') {
const copy = res.clone()
caches.open(CACHE).then((c) => c.put(req, copy)).catch(() => {})
}
return res
})
.catch(() =>
caches.match(req).then((hit) => hit || caches.match('/')),
),
)
})