leaderboard/README.md
2026-08-07 17:20:22 +02:00

5.8 KiB
Raw Blame History

Dose Leaderboard

A tiny self-hosted leaderboard for tracking wins ("doses") in a card game played with friends. The UI is in French; this README is in English for whoever's maintaining/deploying it.

  • Anyone can view the standings.
  • A chart shows each player's doses per day (not cumulative — it goes up and down with how many wins landed on each day).
  • Only the admin (a single shared password) can add/remove players, and add/remove doses — for any date, not just today.

No accounts, no build step, one Docker container, data stored in a local SQLite file.

Quick start (Docker)

Requires Docker and Docker Compose (bundled with Docker Desktop, or docker-compose-plugin on Linux).

git clone <this repo> dose-leaderboard
cd dose-leaderboard
cp .env.example .env
# edit .env and set ADMIN_PASSWORD to something only you and maybe a co-admin know

sudo docker compose up -d --build

Open http://<your-server>:3000 (or whatever PORT you set in .env).

Doses and players are stored in ./data/leaderboard.db, which is bind-mounted into the container — the data survives rebuilds, restarts, and docker compose down. Back it up by copying that one file. (The container runs as root, so that directory ends up root-owned on the host — that's fine, only the container needs to write to it.)

To update after pulling new code:

sudo docker compose up -d --build

To stop it:

sudo docker compose down

Using it (Classement des doses)

  • Standings ("Classement"): everyone can see the list of players ranked by total doses, and the "Total des doses" stat row under the chart.
  • Chart ("Doses par jour"): one line per player, one point per day that has at least one dose (from anyone). Y-axis is that day's count — expect it to go up and down, not just climb.
  • Admin login: click "Connexion admin" top-right and enter the password from ADMIN_PASSWORD. The browser remembers it (in localStorage) until you click "Déconnexion" or clear site data.
  • Add / remove a player: admin-only. Logged in, an "Ajouter un joueur" form appears at the bottom, and a button appears next to each player in the standings (removing a player deletes all of their doses too — it asks for confirmation first).
  • Add / remove a dose for a specific date: admin-only. A date picker ("Modifier les doses du :") appears above the standings, defaulting to today. Pick any date (not in the future), then use each player's +/ buttons to add or remove a dose on that date. is disabled once that date's count for a player reaches 0.

Environment variables

Variable Required Default Meaning
ADMIN_PASSWORD yes Shared password for admin actions. The server refuses to start without it.
PORT no 3000 Host port Docker Compose publishes the app on. The container always listens on 3000 internally.
TZ no UTC IANA timezone (e.g. Europe/Paris) used to compute "today" (the default date picker value and the cap on how far in the future a dose date may be).

Running locally without Docker

Requires Node.js 22.5+ (uses the built-in node:sqlite module — no native dependencies to compile).

npm install
ADMIN_PASSWORD=changeme npm run dev   # auto-restarts on file changes

Then open http://localhost:3000. Data is stored in ./data/leaderboard.db by default (override with DATA_DIR).

API reference

All routes are under /api. Admin-only routes require an x-admin-key header matching ADMIN_PASSWORD.

Method & path Auth Body / query Notes
GET /players?date=YYYY-MM-DD List of players with total doses and dateCount (doses on date, defaults to today).
POST /players admin { name } Create a player. 409 if the name already exists (case-insensitive).
DELETE /players/:id admin Delete a player and all their doses.
GET /timeseries { dates, series: [{ playerId, name, counts }] }counts[i] is that player's dose count on dates[i].
POST /admin/verify { password } { ok: boolean }, used by the UI to check a password before storing it.
POST /doses admin { playerId, date? } Add one dose for playerId on date (defaults to today; can't be in the future).
DELETE /doses/latest?playerId=&date= admin Remove the most recently logged dose for that player on date (defaults to today).

How it's built

  • Backend: Node.js + Express, server/. SQLite via Node's built-in node:sqlite, one file at DATA_DIR/leaderboard.db.
  • Frontend: plain HTML/CSS/JS in public/, no build step. Chart.js is vendored into public/vendor/ at install time (scripts/copy-vendor.js) so the app has no runtime dependency on a CDN.
  • Auth: a single admin password, checked against the x-admin-key header on write endpoints. Stateless — no sessions, no cookies.
server/
  index.js    — Express app bootstrap
  db.js       — SQLite connection + schema
  routes.js   — /api/* endpoints
  auth.js     — admin-key middleware
public/
  index.html, styles.css, app.js  — the UI (French)
  vendor/                          — generated, not committed (see .gitignore)

Troubleshooting

  • "ADMIN_PASSWORD environment variable is required" — you haven't set it in .env (Docker) or your shell (local dev).
  • Permission errors writing to ./data — if you pre-created the data directory as a different user, make sure it's writable by whoever the container runs as (the container runs as root by default for simplicity; chmod 777 data or matching ownership fixes it).
  • Wrong default date in the picker — set TZ in .env to your local timezone and restart (sudo docker compose up -d).