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>
This commit is contained in:
Gwadaking
2026-04-01 03:43:56 -04:00
parent 94bb6f5e8c
commit 2cef3725e9
24 changed files with 780 additions and 42 deletions

View File

@@ -38,7 +38,7 @@ class IngestSentinelCommand extends Command
->addOption('zone', 'z', InputOption::VALUE_OPTIONAL,
'Zone spécifique. Défaut : toutes les zones.', null)
->addOption('source', 's', InputOption::VALUE_OPTIONAL,
'Source satellite (Sentinel-2 ou Sentinel-3)', 'Sentinel-2');
'Source satellite : Sentinel-2, Sentinel-3, ou auto (S2 avec fallback S3)', 'auto');
}
protected function execute(InputInterface $input, OutputInterface $output): int
@@ -71,7 +71,15 @@ class IngestSentinelCommand extends Command
foreach ($zones as $name => $bbox) {
$io->section("Zone : {$name}");
try {
$this->ingestionService->ingest($bbox, $date, $source);
if ($source === 'auto') {
$this->ingestionService->ingestWithFallback($bbox, $date);
} else {
$collection = match (strtolower($source)) {
'sentinel-3' => 'sentinel-3-olci',
default => 'sentinel-2-l2a',
};
$this->ingestionService->ingest($bbox, $date, $source, $collection);
}
$io->success("OK — {$name}");
} catch (\Throwable $e) {
$io->error("ÉCHEC — {$name} : " . $e->getMessage());