# 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](https://docs.docker.com/get-docker/) and Docker Compose (bundled with Docker Desktop, or `docker-compose-plugin` on Linux). ```bash git clone 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://: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: ```bash sudo docker compose up -d --build ``` To stop it: ```bash 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). ```bash 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`).