CS1.6 Leaderboard¶
URL: https://cs16.heezy.info
k8s: heezy namespace, deployment cs16-leaderboard, ClusterIP :80 → container :3000
Source: heezy-containers/dockerfiles/cs16-leaderboard/
Manifests: heezy-k8s/apps/cs16-leaderboard/
Last Updated: 2026-08-14
What It Does¶
Lifetime and per-match statistics for the Counter-Strike 1.6 server in the DMZ
(dmz-cs16, 192.168.3.14). Node/Express, no game-server plugin involved.
The pipeline is entirely one-directional, which is what keeps it inside the DMZ rule:
dmz-cs16 game logs → promtail → Loki (lgtm.heezy.local:3100)
→ tailer.js polls Loki → parses → Postgres
→ app.js serves the leaderboard
Nothing reaches into the DMZ. The game server ships logs out; the leaderboard reads them from Loki.
One container runs both processes: CMD ["sh", "-c", "node tailer.js & node app.js"]. The tailer is
not a sidecar, so a tailer crash does not restart the web process and is only visible in the pod
logs.
Ingestion and dedupe¶
tailer.js polls Loki every 5 seconds (POLL_INTERVAL) for {container="cs16-server"} and parses
kill, suicide, bomb plant and bomb defuse lines.
Each Loki line carries a nanosecond timestamp. That value is stored as cs16_events.loki_ts with a
UNIQUE constraint, and the maximum seen is persisted in tailer_state. On restart the tailer
resumes from last_ts + 1 rather than re-scanning a fixed window, so a pod restart never
double-counts. On a first boot with no saved state it looks back INITIAL_LOOKBACK_SECONDS, default
7 days.
match_id is not something the game emits — it is computed from map name plus timestamp grouping.
Database¶
Tables live in the shared heezy database on big-boi, not a database of their own.
| Table | Holds |
|---|---|
cs16_events |
One row per parsed event; killer, victim, weapon, team, map, loki_ts |
cs16_player_stats |
Lifetime rollup keyed on steam_id |
cs16_matches |
One row per match: map, start and end time |
tailer_state |
Single row, last Loki nanosecond timestamp processed |
The loki_ts column and its unique constraint are added by an idempotent DO $$ block in
schema.sql, so an older table is migrated in place on startup.
API¶
| Route | Returns |
|---|---|
GET /api/leaderboard/lifetime |
Top 100 by kills, then K/D. Computes kd_ratio in SQL, treating zero deaths as kills |
GET /api/matches |
Match list |
GET /api/leaderboard/match/:matchId |
Per-match scoreboard, built as a UNION ALL of killer-side and victim-side aggregates |
GET /api/match/:matchId/kills |
Kill feed for one match |
GET /health |
Liveness and readiness probe target |
The lifetime board filters to players with at least one kill or death, so a player who only ever planted a bomb does not appear.
Access¶
Public. A Cloudflare tunnel ingress rule maps cs16.heezy.info to
cs16-leaderboard.heezy.svc.cluster.local:80, and the hostname is a Cloudflare Access application
with the standard pair of policies (home-IP bypass, then Google allow). On the LAN, dnsmasq points
the name at the SWAG VIP instead. See Cloudflare Access.
Configuration¶
| Env var | Value |
|---|---|
DB_HOST / DB_PORT / DB_NAME |
bigboi.heezy.local / 5432 / heezy |
DB_USER / DB_PASSWORD |
ExternalSecret postgres-credentials |
LOKI_HOST / LOKI_PORT |
lgtm.heezy.local / 3100 |
LOKI_QUERY |
{container="cs16-server"} |
POLL_INTERVAL |
5000 ms |
PORT |
3000 |
An empty leaderboard is usually a Loki label change
LOKI_QUERY is an exact label match. If the game server's promtail scrape config renames the
container label, the tailer keeps polling and finds nothing, with no error. Confirm the label
still exists in Loki before looking at the parser.
Related¶
- Cloudflare Access
- Monitoring — the LGTM stack the logs pass through