93 lines
4.1 KiB
Markdown
93 lines
4.1 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 (hand-rolled SVG, not
|
||
a charting library — scroll to zoom, drag to pan all the way back to
|
||
2015-07-16, click any point for who was online then), a map-to-map
|
||
population bar chart (same zoom/pan, pool of 150 maps), and a "recurring
|
||
window" chart for questions like "who's online 6–15h every Tuesday"
|
||
(pick days of week + time window + date range)
|
||
- `/players` — search players by current/previous name or SteamID (shows
|
||
which old name matched, if any), 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 at any
|
||
point 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.
|
||
|
||
## Redeploying
|
||
|
||
```
|
||
rm -rf node_modules .next
|
||
npm install
|
||
npm run build
|
||
pm2 restart unloze-playtime-display
|
||
```
|
||
|
||
## 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.
|