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:
@@ -26,8 +26,26 @@ class IngestionService
|
||||
#[Autowire('%kernel.project_dir%')] private string $projectDir,
|
||||
) {}
|
||||
|
||||
public function ingest(array $bbox, \DateTimeImmutable $date, string $source = 'Sentinel-2'): void
|
||||
/**
|
||||
* Tente l'ingestion Sentinel-2, puis Sentinel-3 OLCI en fallback si la
|
||||
* couverture nuageuse est trop élevée ou si la scène S2 est indisponible.
|
||||
*/
|
||||
public function ingestWithFallback(array $bbox, \DateTimeImmutable $date): void
|
||||
{
|
||||
try {
|
||||
$this->ingest($bbox, $date, 'Sentinel-2', 'sentinel-2-l2a');
|
||||
} catch (HighCloudCoverageException $e) {
|
||||
$this->logger->info('S2 cloud too high, falling back to Sentinel-3', ['reason' => $e->getMessage()]);
|
||||
$this->ingest($bbox, $date, 'Sentinel-3', 'sentinel-3-olci');
|
||||
}
|
||||
}
|
||||
|
||||
public function ingest(
|
||||
array $bbox,
|
||||
\DateTimeImmutable $date,
|
||||
string $source = 'Sentinel-2',
|
||||
string $collection = 'sentinel-2-l2a',
|
||||
): void {
|
||||
$job = new DataIngestionJob();
|
||||
$job->setSource($source);
|
||||
$job->setTileId(sprintf('[%s]', implode(',', $bbox)));
|
||||
@@ -38,14 +56,16 @@ class IngestionService
|
||||
|
||||
try {
|
||||
// Étape 1 : vérification couverture nuageuse via Catalog API
|
||||
$cloudCoverage = $this->sentinelHub->getCloudCoverage($bbox, $date);
|
||||
$this->logger->info('Cloud coverage check', ['bbox' => $bbox, 'coverage' => $cloudCoverage]);
|
||||
$cloudCoverage = $this->sentinelHub->getCloudCoverage($bbox, $date, $collection);
|
||||
$this->logger->info('Cloud coverage check', ['bbox' => $bbox, 'coverage' => $cloudCoverage, 'collection' => $collection]);
|
||||
|
||||
if ($cloudCoverage > self::CLOUD_THRESHOLD) {
|
||||
$job->setStatus('failed');
|
||||
$job->setErrorMessage("Couverture nuageuse {$cloudCoverage}% > seuil " . self::CLOUD_THRESHOLD . '%');
|
||||
$this->em->flush();
|
||||
return;
|
||||
throw new HighCloudCoverageException(
|
||||
"Cloud coverage {$cloudCoverage}% exceeds threshold for collection {$collection}"
|
||||
);
|
||||
}
|
||||
|
||||
// Étape 2 : téléchargement du raster AFAI binaire
|
||||
@@ -54,7 +74,7 @@ class IngestionService
|
||||
$tifPath = $tmpDir . '/afai.tif';
|
||||
$geojsonPath = $tmpDir . '/polygons.geojson';
|
||||
|
||||
$metadata = $this->sentinelHub->downloadAfaiBinary($bbox, $date, $tifPath);
|
||||
$metadata = $this->sentinelHub->downloadAfaiBinary($bbox, $date, $tifPath, $collection);
|
||||
$this->logger->info('GeoTIFF downloaded', ['path' => $tifPath, 'tile' => $metadata['tileId']]);
|
||||
|
||||
// Étape 3 : vectorisation via GDAL
|
||||
|
||||
Reference in New Issue
Block a user