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>
- scoreLabel/etaLabel/SpotMarker conditionnent Veille à distance<100km
pour éviter l'alerte ambre sur les spots sans sargasses proches
- "patch" → "banc de sargasses" dans le SpotPanel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- "Indice de risque" en petit header explicite au-dessus
- Le label (VEILLE / RISQUE / IMPACT) devient l'élément visuel principal (30px)
- Le nombre /100 rétrogradé en hint discret sous la barre
- Suppression de l'ambiguïté "est-ce une note ou un risque ?"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- scoreLabel() : low <15 → "OK", low ≥15 → "Veille", cohérence avec valeur numérique
- etaLabel() : score ≥15 sans ETA → "Sargasses en surveillance" (eta--watch)
- SpotMarker : étoile ambre pour level=low + scoreValue≥15 vs étoile verte pure OK
- spotScores stocke {level, value} au lieu de level seul
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- composer.json : symfony/messenger, symfony/scheduler, symfony/test-pack
- PHPStan fix : is_string() pour narrower mixed dans buildForecastGeometry
- phpunit.dist.xml + tests/bootstrap.php + bin/phpunit pour la suite de tests
- Suppression de src/Schedule.php (schedule vide généré par Flex, inutile)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ST_Dump sur la dernière observation → 5 patches les plus proches dans 200km, exposés
dans /spots/{id}/score. SpotPanel affiche le nombre de patches et la distance+surface
de chacun sous forme de barres (échelle 0–100km).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Expose breakdown {distance, density, velocity, trend} dans l'API /spots/{id}/score
(pour le score courant reconstruit depuis les champs stockés, et pour chaque horizon
avec confidence appliquée). Affiche les barres avec valeur/max dans le SpotPanel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- OceanCurrentClient : requête ERDDAP/OSCAR sans clé API, JSON direct
Résolution 1/3°, snap au point de grille le plus proche
Fallback gracieux (wind-only) si OSCAR indisponible
- DriftSimulationService v3 : déplacement total = vent Stokes (3%) + courant OSCAR (100%)
Physique correcte : les sargasses dérivent avec les courants de surface,
pas uniquement sous l'effet du vent
MODEL_VERSION 2.0.0 → 3.0.0
- Courant Caraïbe typique ~0.3–0.5 m/s (courant des Antilles NW) — contribution
majeure sur 24-48h vs vent Stokes seul (~0.15-0.25 m/s)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Symfony Scheduler (MainSchedule) : cron 0,6,12,18h UTC
- IngestionScheduleMessage + IngestionScheduleHandler : pipeline complet
ingestion → forecasts → impact scores pour toutes les zones Antilles
- messenger.yaml : transport scheduler_main + async Redis (rs971_async)
- entrypoint.sh : 2 workers en background (scheduler_main + async)
- GET /api/status : date/heure de la dernière observation
- SargassesMap : badge "Données du JJ/MM à HH:MM" en bas à gauche
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Avant : horizons = (1 - dist/50km) × 100 × confidence — échelle incomparable
avec le score now qui utilise distance(40) + densité(30) + vitesse(20) + tendance(10).
Après : même décomposition pour les 4 horizons, en passant currentDistanceKm
pour calculer la vitesse d'approche réelle par rapport à la position courante.
La confiance s'applique sur le score total (incertitude du modèle).
Résultat : si la sargasse s'éloigne à H+6, le score baisse. Si elle approche,
le score monte — mais atténué par la confiance du modèle (0.85 à H+6).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sans pondération, H+6 pouvait afficher 67 quand "now" était 34 car les
deux formules n'ont pas la même échelle. Multiplier par confidence (0.85
à H+6, 0.45 à H+48) rend les scores cohérents entre horizons : un risque
ne peut pas paraître croître si c'est juste l'incertitude qui augmente.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rm -rf assets/ + index.html avant chaque build React pour ne garder
que les fichiers du dernier build. index.php et sw.js Symfony sont préservés.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Les prévisions utilisent la même couleur (#f4a020/#c47a10) que les
observations confirmées, avec fill à 20% (vs 50%) et contour tirets.
Lecture immédiate : "sargasses futures" sans rupture visuelle arbitraire.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Les prévisions sont des estimations approximatives : réduire le fill à 0.12
laisse voir la carte (et la terre) en dessous, communiquant visuellement
"zone probable" vs observation confirmée. Contour tirets [6,4] plus lisible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. DriftSimulationService : suppression ST_Buffer(::geography) — produisait des
géométries invalides/dégénérées sur grand MULTIPOLYGON → ST_Distance retournait
652 029km. Remplacé par ST_Translate + ST_SimplifyPreserveTopology uniquement.
2. ImpactScoreService : filtre MAX_RELEVANT_M (300km) — une observation distante
ne remplace plus le score d'un spot proche. Les scores existants sont préservés
si l'observation est hors zone pertinente.
3. ComputeForecastsCommand --force : suppression du DELETE FROM impact_score.
On ne purge plus les scores existants ; computeForObservation ajoute les
nouveaux scores, les anciens restent valides pour les spots éloignés.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
L'ancienne approche (ST_GeneratePoints × 40 + ST_ConcaveHull) avait un défaut
majeur : les points aléatoires pouvaient ne pas couvrir les patches locaux
(ex: le patch à 3.61km de Barbados), donnant des distances absurdes à H+6 (654km).
Nouvelle approche :
- ST_Translate(geometry, dx_deg, dy_deg) : translation directe du MULTIPOLYGON
source → chaque patch individuel se déplace correctement
- ST_Buffer(... DIFFUSION_M × √horizon) : diffusion turbulente via dilatation
(H+6 ≈ +3.7km, H+48 ≈ +10.4km) — formes qui s'élargissent sans perdre la structure
- Déterministe : pas d'aléatoire, distances ST_Distance fiables
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DriftSimulationService : ajout diffusion turbulente 600m/h (marche aléatoire
indépendante par point) → les patches se dispersent et déforment au fil des horizons
au lieu d'une simple translation rigide avec les alizés constants
- SAMPLE_POINTS 30→40, CONCAVITY 0.8→0.7, MODEL_VERSION 1.0→1.1
- ComputeForecastsCommand : option --force pour supprimer et recalculer les
forecasts existants (utile après changement de modèle)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ST_Collect peut retourner une GeometryCollection selon les sous-types.
Correction : ST_CollectionExtract(..., 3) extrait uniquement les polygones,
ST_Multi garantit le type MultiPolygon, ST_GeomFromText fournit un fallback
vide typé correctement.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Problème : le threshold AFAI 0.005 (S2) / MCI 0.008 (S3) trop bas
classifie la majorité des pixels océan comme sargasse → gros rectangle
orange sur des zones sans sargasses.
Corrections :
- S2 AFAI threshold 0.005 → 0.015 (littérature : Hu 2009, Wang & Hu 2016)
- S3 MCI threshold 0.008 → 0.015
- Ajout protection sun glint : si NIR > 0.08 → pixel rejeté (S2 + S3)
- Division NDWI sécurisée (dénominateur = 0 → return 0)
- gdal_polygonize avec -mask (gdal_calc.py) : seuls les pixels DN=1
sont vectorisés, le reste n'existe pas dans le GeoJSON
- Filtre MIN_RING_POINTS=8 : supprime les polygones < ~2px (bruit)
- PROCESSING_VERSION 1.0.0 → 1.1.0 pour traçabilité
⚠️ Les observations en base avec processing_version='1.0.0' sont
faussées — les supprimer avant de relancer l'ingestion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Les dots couvraient tout le tile Sentinel (pas seulement les pixels
sargasses), saturant la vue. Fill opacity réduit à 0.5 pour laisser
respirer la carte sous-jacente.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>