| public | ||
| scripts | ||
| server | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
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 (inlocalStorage) 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-innode:sqlite, one file atDATA_DIR/leaderboard.db. - Frontend: plain HTML/CSS/JS in
public/, no build step. Chart.js is vendored intopublic/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-keyheader 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 thedatadirectory as a different user, make sure it's writable by whoever the container runs as (the container runs asrootby default for simplicity;chmod 777 dataor matching ownership fixes it). - Wrong default date in the picker — set
TZin.envto your local timezone and restart (sudo docker compose up -d).