136 lines
5.8 KiB
Markdown
136 lines
5.8 KiB
Markdown
# 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`).
|