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.
The bind-mount permission fix only takes effect after a rebuild, so a
stale image keeps failing with the opaque "unable to open database file".
This hardens both sides so any remaining failure is self-explanatory:
- docker-entrypoint.sh: fall back to chmod 777 when chown is unsupported
(network/9p mounts), and fall back to running as root when su-exec is
unavailable, so the data dir is always writable on any filesystem.
- server/db.mjs: wrap the DatabaseSync open in a try/catch and, on
failure, report the exact path, whether the directory is writable, and
the process UID instead of the bare SQLite error.
Verified with a real container: a normal bind mount serves /health and
creates trxtd.db; a read-only mount now prints the directory-permission
diagnostic instead of the raw SQLite error.