4.0 KiB
4.0 KiB
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
mysql2connecting 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
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_dtis refreshed roughly every 60s by the plugin as a heartbeat while a player is connected (not just on disconnect) — seelib/db.tsusage in/api/population, which treats aNULLor recentsession_end_dtas "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 theparams(andsearchParams) prop passed to pages/route handlers aPromiseyou need toawait, 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 existingplayer_timetable.lib/steam.tsconverts to Steam64 on the fly for profile links/avatars — nothing needs to be stored pre-converted. - Timezone:
ecosystem.config.jspinsTZ=Europe/Berlin, confirmed againstSELECT @@global.time_zone, @@session.time_zone, NOW();on the actual server (MySQL runsSYSTEM, which resolves to German local time). All the datetime parsing (lib/dates.ts,/api/population) reads MySQLDATETIMEstrings 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-envis required — a plainpm2 restart <name>won't pick up the new env var.