From 83690b94ab77ade1c08faf90fac112b65a4f9436 Mon Sep 17 00:00:00 2001 From: Gwadaking Date: Tue, 14 Apr 2026 12:43:38 -0400 Subject: [PATCH] =?UTF-8?q?perf:=20spots/overview=20=E2=80=94=20100+=20req?= =?UTF-8?q?u=C3=AAtes=20individuelles=20=E2=86=92=201=20requ=C3=AAte=20cac?= =?UTF-8?q?h=C3=A9e=2030=20min?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avant : SargassesMap faisait 1 appel /api/spots (API Platform) puis N appels /api/spots/{id}/score (un par spot, ~100+ pour la Guadeloupe). Après : un seul GET /api/spots/overview (LATERAL JOIN spots+scores) avec Cache-Control: public, max-age=1800, stale-while-revalidate=300. Les marqueurs colorés s'affichent immédiatement depuis la réponse unique ; /api/spots/{id}/score n'est appelé qu'au clic sur un spot (SpotPanel). Co-Authored-By: Claude Sonnet 4.6 --- .../src/Controller/SpotScoreController.php | 40 +++++++++++++++++++ frontend/src/api/client.js | 6 +-- frontend/src/components/Map/SargassesMap.jsx | 39 +++++++++--------- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/backend/src/Controller/SpotScoreController.php b/backend/src/Controller/SpotScoreController.php index 672b8a4..8ba6982 100644 --- a/backend/src/Controller/SpotScoreController.php +++ b/backend/src/Controller/SpotScoreController.php @@ -20,6 +20,46 @@ class SpotScoreController extends AbstractController private Connection $connection, ) {} + /** + * GET /api/spots/overview + * + * Retourne tous les spots avec leur dernier score en UNE seule requête. + * Remplace les N appels individuels à /api/spots/{id}/score pour le rendu + * initial de la carte. + * + * Cache HTTP 30 min (scores mis à jour au max 1-2×/jour par ingestion). + */ + #[Route('/overview', name: 'overview', methods: ['GET'])] + public function overview(): JsonResponse + { + $rows = $this->connection->fetchAllAssociative( + "SELECT + cp.id::text, + cp.name, + cp.type, + cp.region, + ST_X(cp.geometry::geometry) AS longitude, + ST_Y(cp.geometry::geometry) AS latitude, + ist.score, + ist.level, + ROUND(CAST(ist.distance_to_nearest_sargassum AS numeric), 2) AS distance_km + FROM coastal_point cp + LEFT JOIN LATERAL ( + SELECT score, level, distance_to_nearest_sargassum + FROM impact_score + WHERE coastal_point_id = cp.id + ORDER BY timestamp DESC + LIMIT 1 + ) ist ON true + ORDER BY cp.name" + ); + + $response = $this->json($rows); + $response->headers->set('Cache-Control', 'public, max-age=1800, stale-while-revalidate=300'); + + return $response; + } + /** * GET /api/spots/{id}/score * diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index 09d4a03..1bc8086 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -25,9 +25,9 @@ async function post(path, body) { export const api = { spots: { - list: () => get('/spots'), - get: (id) => get(`/spots/${id}`), - score: (id, at) => get(`/spots/${id}/score`, { at }), + overview: () => get('/spots/overview'), // tous les spots + score en 1 requête cachée + get: (id) => get(`/spots/${id}`), + score: (id, at) => get(`/spots/${id}/score`, { at }), }, observations: { list: (bbox, date, source) => get('/observations', { bbox, date, source }), diff --git a/frontend/src/components/Map/SargassesMap.jsx b/frontend/src/components/Map/SargassesMap.jsx index 2af2249..8b6514c 100644 --- a/frontend/src/components/Map/SargassesMap.jsx +++ b/frontend/src/components/Map/SargassesMap.jsx @@ -179,8 +179,9 @@ function SpotMarker({ level, scoreValue, scoreDist, isSelected, name }) { export default function SargassesMap({ onSpotSelect, selectedSpotId, activeStep, flyToTarget }) { const mapRef = useRef(null); + // spots : [{ id, name, type, region, longitude, latitude, score, level, distance_km }] const [spots, setSpots] = useState([]); - const [spotScores, setSpotScores] = useState({}); + const [spotScores, setSpotScores] = useState({}); // id → { level, value, distance } const [viewState, setViewState] = useState(INITIAL_VIEW); const [layerOpacity, setLayerOpacity] = useState(1); const [mapStyle, setMapStyle] = useState(null); // null = style not yet ready @@ -212,28 +213,28 @@ export default function SargassesMap({ onSpotSelect, selectedSpotId, activeStep, return () => ctrl.abort(); }, []); - // Load coastal spots once + // Charge tous les spots + scores en UNE requête (cachée 30 min côté serveur) useEffect(() => { - api.spots.list() - .then(data => setSpots(Array.isArray(data) ? data : (data['hydra:member'] ?? data.member ?? []))) + api.spots.overview() + .then(rows => { + if (!Array.isArray(rows)) return; + setSpots(rows); + // Pré-remplir spotScores depuis la même réponse — aucune requête supplémentaire + const scores = {}; + rows.forEach(r => { + if (r.level) { + scores[r.id] = { + level: r.level, + value: r.score ?? 0, + distance: r.distance_km ?? null, + }; + } + }); + setSpotScores(scores); + }) .catch(console.error); }, []); - // Load scores progressively for marker colors - useEffect(() => { - if (!spots.length) return; - spots.forEach(s => { - api.spots.score(s.id) - .then(d => { - const level = d?.score?.level; - const value = d?.score?.value ?? 0; - const distance = d?.score?.distance ?? null; - if (level) setSpotScores(prev => ({ ...prev, [s.id]: { level, value, distance } })); - }) - .catch(() => {}); - }); - }, [spots]); - // Fade transition on step change useEffect(() => { setLayerOpacity(0);