Files
JWE/docs/architecture.md
T

13 KiB
Raw Blame History

JWE v2 — DĂ©cisions d'architecture (Agent Architecte)

Date : 2026-07-16 · Mission : refonte GeoGuessr souverain (voir projet_geoguessr_97x.md)

1. Décision back-end : SvelteKit seul (pas de Strapi)

Décision : SvelteKit seul, avec les lieux dans un fichier JSON versionné dans Git.

Arguments :

  • FrugalitĂ© OKI : un seul processus Node Ă  hĂ©berger, pas de CMS + admin + DB Ă  maintenir, pas de surface d'attaque supplĂ©mentaire. SQLite serait de toute façon peu utile pour un corpus de ~40 lieux statiques.
  • CritĂšre du prompt (« qui ajoute les lieux ? ») : le corpus est Ă©ditorial, produit par l'Agent Contenu et relu. Le workflow Git (pull request = relecture + crĂ©dits photos vĂ©rifiĂ©s) est la modĂ©ration. Le docs/contribution.md documente l'ajout d'un lieu par PR.
  • Livrables finaux : le guide de dĂ©ploiement demandĂ© est « SvelteKit + Tile Server » — Strapi n'y figure pas.
  • Anti-triche : le JSON reste cĂŽtĂ© serveur (src/lib/server/), jamais importĂ© par le client ; les endpoints filtrent les champs. Impossible de faire plus simple.

Si un jour des contributeurs non-dev doivent saisir des lieux en masse, la porte vers Strapi reste ouverte (le schéma JSON est le content-type tout désigné), mais ce n'est pas le besoin actuel.

2. Architecture des tiles MapLibre

  • Rendu : MapLibre GL JS (verrouillĂ©), chargĂ© en lazy (import dynamique uniquement sur les Ă©crans de jeu / exploration).
  • Dev / dĂ©mo : tiles vectorielles OpenFreeMap — libres, basĂ©es OpenStreetMap, sans clĂ© API. Style custom OKI (JSON de style propre, dĂ©rivĂ© du style Positron, re-teintĂ© : mer bleu profond caraĂŻbe, terre sable, vĂ©gĂ©tation verte dense, labels sobres — pas le « bleu Google »).
  • Prod auto-hĂ©bergĂ©e : tileserver-gl + extraits .mbtiles OpenStreetMap des 4 rĂ©gions (Guadeloupe, Martinique, Guyane, La RĂ©union — quelques dizaines de Mo au total). ProcĂ©dure dans docs/deploiement.md. Le style OKI accepte l'URL des tiles par variable d'environnement (PUBLIC_TILES_URL), bascule sans changement de code.
  • Contrainte gĂ©ographique : maxBounds = enveloppe union des 4 rĂ©gions (≈ lon −63
57, lat −23
18) + minZoom, donc impossible de scroller en Europe/AmĂ©rique du Nord. 4 boutons « saut de rĂ©gion » (GUADELOUPE / MARTINIQUE / GUYANE / RÉUNION) replacent la vue sur la rĂ©gion. Limite connue : l'enveloppe rectangulaire inclut l'Atlantique tropical et le BrĂ©sil — inoffensif (aucun indice sur le lieu), documentĂ©.
  • Attribution © OpenStreetMap contributors affichĂ©e.

3. Stratégie Wikipedia / Wikidata

Principe : rien de stocké en base, interroge l'API en temps réel avec cache court, cÎté serveur uniquement.

Endpoint serveur : GET /api/wiki/[qid] (ex. Q3077840 = Fort DelgrĂšs — l'exemple Q2216838 du prompt initial Ă©tait erronĂ©, corrigĂ© par l'Agent Contenu)

  1. wbgetentities sur www.wikidata.org → rĂ©cupĂšre le sitelink frwiki (et le libellĂ© fr en fallback). Fallback de langue : fr uniquement — dĂ©cision d'audit (A3) : pas de repli sur enwiki, on prĂ©fĂšre l'appel Ă  contribution qu'une redirection silencieuse vers l'anglais.
  2. GET https://fr.wikipedia.org/api/rest_v1/page/summary/{titre} → title, extract, content_urls, thumbnail.
  3. Réponse normalisée : { qid, title, extract, url, thumbnail, description }.
  4. QID sans sitelink frwiki → 404 {"reason":"no-frwiki"} (cas intermĂ©diaire, A3) : le front affiche la variante Cas B « Pwen blindĂ© — Pa nyen an fransĂ© » (la fiche Wikidata existe, l'article français reste Ă  traduire/crĂ©er ; CTA recherche prĂ©-remplie + lien vers la fiche Wikidata). wikidata_id null cĂŽtĂ© lieu → Cas B « lacune totale » inchangĂ©.
  5. Cache : Map en mémoire, TTL 24 h + Cache-Control: max-age=86400. User-Agent explicite (JWE-OKI/x.y (contact)) comme demandé par la politique Wikimédia.

4. Arborescence du projet

JWE/                          (dĂ©pĂŽt existant — l'ancien PHP est conservĂ© tel quel)
├── app/                      ← nouvelle application SvelteKit 2 / Svelte 5
│   ├── src/
│   │   ├── lib/
│   │   │   ├── server/
│   │   │   │   ├── lieux.ts        (accĂšs donnĂ©es + sĂ©lection alĂ©atoire + scoring)
│   │   │   │   └── data/lieux.json (corpus 40 lieux — serveur uniquement)
│   │   │   ├── components/         (GameMap, PhotoPanel, ResultPanel, ScoreCard
)
│   │   │   ├── i18n/               (fr.ts, gcf.ts, store)
│   │   │   ├── styles/oki.css      (tokens couleurs OKI)
│   │   │   └── utils/              (score.ts, geo.ts)
│   │   ├── routes/
│   │   │   ├── +layout.svelte      (header, switch langue FR/CR)
│   │   │   ├── +page.svelte        (accueil : 4 rĂ©gions, modes, Jouer)
│   │   │   ├── jeu/[mode]/         (Ă©cran de jeu split photo/carte)
│   │   │   ├── explorer/           (mode Aprann : carte pleine page)
│   │   │   └── api/
│   │   │       ├── round/+server.ts      (GET : lieu alĂ©atoire SANITISÉ)
│   │   │       ├── photo/[id]/+server.ts (GET : proxy photo sharp → WebP, sans EXIF, cache 24 h)
│   │   │       ├── guess/+server.ts      (POST : {placeId, lat, lon, hints, timeMs} → score + vĂ©ritĂ©)
│   │   │       ├── lieux/+server.ts      (GET : catalogue sanitise pour Explorer, SANS id)
│   │   │       └── wiki/[qid]/+server.ts (GET : extrait Wikipedia, cache 24 h)
│   │   ├── app.html / app.d.ts
│   │   └── service-worker.ts       (PWA)
│   ├── static/                     (manifest.webmanifest, icĂŽnes, photos Ă©ventuelles)
│   └── package.json / svelte.config.js / vite.config.ts
├── docs/                         (architecture, dĂ©ploiement, contribution, audit)
├── content/                      (sources de travail de l'Agent Contenu : crĂ©dits photos)
└── projet_geoguessr_97x.md

5. Spécifications API REST (anti-triche)

Endpoint Méthode Payload Réponse
/api/round?region=&mode=&exclude= GET — { id, nom, nom_creole, categorie, difficulte, photo{url,credit,alt,srcset}, indices[] } — jamais coordonnees, commune, region, wikidata_id. photo.url = /api/photo/<uuid> (opaque, proxy serveur — C4) + photo.srcset { "400", "800", "1200" }
/api/photo/[id]?w= GET w ∈ {400, 800, 1200} (défaut 1200) image/webp retraitée (sharp : rotation auto, métadonnées EXIF/GPS supprimées), Cache-Control: public, max-age=86400, immutable, cache mémoire 24 h. 404 si id inconnu, 400 si w invalide. Fetch amont = URL Commons du corpus uniquement (pas de proxy ouvert)
/api/guess POST { id, lat, lon, hintsUsed, timeMs? } { distanceKm, score, radiusEffKm, coordonnees, commune, region, nom, wikidata_id }
/api/lieux GET — Catalogue sanitise pour Explorer (coords arrondies Ă  0.05°, mode sans score). Sans id (anti-corrĂ©lation round → commune, C4) ; photos en URLs Commons directes (mode sans enjeu)
/api/wiki/[qid] GET — { title, extract, url, thumbnail }, Cache-Control: max-age=86400 ; 404 {"reason":"no-frwiki"} si QID sans article frwiki

RĂšgles :

  • Le scoring est recalculĂ© cĂŽtĂ© serveur : score = round(5000 × e^(−d/r_eff)) avec r_eff = max(r_difficultĂ©, sqrt(aire_commune_km2 / π)) quand le champ optionnel aire_commune_km2 est renseignĂ© (A1 — Ă©quitĂ© Guyane : communes immenses), sinon r_difficultĂ© seul (50/15/5/1 km). PĂ©nalitĂ© −10 %/indice, bonus temps ≀ +20 % plafonnĂ© Ă  5000. radiusEffKm (0,1 km) est renvoyĂ© pour transparence.
  • Distance : haversine.
  • Le client ne reçoit la vĂ©ritĂ© (coordonnĂ©es, commune, wikidata_id) qu'aprĂšs la soumission du marqueur.
  • exclude : liste d'ids dĂ©jĂ  jouĂ©s (mode DĂ©fi 5 rounds, stateless — pas de session serveur nĂ©cessaire, pas de leaderboard).

6. Décisions annexes

  • Adapter : adapter-node (auto-hĂ©bergement sobre derriĂšre nginx/caddy).
  • PWA : manifest + service worker minimal (cache app shell, photos en stale-while-revalidate).
  • i18n : dictionnaires maison fr / gcf (crĂ©ole), store Svelte, lang persistĂ©e en localStorage.
  • Photos : en mode jeu, proxy serveur /api/photo/[id] (C4 — l'URL Wikimedia Commons contient le nom du fichier, souvent un spoiler, et les EXIF peuvent embarquer du GPS) : le serveur fetch l'URL Commons du corpus, retraite avec sharp (rotation auto, suppression de toutes les mĂ©tadonnĂ©es par dĂ©faut, WebP, largeurs 400/800/1200), cache mĂ©moire 24 h + Cache-Control: immutable. PhotoPanel utilise <img src srcset sizes>. L'Explorer (/api/lieux) garde les URLs Commons directes (mode sans enjeu) et le service worker met en cache les deux origines.
  • A11y / calm tech : prefers-reduced-motion respectĂ© (pas de vol de marqueur), timer informatif non anxiogĂšne, navigation clavier (carte : flĂšches dĂ©placent le marqueur, EntrĂ©e valide).
  • Licence code : MIT (le dĂ©pĂŽt historique Ă©tait AGPL — la refonte est un nouveau module app/ sous MIT comme exigĂ© par le prompt ; le LICENSE racine reste l'historique, un app/LICENSE MIT est ajoutĂ©).

7. Correctifs d'audit (2026-07-17)

  • C4 — URLs photo spoiler : voir §6 « Photos » et §5. Deux fuites colmatĂ©es : le nom de fichier Commons dans photo.url (remplacĂ© par le proxy opaque /api/photo/<uuid>) et la corrĂ©lation id round ↔ id catalogue (/api/lieux n'expose plus d'id ; l'Explorer utilise des indices Ă©phĂ©mĂšres cĂŽtĂ© client).
  • A1 — Radius de score en Guyane : voir §5 « RĂšgles ». aire_commune_km2 (champ optionnel du schĂ©ma) Ă©largit le rayon effectif : sqrt(aire/π). Test de rĂ©fĂ©rence : Saint-Laurent-du-Maroni (4320,8 kmÂČ, EXPERT), guess Ă  40 km → radiusEffKm 37,1, score ≈ 1700 (au lieu de ≈ 0). Le code tolĂšre l'absence du champ.
  • A2 — Pas de fuite de rĂ©gion : la camĂ©ra initiale de GameMap est globale (center: [-58, 0], zoom: 3.6 — vue d'ensemble des 4 rĂ©gions, aucun indice sur le lieu). Les 4 boutons rĂ©gion sont la devinette en deux temps : Ă©tape 1 = sauter vers la rĂ©gion supposĂ©e, Ă©tape 2 = poser le marqueur. Les deux Ă©tapes sont combinĂ©es par la distance seule : mauvaise rĂ©gion → distance Ă©norme → score ≈ 0. Aucune donnĂ©e serveur ne rĂ©vĂšle la rĂ©gion avant le guess (/api/round ne l'inclut pas).
  • A3 — ChaĂźne Wikipedia : voir §3. Cache-Control: max-age=86400 ; cas intermĂ©diaire QID sans frwiki → 404 {"reason":"no-frwiki"} → variante Cas B dĂ©diĂ©e cĂŽtĂ© front ; fallback de langue fr uniquement, documentĂ© dans le code.

8. Phase 2 (2026-07-17) — ADR-001 v1 et ADR-002

  • ADR-001 v1 — DĂ©fi par lien Ă  seed (/defi/<seed>) : src/lib/server/tirage.ts (hash cyrb53 → PRNG mulberry32, Fisher-Yates dĂ©terministe, contraintes ≄1 MONUMENT / ≄1 LIEU / ≄2 rĂ©gions par Ă©changes dĂ©terministes — zĂ©ro Math.random, dĂ©terminisme inter-machines). GET /api/defi/[seed] → 5 rounds sanitisés identiques pour tous (no-store, seed alphanumĂ©rique 4–40). Accueil : « CrĂ©er un lien de dĂ©fi » (seed 8 chars sans ambiguĂŻtĂ©). Bilan : « Copier le lien du dĂ©fi ». La comparaison des scores reste humaine (image canvas) — zĂ©ro compte, zĂ©ro Ă©tat serveur, zĂ©ro leaderboard. La v2 temps rĂ©el (PlaySocketJS) reste conditionnĂ©e au gate de traction de l'ADR (≄30 parties/sem. ×4 sem. + ≄5 demandes explicites).
  • ADR-002 — Carte SVG lĂ©gĂšre hors-jeu : pipeline app/scripts/build-carte-regions.mjs (geo.api.gouv.fr → mapshaper 7 % → src/lib/map/regions-geo.json, 91,8 KB gz ≀ 150 KB) ; composant CarteRegions.svelte from scratch (SVG, projection Ă©quirectangulaire par inset, 2×2 responsive, <button> par rĂ©gion avec aria-label FR/crĂ©ole, SSR sans JS, prop marqueurs pour le rĂ©cap guess→rĂ©el, attribution © OSM/donnĂ©es publiques). UtilisĂ©e sur l'accueil (sĂ©lection de rĂ©gion) et les bilans dĂ©fi — MapLibre reste strictement aux Ă©crans de jeu (devinette, rĂ©sultat, explorer). Attribution : contours © geo.api.gouv.fr / donnĂ©es publiques.

9. Correctif UX résultat (2026-07-17)

  • Carte rĂ©sultat façon GeoGuessr : Ă  la rĂ©vĂ©lation, le marqueur du joueur reste Ă  sa position (corail) et la position rĂ©elle apparaĂźt (vert), reliĂ©es par une ligne pointillĂ©e avec Ă©tiquette de distance au milieu (distanceKm) ; la carte cadre les deux points (fitBounds, instantanĂ© si prefers-reduced-motion). Remplace l'ancien « vol » du marqueur vers la position rĂ©elle, qui dĂ©truisait la comparaison visuelle. Prop distanceKm ajoutĂ©e Ă  GameMap (alimentĂ©e par la rĂ©ponse serveur, jamais calculĂ©e cĂŽtĂ© client).