initial commit of the paytime session frontend

This commit is contained in:
jenz
2026-08-22 14:53:27 +02:00
parent 4ad037bd13
commit 9e8e8378a2
43 changed files with 2936 additions and 0 deletions
@@ -0,0 +1,80 @@
# unloze playtime display
Next.js dashboard for the `unloze_playtimestats` database. Runs as a single
Node process; nginx reverse-proxies a subdomain to it.
## Stack
- Next.js 14 (App Router) + TypeScript
- Tailwind CSS
- Recharts for graphs
- `mysql2` connecting directly to MySQL (server-side only, in API routes —
the browser never sees DB credentials)
## Pages built so far
- `/` — Overview: concurrent-player population graph (last 24h by default)
and a map-to-map population bar chart (last 40 maps). Click any point on
either chart to see the actual players (avatar + name) who were online
then.
- `/players` — search players by current/previous name or SteamID, sortable
by recent activity, name, or highest playtime
- `/players/[steamid]` — avatar, previous names, SteamID, Steam profile link,
RaceTimer rank/level + profile link, total playtime, and a session-by-session
history (with the map(s) each session overlapped)
- `/maps` — search maps by name
- `/maps/[mapname]` — every time period that map was played
- `/maps/period/[mapId]` — one specific occurrence: players present during
it, votes cast (grouped by vote event, since a map could theoretically see
more than one), and prev/next map navigation
- `/countries` — every country with at least one player, sorted by player
count descending
- `/countries/[code]` — players from that country, sortable by highest
playtime or most recently played
A `favicon.ico` in `app/` is picked up automatically by Next — no config needed.
Every feature from the original planning conversation is now built.
## Deploying on the Hetzner box
```bash
npm install
npm run build
pm2 start ecosystem.config.js
pm2 save # persist across reboots
pm2 startup # follow the printed instructions once, to enable on boot
```
Then point nginx at it — see `nginx.example.conf` for a working server block
(adjust the domain and cert paths). After linking it into
`/etc/nginx/sites-enabled/`, reload nginx to pick it up.
## Notes
- `session_end_dt` is refreshed roughly every 60s by the plugin as a
heartbeat while a player is connected (not just on disconnect) — see
`lib/db.ts` usage in `/api/population`, which treats a `NULL` or
recent `session_end_dt` as "still active."
- Running on **Next.js 15**. One thing to remember when building the
`/players/[steamid]` and `/maps/[mapname]` dynamic pages next: Next 15
made the `params` (and `searchParams`) prop passed to pages/route handlers
a `Promise` you need to `await`, e.g.
`export default async function Page({ params }: { params: Promise<{ steamid: string }> }) { const { steamid } = await params; ... }`.
Doesn't affect anything currently built (no dynamic segments yet), but
will the moment those pages get added.
- SteamIDs are stored in Steam2 format (`STEAM_0:1:12345678`) to match the
existing `player_time` table. `lib/steam.ts` converts to Steam64 on the
fly for profile links/avatars — nothing needs to be stored pre-converted.
- **Timezone**: `ecosystem.config.js` pins `TZ=Europe/Berlin`, confirmed
against `SELECT @@global.time_zone, @@session.time_zone, NOW();` on the
actual server (MySQL runs `SYSTEM`, which resolves to German local time).
All the datetime parsing (`lib/dates.ts`, `/api/population`) reads MySQL
`DATETIME` strings with no timezone info attached, so Node's own local
timezone determines how they're interpreted — it must match MySQL's, or
"how many players are online right now" ends up quietly wrong (this was
a real bug, not hypothetical — it undercounted the live player count by
roughly half before this fix). If the DB server ever moves or MySQL's
timezone changes, update this value to match. After changing it,
`pm2 restart ecosystem.config.js --update-env` is required — a plain
`pm2 restart <name>` won't pick up the new env var.