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

136 lines
5.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 <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:
```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`).