113 lines
13 KiB
Markdown
113 lines
13 KiB
Markdown
# 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](https://openfreemap.org) â 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 sanitiseÌ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).
|