Skip to content

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.