Commit Graph

31 Commits

Author SHA1 Message Date
Gwadaking
d60951cae8 fix: score basé sur l'observation la plus proche du spot, pas la dernière globale
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>
2026-04-08 02:38:48 -04:00
Gwadaking
51ca910e11 fix: l'observation la plus proche gagne — évite l'écrasement par zone distante
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>
2026-04-08 02:33:31 -04:00
Gwadaking
5dfa000247 chore: add symfony/messenger + symfony/scheduler + phpunit infrastructure
- 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>
2026-04-08 01:51:42 -04:00
Gwadaking
c89560aea2 feat: suivi par patch — décomposition MULTIPOLYGON avec distance et surface par patch
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>
2026-04-03 23:36:19 -04:00
Gwadaking
d4d2314f7b feat: explainability — décomposition des 4 composantes du score dans le SpotPanel
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>
2026-04-03 23:33:07 -04:00
Gwadaking
779c07ced0 feat: courants océaniques OSCAR dans le modèle de dérive (v3)
- 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>
2026-04-03 22:39:11 -04:00
Gwadaking
48e6e5060c feat: ingestion automatique toutes les 6h + indicateur fraîcheur UI
- 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>
2026-04-03 22:31:58 -04:00
Gwadaking
55caca001c fix: scores horizons — même formule 4 composantes que le score now
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>
2026-04-03 21:51:56 -04:00
Gwadaking
0014870c3b fix: pondérer le score horizon par la confiance
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>
2026-04-03 21:49:09 -04:00
Gwadaking
a14782b067 fix: 3 bugs introduits lors du refactoring DriftSimulation/--force
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>
2026-04-03 20:25:49 -04:00
Gwadaking
1429eaa71c fix: DriftSimulationService v2 — ST_Translate au lieu de sample+ConcaveHull
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>
2026-04-03 20:14:22 -04:00
Gwadaking
fb25142bad feat: diffusion turbulente + recalcul forcé — polygones qui évoluent dans le temps
- 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>
2026-04-03 20:03:51 -04:00
Gwadaking
492ed02b6f feat: ForecastController avec filtres bbox+horizon — polygones par horizon sur la carte 2026-04-03 19:38:22 -04:00
Gwadaking
744f76f85e feat: ETA côtier — transformer la map en outil de décision actionnable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 18:42:21 -04:00
Gwadaking
00996cb163 fix: monter PHPStan à niveau 8 — corriger 2 bugs file_get_contents/json_encode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 17:08:45 -04:00
Gwadaking
0d323b871b add trend UX + PHPStan level 6 clean (0 errors)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 16:06:13 -04:00
Gwadaking
997e2d186c fix: GeometryCollection incompatible avec colonne MultiPolygon
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>
2026-04-02 17:06:42 -04:00
Gwadaking
6752b6df0b fix: durcir les filtres AFAI — SWIR guard, NDWI marge côtière, filtre PostGIS 1km²
- S2 : ajout garde SWIR (B11 > 0.03 = artifact atmo/cirrus), NIR abaissé
  à 0.06, NDWI marge côtière (> 0.05), AFAI threshold 0.020
- S3 : même logique, NIR 0.06, NDWI 0.05, MCI 0.020
- PostGIS : suppression des polygones < 1 km² après vectorisation
  (résidus ponctuels = bruit ou artefact atmosphérique en Caraïbe)
- Auto-suppression de l'observation si plus aucun polygone valide
- PROCESSING_VERSION 1.2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 17:04:20 -04:00
Gwadaking
edca50200e fix: corriger les faux positifs AFAI — seuils, masque gdal, filtre bruit
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>
2026-04-02 16:52:28 -04:00
Gwadaking
438a1fc392 fix: add NDWI water mask to evalscripts (S2 + S3) to exclude land pixels 2026-04-01 23:21:19 -04:00
Gwadaking
f0f28dca47 fix: json_encode geometry array before persisting SargassumObservation 2026-04-01 23:08:06 -04:00
Gwadaking
1d03698888 fix: remove unsupported sortby from CDSE catalog request 2026-04-01 23:06:29 -04:00
Gwadaking
1485339aec fix: expose lat/lng from CoastalPoint, fix getCoords in map 2026-04-01 22:37:14 -04:00
Gwadaking
1474b14e35 fix: use traefik-public network name 2026-04-01 19:56:44 -04:00
Gwadaking
2cef3725e9 feat: Phase 3 complete — push alerts, feedback scoring, S3 fallback, SPA routing, mobile CSS
- ImpactScoreService: implement getFeedbackBonus() (ST_Distance query on UserFeedback within 20km/6h)
- ImpactScoreService: trigger PushNotificationService when score >= 70
- PushNotificationService: send VAPID WebPush to spot subscribers, clean expired subs
- PushController: GET vapid-public-key, POST/DELETE subscribe with rate limiting
- SentinelHubClient: add optional $collection param, add Sentinel-3 OLCI FAI evalscript (MCI)
- IngestionService: add $collection param + HighCloudCoverageException for fallback logic
- IngestionService: add ingestWithFallback() — tries S2, falls back to S3 on high cloud
- IngestSentinelCommand: --source=auto (default) triggers ingestWithFallback
- FeedbackController: rate limiting via apiFeedbackLimiter
- Migration: push_subscription table
- rate_limiter.yaml: api_read(120/min), api_feedback(10/min), api_push(5/min)
- sw.js: service worker handling push events + notificationclick
- usePushSubscription hook: subscribe/unsubscribe lifecycle with VAPID
- SpotPanel: PushButton component integrated
- SpotPanel.css + TimelineSlider.css: mobile responsive (bottom-sheet on small screens)
- Caddyfile: SPA served at / with try_files fallback, sw.js served from root scope
- vite.config.js: build outDir → backend/public (not /spa)
- deploy/post-receive.sh: full deploy script (composer, npm build, migrations, cache, docker up)
- docs/roadmap.md: all Phase 3 + transversal items marked done

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 03:43:56 -04:00
Gwadaking
94bb6f5e8c Phase 2 : dérive, ImpactScore, slider temporel
Backend :
- OpenMeteoClient : vent horaire 72h (gratuit, sans clé)
- DriftSimulationService : échantillonnage ST_GeneratePoints,
  déplacement itératif Stokes 3%, reconstruction ST_ConcaveHull,
  4 horizons H+6/12/24/48, confiance décroissante
- ImpactScoreService : score 4 composantes (distance/densité/
  vitesse/tendance), cache Redis TTL 3h, invalidation à chaque calcul
- ComputeForecastsCommand : traite les observations sans forecast
- SpotScoreController : score courant + horizons depuis forecasts

Frontend :
- TimelineSlider : navigation Now/+6h/+12h/+24h/+48h
- SargassesMap : couche observations (orange) vs forecasts (violet)
  rechargée à chaque changement d'étape
- SpotPanel : affichage score par horizon actif + indicateur confiance
- Build → backend/public/spa/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 03:23:21 -04:00
Gwadaking
0c1deb93cb Phase 1 : seed CoastalPoints + frontend React complet
Backend :
- SeedCoastalPointsCommand : 29 points Antilles (idempotent)
  Martinique, Guadeloupe, Sainte-Lucie, Barbade, Saint-Martin

Frontend :
- api/client.js : wrapper fetch pour spots, observations, feedback
- SargassesMap : MapLibre GL JS, marqueurs spots, couche observations
  GeoJSON avec rechargement bbox dynamique à chaque mouvement carte
- SpotPanel : Spot Mode (score/niveau/distance/trend), bouton feedback
  terrain anonyme, gestion état loading/error/null
- App.jsx : composition map + panel, état selectedSpot
- vite.config.js : proxy /api → Symfony dev, build → backend/public/spa

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 03:17:43 -04:00
Gwadaking
4af5e62465 Phase 1 : endpoints API complets
- CoastalPoint → shortName Spot → /api/spots (API Platform)
- SargassumForecast → shortName Forecast → /api/forecasts
- ObservationController : GET /api/observations avec filtres
  bbox (PostGIS ST_Intersects), date, source + geometry ST_AsGeoJSON
- SpotScoreController : GET /api/spots/{id}/score avec ?at= optionnel
- FeedbackController : POST /api/feedback, anonyme, fingerprint SHA256
- CORS : autorisation radarsargasses971.com + localhost dev

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 02:50:06 -04:00
Gwadaking
bf64340525 Phase 1 : pipeline d'ingestion Sentinel-2
- SentinelHubClient : auth OAuth2, Catalog API (cloud coverage),
  Process API avec evalscript AFAI binaire (UINT8, cloud-side)
- IngestionService : orchestration complète — job tracking,
  rejet nuages, téléchargement GeoTIFF, polygonize GDAL,
  simplification PostGIS, persistance SargassumObservation
- IngestSentinelCommand : 5 zones Antilles configurées,
  options --date et --zone, appelable via cron docker exec
- Dockerfile : ajout gdal-bin + python3-gdal
- Correction entity : paramètre immutable retiré de #[ORM\Column]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 02:45:12 -04:00
Gwadaking
e5e729c487 Phase 0 : entités, migrations, API Platform, PostGIS
- API Platform + Doctrine ORM + jsor/doctrine-postgis installés
- 6 entités Symfony (SargassumObservation, SargassumForecast,
  DataIngestionJob, CoastalPoint, ImpactScore, UserFeedback)
- Migration initiale manuelle avec CREATE EXTENSION postgis,
  toutes les tables, index GIST et BTREE
- doctrine.yaml configuré pour PostgreSQL 16 + types PostGIS
- Roadmap Phase 0 complète

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 02:37:27 -04:00
Gwadaking
34e9675350 Fix : backend intégré en monorepo (suppression git imbriqué)
symfony new crée son propre .git — supprimé pour intégrer
backend/ comme dossier ordinaire dans le monorepo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 02:23:26 -04:00