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

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { api } from '../../api/client';
import usePushSubscription from '../../hooks/usePushSubscription';
import './SpotPanel.css';
const LEVEL_LABEL = { low: 'OK', medium: 'Risque', high: 'Impact' };
@@ -89,6 +90,7 @@ export default function SpotPanel({ spot, activeStep, onClose }) {
</div>
<FeedbackButton spot={spot} />
<PushButton spotId={spot.id} />
</div>
);
}
@@ -123,3 +125,23 @@ function FeedbackButton({ spot }) {
</div>
);
}
function PushButton({ spotId }) {
const { supported, subscribed, loading, error, toggle } = usePushSubscription(spotId);
if (!supported) return null;
return (
<div className="spot-panel__push">
<button
className={`spot-panel__push-btn ${subscribed ? 'spot-panel__push-btn--active' : ''}`}
onClick={toggle}
disabled={loading}
aria-pressed={subscribed}
>
{loading ? '…' : subscribed ? '🔔 Alertes activées' : '🔕 Activer les alertes'}
</button>
{error && <p className="spot-panel__push-error">{error}</p>}
</div>
);
}