Commit Graph

107 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
163c2c3a56 fix: nettoyer assets/ avant npm build — plus de bundles orphelins
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>
2026-04-03 21:37:14 -04:00
Gwadaking
3136b5e7fc fix: couleur forecasts → même orange sargasses que observations
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>
2026-04-03 21:01:46 -04:00
Gwadaking
cdc8fe90ac fix: forecasts fill opacity 0.45→0.12 — zones prédictives non opaques
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>
2026-04-03 20:43:08 -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
878174404e fix: calculer ETA depuis horizons API — évite dépendance aux données stale en base 2026-04-03 19:35:21 -04:00
Gwadaking
22a3ce23a4 fix: corriger null paint et line-dasharray invalide dans cartoonizeStyle 2026-04-03 19:27:15 -04:00
Gwadaking
8bf6e20ddf fix: corriger chemin DEPLOY_DIR et docker run dans le hook 2026-04-03 19:14:19 -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
da7d62fa6a refactor: simplifier Dockerfile et hook — code via volume, pas image
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 17:13:09 -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
8a60c783b1 fix: supprimer le pattern dots, réduire opacité observations
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>
2026-04-02 16:42:33 -04:00
Gwadaking
6bf6762dac fix: ne pas injecter afai:null dans les propriétés GeoJSON
MapLibre considère null comme "propriété présente" → le filtre !has
ne prenait pas le relais → tous les polygones étaient masqués.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 16:36:57 -04:00
Gwadaking
3f1e49bb3b fix: masquer les zones d'observation sans sargasses (afaiIndex = 0)
Passe afaiIndex en propriété GeoJSON et applique un filtre MapLibre
sur les layers observation : seules les features avec afai > 0 sont
affichées. Fallback : si la propriété est absente, affichage normal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 16:21:15 -04:00
Gwadaking
7169dffc67 feat: refonte visuelle cartoon — fond OpenFreeMap, marqueurs SVG, légende comic, SpotPanel clair
- Fond cartoon : OpenFreeMap bright transformé (îles #7ec850, océan #3ab5e6, bordure #4a7c2f)
- Overlay vagues SVG subtil sur toute la carte (opacity 6%)
- Marqueurs spots SVG expressifs : ★ (ok), ! (risque), ✕ (impact) — animation bounce sur impact
- Sargasses #f4a020 avec texture dots MapLibre + contour épais 3.5px
- Légende comic : fond #fffbf0, bordure 3px, ombre dure 4px 4px 0 #333, Fredoka One
- SpotPanel blanc cassé avec ombre dure cartoon, gauge coloré pastel (vert/jaune/rouge)
- Topbar #1a1a2e avec boutons bleus ombre dure + hover translate(-2px,-2px)
- Timeline : step actif avec ombre dure cartoon
- Police Fredoka One chargée via Google Fonts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 15:25:17 -04:00
Gwadaking
1078a3459b fix: supprimer le width 1126px du #root qui bridait la map en desktop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 15:05:13 -04:00
Gwadaking
6e6df9fd89 build: bundle frontend assets
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 14:42:46 -04:00
Gwadaking
6fe6252a25 feat: refonte UX front — topbar, navigation île, marqueurs colorés, gauge score, fade layers
- Topbar glassmorphism : logo + boutons île (Guadeloupe/Martinique/Barbade/St-Martin/Dominique) + timeline intégrée
- flyTo() animé par île, vue initiale centrée sur Guadeloupe
- Marqueurs spots colorés par niveau d'impact (vert/jaune/rouge) chargés progressivement
- Score remplacé par un gauge : valeur en grand + barre de progression animée
- Légende flottante bas-gauche (observations / prévisions / niveaux)
- Transition fade 350ms sur les layers MapLibre au changement d'horizon temporel
- NavigationControl déplacé en bas à droite

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 00:06:40 -04:00
Gwadaking
d4c513e5e5 build: bundle frontend assets 2026-04-01 23:39:52 -04:00
Gwadaking
e8e82c4b5a feat: display current date in timeline slider 2026-04-01 23:31:34 -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
bb6ca7b563 fix: use spot.latitude/longitude for feedback (geometry no longer serialized) 2026-04-01 22:58:19 -04:00
Gwadaking
1485339aec fix: expose lat/lng from CoastalPoint, fix getCoords in map 2026-04-01 22:37:14 -04:00
Gwadaking
15079ea7b4 fix: enable application/json format in API Platform 2026-04-01 22:14:55 -04:00
Gwadaking
25244c308a fix: add SERVER_NAME=:80 for FrankenPHP, remove redundant traefik.docker.network label 2026-04-01 21:59:02 -04:00
Gwadaking
6cbd190ba0 fix: disable transaction on initial migration (CONCURRENTLY indexes) 2026-04-01 21:07:12 -04:00
Gwadaking
c02b58c3ce fix: remove unused frontend/dist volume, commit built assets 2026-04-01 20:57:28 -04:00
Gwadaking
77be044e3f fix: commit Symfony .env defaults, fix gitignore, rename to CLAUDE.md 2026-04-01 20:48:40 -04:00
Gwadaking
dc2f5af0fd fix: use shared db-network/redis_global, set container_name rs971_app 2026-04-01 20:41:53 -04:00
Gwadaking
8b7b2cf64f fix: add missing tls=true label for Traefik cert 2026-04-01 20:07:07 -04:00
Gwadaking
1474b14e35 fix: use traefik-public network name 2026-04-01 19:56:44 -04:00
Gwadaking
813b26ec1d fix: use traefik-public network name 2026-04-01 17:55:38 -04:00
Gwadaking
7ecb71599d fix: move cache:warmup to runtime entrypoint, fix POSTGRES_PASSWORD default
cache:warmup at build time fails because .env.local is not available in the
image (gitignored). Moved to entrypoint.sh which runs after volume mounts.
Also added default value for POSTGRES_PASSWORD in docker-compose.yml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 17:44:05 -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