Commit Graph

29 Commits

Author SHA1 Message Date
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