Pronostics de la Coupe du Monde FIFA 2026 — dépôt
wcNg2026·bencrob/wc2026
🔗 Application en ligne → https://wc2026-rho.vercel.app/
Application de pronostics pour la Coupe du Monde FIFA 2026 (48 équipes, 12 groupes, 104 matchs) : saisie des scores, classements et qualification automatiques (2 premiers
- 8 meilleurs 3es), tableau final à 32 avec propagation des vainqueurs, comparaison aux résultats officiels. Statique, hors-ligne (PWA) ; backend cloud optionnel (Supabase) pour la sauvegarde par compte, l'authentification (Google ou e-mail) et le classement entre joueurs — l'app reste 100 % fonctionnelle en local si le cloud n'est pas configuré.
- Vue d'ensemble
- Architecture (SOLID, hexagonale)
- Démarrage & scripts
- Cloud optionnel (Supabase)
- Mettre à jour les résultats officiels
- Documentation & licence
flowchart TB
USER(["👤 Joueur"])
subgraph BROWSER["📱 Navigateur / PWA (offline-first)"]
SPA["App Angular — zoneless, signals<br/>domain · store · presentation"]
LS[("localStorage<br/>pronostics + session")]
SW["Service Worker<br/>cache app + offline + invite MAJ"]
SPA <--> LS
SW -.->|sert l'app| SPA
end
subgraph VERCEL["▲ Vercel — hébergement statique"]
HOST["Build Angular (CDN)"]
OFF[("official-results.json<br/>résultats officiels")]
end
subgraph SUPA["☁️ Supabase — cloud optionnel"]
AUTH["Auth · Google / e-mail OTP"]
DB[("Postgres + RLS<br/>profiles · predictions · leaderboard")]
end
subgraph CI["⚙️ Automatisation des scores"]
CRON["cron-job.org (horaire)<br/>+ cron GitHub (secours 3 h)"]
GHA["GitHub Actions<br/>update-scores.yml"]
SRC["football-data.org"]
end
USER --> SPA
HOST -->|charge| SPA
SPA -->|GET officiels| OFF
SPA -->|login| AUTH
SPA <-->|connecté : sync write-through| DB
AUTH --- DB
CRON --> GHA
GHA -->|fetch scores| SRC
GHA -->|commit si nouveau| OFF
OFF -->|redéploie| HOST
GHA -->|recalcul| DB
En clair : le joueur charge l'app (servie en statique par Vercel, utilisable hors-ligne via le
service worker). Ses pronostics vont d'abord en localStorage ; s'il se connecte (Google ou
e-mail OTP), ils sont synchronisés dans Supabase (write-through) et il entre au classement.
En parallèle, cron-job.org déclenche GitHub Actions qui récupère les scores officiels,
met à jour official-results.json (→ Vercel redéploie) et recalcule le classement dans Supabase.
src/app/
domain/ # TypeScript pur (0 dépendance Angular) : modèles, données,
# moteurs (poules, KO, 3es par backtracking), policies, validation,
# ports (persistence, official-results, file-io, auth, remote-predictions, profile, sync-prompts)
application/ # TournamentStore (signals + computed + garde de verrouillage),
# AccountSyncService (import/sync de compte), PwaUpdateService (maj PWA)
infrastructure/ # adapters : LocalStorage, résultats officiels, I/O fichier,
# facade write-through (cache local + push cloud), adaptateurs Supabase (auth/predictions/profile)
presentation/ # Angular Material + routeur : accueil (poules/3es/tableau), page /classement, login e-mail
- Signals (zoneless, OnPush) : 2 sources de vérité (pronostics + officiels) → tout le reste
computed. - Cloud optionnel : nouveaux ports + adaptateurs Supabase (voir Cloud optionnel (Supabase)). Si l'environnement Supabase n'est pas renseigné, ces adaptateurs sont inertes → mode local pur.
- Résultats officiels = fichier serveur
public/official-results.json, alimenté automatiquement (API football-data.org via GitHub Actions, voir plus bas) ou à la main (la saisie manuelle prime). Tout match ayant un résultat officiel — ou dont le coup d'envoi est passé — est en lecture seule (garde dans le store +disabledUI) ; l'officiel pilote classements et bracket, le pronostic reste affiché en comparaison (✓ exact / ≈ bon résultat / ✗ raté). - PWA offline : service worker, polices auto-hébergées,
official-results.jsonen cache freshness.
npm install
npm start # http://localhost:4200| Script | Rôle |
|---|---|
npm start |
Serveur de dev — http://localhost:4200 |
npm run build |
Build de production → dist/wcng2026/browser |
npm run watch |
Build de développement en watch |
npm test |
Tests unitaires de l'app (Vitest, jsdom) — 128 |
npm run test:scripts |
Tests des scripts (updater + classement, Vitest env node) — 18 |
npm run e2e |
Tests end-to-end (Playwright) — 3 |
npm run lint |
ESLint (angular-eslint, flat config) |
npm run update:scores |
Met à jour public/official-results.json depuis l'API de scores |
npm run update:leaderboard |
Recalcule la table leaderboard Supabase |
Build PWA : le service worker n'est actif qu'en build de production. Pour tester l'offline, servez
dist/wcng2026/browser(ex.npx http-server dist/wcng2026/browser).
Par défaut, les pronostics sont stockés en localStorage (anonyme, par appareil) — l'app marche hors-ligne sans compte. Se connecter active la sauvegarde par compte, la synchro multi-appareils et le classement entre joueurs. Tant que l'environnement Supabase n'est pas renseigné, tout le volet cloud est désactivé (mode local pur).
- Google (OAuth) et e-mail (code OTP, sans mot de passe) — l'e-mail est idéal iOS/PWA (aucune redirection). Le mode anonyme reste disponible (saisie locale sans compte).
- L'unicité du compte est garantie par l'e-mail (
auth.users, géré par Supabase). - À la première connexion : choix d'un pseudo (public, affiché au classement), puis import des pronostics locaux / résolution de conflit si les deux côtés diffèrent.
SyncPersistenceFacade écrit le cache localStorage immédiatement (offline) puis pousse vers le
cloud en différé (débounce ~1,5 s) si connecté ; en cas d'échec réseau, re-tentative à la reconnexion.
Supabase (@supabase/supabase-js) est chargé paresseusement et uniquement si une session existe
ou au retour OAuth → un visiteur anonyme ne le télécharge jamais.
flowchart LR
UI["Saisie pronostic"] --> FACADE["SyncPersistenceFacade"]
FACADE -->|synchrone| LS["localStorage (cache)"]
FACADE -->|si connecté, débounce| SUPA["Supabase · predictions"]
AUTH["Google / e-mail OTP"] --> SUPA
JOB["job CI (résultats officiels)"] --> LB["Supabase · leaderboard"]
SUPA --> APP["Classement / page /classement"]
LB --> APP
Schéma de la base (supabase/migrations/0001_init.sql)
| Table | Contenu | RLS |
|---|---|---|
profiles |
user_id, pseudo (unique), created_at |
lecture publique (pseudos), écriture self |
predictions |
user_id, scores (jsonb), version, updated_at |
privé : chacun ne lit/écrit que sa ligne |
leaderboard |
user_id, pseudo, points, exact, outcome, updated_at |
lecture publique ; écriture réservée au job (service_role) |
Les pronostics restent privés (RLS own-row) ; le classement n'expose que pseudo + points.
Le user_id ↔ e-mail vit dans auth.users (visible seulement dans la console Supabase / SQL Editor,
jamais depuis l'app).
Le score d'un joueur ne change qu'au changement des résultats officiels. Le script
scripts/update-leaderboard.ts (lancé après l'updater de scores)
lit tous les predictions + profiles (clé service_role), recalcule via TournamentEngine
(barème : 3 pts/score exact, 1 pt/bon résultat) et upsert la table leaderboard.
-
Créer un projet Supabase (free) et exécuter
supabase/migrations/0001_init.sql(SQL Editor). -
Authentication → Providers : activer Google (client OAuth Google Cloud) et/ou Email (OTP). URL Configuration → Redirect URLs :
http://localhost:4200+ le domaine Vercel. -
Renseigner l'URL + la clé anon (publiques) dans
src/environments/environment.ts:supabase: { url: 'https://<ref>.supabase.co', anonKey: 'sb_publishable_…' }
⚠️ La cléservice_role(secrète) ne va jamais dans le code — uniquement dans les secrets GitHub pour le job de classement (voir ci-dessous).
Deux voies alimentent public/official-results.json — la saisie manuelle prime.
Le workflow .github/workflows/update-scores.yml interroge
l'API football-data.org (Coupe du Monde), mappe chaque match terminé
sur nos ids M1..M104 (orientation + tirs au but, via TournamentEngine), valide, et commit le
fichier si un score est tombé → Vercel redéploie. Un match n'est relevé qu'à partir de 2 h après son
coup d'envoi ; sinon le tick suivant réessaie.
Déclenchement : horaire via cron-job.org (workflow_dispatch, ponctuel),
avec un filet de secours toutes les 3 h via le cron GitHub 0 */3 * * *.
flowchart LR
CRON["cron-job.org (horaire)<br/>+ cron GitHub (secours 3 h)"] --> SCRIPT["update-official-results.ts"]
SCRIPT -->|X-Auth-Token| API["football-data.org"]
API --> MAP["map → M1..M104<br/>(orientation + t.a.b.)"]
MAP --> MERGE{"absent du fichier<br/>& ≥ 2 h après K.O. ?"}
MERGE -->|non| SKIP["inchangé (manuel prime)"]
MERGE -->|oui| WRITE["écrit + commit"]
WRITE --> VERCEL["Vercel redéploie"]
Après l'écriture des scores, le workflow lance aussi l'étape classement
(scripts/update-leaderboard.ts) qui recalcule la table
leaderboard Supabase (no-op si les secrets Supabase sont absents).
- Secrets (Settings → Secrets → Actions) :
FOOTBALL_DATA_TOKEN— jeton lecture seule de l'API de scores.SUPABASE_URL+SUPABASE_SERVICE_ROLE_KEY— pour l'étape classement (clé secrète, ici uniquement).
- Manuel / test : Actions → Update official scores → Run workflow (ou
npm run update:scores/npm run update:leaderboard, options… -- --dry-run). ⚠️ Vérifier que la CDM 2026 est couverte par l'offre gratuite de l'API (sinon, changer de source — même script).
Le cron interne de GitHub Actions est souvent retardé/sauté (file d'attente partagée). Pour un
déclenchement ponctuel et fiable, un planificateur externe gratuit
(cron-job.org) appelle l'API workflow_dispatch toutes les heures :
POST https://api.github.com/repos/bencrob/wc2026/actions/workflows/update-scores.yml/dispatches
Headers: Authorization: Bearer <PAT fine-grained, scope Actions:write>
Accept: application/vnd.github+json · X-GitHub-Api-Version: 2022-11-28
Body: {"ref":"main"} # réponse attendue : 204
Le cron natif GitHub (0 */3 * * *) reste le filet de secours basse fréquence. L'updater étant
idempotent (+ concurrency), un double déclenchement est sans risque.
(Cron Vercel non utilisé : free tier = 1/jour.)
Éditer public/official-results.json puis git push (Vercel redéploie). Une valeur saisie à la
main n'est jamais écrasée par l'auto.
{
"version": 1,
"results": {
"M73": { "home": 1, "away": 1, "winner": "home" }
}
}Les matchs concernés passent en lecture seule au prochain chargement.
- Documentation complète (vault Obsidian, français) :
docs/Index.md— règles métier, règles techniques, tests & qualité, déploiement. - Guide des conventions (agents IA & contributeurs) :
CLAUDE.md. - Licence : MIT.