- AppReport entity + migration (type, message, fingerprint SHA-256, created_at)
- POST /api/report avec rate-limit 5/heure par fingerprint IP
- ReportModal : formulaire type radio (bug/idée) + textarea 1 000 chars max
- ChangelogModal : fonctionnalités en prod, roadmap, 5 limitations honnêtes
- Deux boutons flottants bas-gauche (? info + ✉ signalement)
- Légende remontée pour éviter le chevauchement
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Avant : SargassesMap faisait 1 appel /api/spots (API Platform) puis
N appels /api/spots/{id}/score (un par spot, ~100+ pour la Guadeloupe).
Après : un seul GET /api/spots/overview (LATERAL JOIN spots+scores)
avec Cache-Control: public, max-age=1800, stale-while-revalidate=300.
Les marqueurs colorés s'affichent immédiatement depuis la réponse unique ;
/api/spots/{id}/score n'est appelé qu'au clic sur un spot (SpotPanel).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
S3 OLCI n'a pas de bande SWIR : impossible de filtrer sun glint résiduel
et aérosols sahariens → faux positifs massifs à 300 m de résolution.
- Tuiles : ajout des propriétés source/confidence/coverage_km2
- Carte : styling data-driven par source
S2 → orange vif (#f4720a), opacité 65 % — signal confirmé
S3 → or pâle (#d4b830), opacité 20 % — signal incertain
- Légende : entrées distinctes S2 / S3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tippecanoe compresse les tiles .pbf par défaut. Caddy ne gzip pas
application/x-protobuf (pas dans sa liste par défaut), donc aucun
header Content-Encoding n'est émis. MapLibre v5 a supprimé la
détection auto des magic bytes gzip → erreur "Unable to parse tile".
--no-tile-compression génère des tiles protobuf bruts, lisibles
directement par MapLibre sans header supplémentaire.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Le style basculait de positron (URL) vers cartoonizeStyle() ~500 ms
après le premier rendu. Ce setStyle() déclenchait une réinscription
des sources/layers (react-map-gl v8 / maplibre-gl v5) qui échouait
silencieusement — les bancs de sargasses devenaient invisibles.
La carte est désormais montée UNE SEULE FOIS quand le style est prêt
(avec timeout 4 s → fallback positron). Plus de transition post-mount,
plus de race condition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remplace createMock() par createStub() pour tous les collaborateurs
sans expectations configurées. Les mocks avec expects() restent des
mocks locaux créés dans le test concerné. 0 notices sur 49 tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remplace le logo PNG par un SVG radar inline (cercles concentriques +
bras rotatif + blip) en bleu #2563eb/#60a5fa sur fond #1a1a2e.
Même SVG utilisé pour favicon.svg et le picto dans la topbar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Les subqueries de loadHorizonScores() et loadPatches() triaient par
detected_at DESC, ce qui provoquait une contamination inter-zones :
une ingestion récente de Guadeloupe avec des patches à ~189km de la Barbade
écrasait les données locales pourtant plus proches.
Aligner sur la logique de loadLatestScore() (ImpactScoreService) qui
trie déjà par ST_Distance ASC — les 3 sources (score now, patches,
horizons) utilisent maintenant la même observation de référence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
impact_score stocke source_observation_id — la lecture trie par distance(obs→spot)
ASC pour garantir que Barbade n'écrase jamais Guadeloupe. Suppression du garde
"closest wins" au write, remplacé par un ORDER BY déterministe au read.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ImpactScoreService : ne pas écraser le score si la nouvelle observation est plus
loin que le score actuel (sauf après 6h de stale). Empêche Barbade d'écraser
la Guadeloupe avec 367km quand des sargasses sont à 8km.
SpotScoreController : horizons et bancs filtrés par proximité (<300km) du spot,
plus la dernière observation globale quelle que soit sa zone.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>