81 lines
3.8 KiB
Markdown
81 lines
3.8 KiB
Markdown
# 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.
|