Solo games run entirely in the browser, so the server previously had no
idea who was playing right now (it only saw logins and finished games).
This adds lightweight presence reporting and an admin dashboard.
Presence (server):
- POST /api/presence (logged-in users): heartbeat while a solo game runs,
stores username, map (validated), difficulty, wave, since/lastSeen per
user; entries expire automatically after 90s without a heartbeat
- POST /api/presence/stop: explicit removal when the player exits
Admin API (ADMIN_USERS env, comma-separated usernames):
- GET /api/admin/overview: live solo players, multiplayer room summaries
(code, mode, map, players — the server already tracks rooms), and
global stats (accounts, rounds, crystals in circulation)
- GET /api/admin/users?limit=100: user list with stats, newest login first
- publicUser now carries an admin flag; non-admins get 403
Admin UI (src/components/AdminDashboard.vue, served at /admin):
- Login gate for guests/non-admins (guest profiles are detected via
isLoggedIn, not just user presence)
- KPI cards, live solo table (player, map, difficulty, wave, duration),
room table, and account table; auto-refresh every 5 seconds
- App.vue renders the dashboard for /admin instead of the game and runs
a screen watcher that starts/stops the solo presence heartbeat
Config: ADMIN_USERS documented in docker-compose.yml and README.
Tests: 6 new integration checks (admin flag, presence report/stop,
403 guard, overview contents, user list) — 37/37 green, build clean.
Verified end-to-end in the browser: guest gate, admin login, and a live
second player (map/difficulty/wave) appearing in the dashboard.
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.
- Add CSP, frame and referrer headers for served static files
- Enforce WebSocket origin check to prevent cross-site hijacking
- Trust X-Forwarded-For only when the peer is from a private proxy network
- Limit concurrent connections (500 total / 20 per IP) and rooms (300)
- Add ALLOWED_ORIGINS env var for additional WebSocket origins
- Document reverse proxy setup (NPM/NPMplus) in README
- Add scripts/security-test.mjs to verify origin and limit behavior