- TypeScript 71.9%
- Svelte 27.2%
- CSS 0.3%
- Dockerfile 0.3%
- HTML 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| docs | ||
| drizzle | ||
| e2e | ||
| scripts | ||
| src | ||
| static | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .npmrc | ||
| .oxfmtrc.json | ||
| .oxlintrc.json | ||
| .renovate.json | ||
| compose.yaml | ||
| Containerfile | ||
| docker-entrypoint.sh | ||
| drizzle.config.ts | ||
| package.json | ||
| playwright.config.ts | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
FLBX Cup
Webapp pour organiser des tournois de thèque (scouts) le jour J : plusieurs tournois en parallèle, poules, arbres (simple/double élimination, arbres incomplets), terrains, rotations lancées au micro, goûter, pauses, saisie des scores à plusieurs en temps réel.
Démarrer en local
pnpm install
cp .env.example .env # puis changez les codes
pnpm dev # http://localhost:5173
| Variable | Rôle |
|---|---|
ADMIN_PIN |
code administrateur (configuration + tout le reste) |
REFEREE_PIN |
code arbitre (saisie des scores) |
SESSION_SECRET |
chaîne aléatoire qui signe les cookies (openssl rand -hex 32) |
DATABASE_URL |
fichier SQLite (data/flbxcup.db) |
PUBLIC_TIME_ZONE |
fuseau d’affichage des heures (Europe/Paris) |
ORIGIN |
URL publique en production (ex. https://tournoi.example.org) |
METRICS_TOKEN |
facultatif : jeton des métriques Prometheus (voir Supervision) |
Déployer (VPS + Docker)
cp .env.example .env # codes + SESSION_SECRET
ORIGIN=https://tournoi.example.org docker compose up -d --build
- L’app écoute sur
127.0.0.1:3000; mettez votre reverse proxy devant (HTTPS, HTTP/2 recommandé : chaque écran ouvert garde une connexion SSE). - Désactivez le buffering du proxy pour
/api/stream(l’app envoie déjàX-Accel-Buffering: no). - Le proxy doit transmettre l’IP du visiteur dans
X-Forwarded-For(Caddy le fait tout seul ; nginx :proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;). Sinon tous les visiteurs partagent la même IP et 5 codes faux d’affilée bloquent la connexion de tout le monde pendant une minute. Plusieurs proxys en chaîne :XFF_DEPTH= leur nombre. - La base et les sauvegardes sont dans
./data(volume). GET /healthzrépondoksi la base répond.
Supervision (Grafana)
GET /api/metrics/prometheus expose l’état du serveur au format Prometheus (mémoire, CPU,
réactivité, flux SSE ouverts/fermés, cache public, requêtes). Accès avec une session admin ou
l’en-tête Authorization: Bearer $METRICS_TOKEN (sans METRICS_TOKEN, seul l’admin y accède).
Exemple pour vmagent ou Prometheus, puis une source VictoriaMetrics/Prometheus dans Grafana :
scrape_configs:
- job_name: flbxcup
scrape_interval: 15s
metrics_path: /api/metrics/prometheus
authorization:
credentials: <METRICS_TOKEN> # ou credentials_file
static_configs:
- targets: ['127.0.0.1:3000'] # ou tournoi.example.org avec scheme: https
Tableau de bord prêt à l’emploi : dans Grafana, Dashboards → New → Import, fichier
docs/grafana/flbxcup.json, puis choisir la source en haut du tableau
(téléphones connectés, réactivité, mémoire, flux SSE, cache public, CPU, redémarrages).
Depuis un autre conteneur, 127.0.0.1 n’est pas l’app : passer par le proxy (HTTPS) ou mettre les
deux conteneurs sur le même réseau Docker (cible flbxcup:3000).
Fiabilité
- Sauvegardes automatiques toutes les 5 minutes (s’il y a eu des changements) dans
data/backups/, les 60 dernières sont gardées. Liste, sauvegarde immédiate et téléchargement sur la page d’accueil admin. - Restaurer une sauvegarde :
docker compose stop, remplacerdata/flbxcup.dbpar le fichier sauvegardé (et supprimerflbxcup.db-wal/-shm),docker compose start. - Export d’un événement (JSON, page Historique) et import sur un autre serveur (page d’accueil).
- Historique + annulation de chaque action ; une annulation est refusée si les matchs concernés ont changé depuis.
- Plan B papier : bouton « Imprimer » du planning.
- Répétition : Configuration → « Créer une répétition », puis à la régie « Simuler les scores en cours » pour dérouler toute la journée à blanc la veille.
- Page publique (
/direct, sans code, QR code dans l'onglet « Page publique ») : données calculées une fois par changement et partagées par tous les téléphones, bouton pour la couper instantanément, état du serveur en direct (téléphones connectés, mémoire, réactivité). - Test de charge avant le jour J : bots qui imitent des téléphones + régie simulée, voir
docs/test-de-charge.md (
pnpm loadtest …). Variable facultativePUBLIC_MAX_STREAMS(3000 par défaut) : au-delà, les téléphones relisent toutes les 20 s au lieu de rester connectés.
Déroulé du jour J
- Configuration : terrains (attribués à un tournoi), tournois, équipes (coller une liste), format (modèles : élimination directe, double élimination, championnat, poules puis arbre, poules puis 2 arbres), aperçu, puis « Générer tous les matchs ».
- Régie (ordinateur) : « Lancer la rotation N » au moment de l’annonce au micro (texte à lire fourni). Les scores arrivent en direct depuis les arbitres, ou se saisissent directement.
- Arbitres (téléphone) : se connecter avec le code arbitre, choisir son terrain, saisir le score après le coup de sifflet.
- Fin des poules : la régie propose de valider le classement (égalités parfaites tranchées à la main), ce qui remplit les arbres.
- Goûter : page « Goûter & pauses », cocher les équipes et les rotations ; leurs matchs sont décalés, les terrains libérés sont utilisés par les autres.
- Imprévu : planning glisser-déposer (📌 = placé à la main), terrain fermé, équipe retirée (forfaits automatiques), correction de score (bloquée si la suite a déjà été jouée).
- Écran géant : sur l’ordinateur branché à l’écran, se connecter avec le code admin, ouvrir
l’onglet « Écran géant » puis « Ouvrir l’écran », et passer en plein écran (F11 ou
double-clic). Les diapos tournent tournoi par tournoi (en cours, à suivre, résultats, record du
jour, goûter : chacune se désactive) avec le compte à rebours de la rotation lancée (« Durée
d’un match »). Page pensée pour un mur LED d’environ 576×432 px. Si le boîtier LED ne reprend
qu’une zone du bureau pixel pour pixel, indiquer une taille fixe (ou
?w=576&h=432dans l’adresse) : l’image est alors collée en haut à gauche. En cas de coupure, l’écran garde les dernières infos (point rouge) et reprend tout seul.
Développement
pnpm run ci # format:check + lint (oxlint) + check (svelte-check --tsgo) + tests unitaires
pnpm test:e2e # Playwright sur un build de production
pnpm format # oxfmt
pnpm db:generate # nouvelle migration Drizzle après un changement de schéma
Structure :
src/lib/domain/: logique pure testée (poules, classements, seeding, arbres, résolution des sources, planificateur rotation × terrain, horaires).src/lib/server/: base (Drizzle + SQLite WAL), services (configuration, jour J, audit, planification), SSE, sauvegardes.src/routes/:admin/(bureau),arbitre/(mobile),imprimer/.