# 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 ` won't pick up the new env var.