info-canicule/CLAUDE.md
Florian 290f9be7b9 fix+perf: corrections de revue (currentEcheance, dayOfYear, SWR, last-good, doc apikey)
- vigilance: currentEcheance basée sur productDatetime (jour calme renvoyait J1 à tort)
- normales: dayOfYear extrait en Europe/Paris pour 'now' (UTC mélangeait les jours après minuit)
- meteofrance-auth + CLAUDE.md: header `apikey:` documenté correctement (pas Authorization Bearer)
- cache: SWR — envelope {v, fu}, hard TTL = ttl*6, refresh background avec lock anti-stampede
- vigilance: snapshot last-good (TTL 30j) écrit à chaque fetch, fallback final si MF+ODS KO
- vigilance: nettoyage variable url morte dans fetchOpendatasoft

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:58:03 +02:00

8 KiB
Raw Blame History

CLAUDE.md — info-canicule

Objectif

Site d'utilité publique qui affiche en temps réel la Vigilance Météo France par département

  • les conseils officiels (canicule, orages, vent, pluie, neige, avalanches, vagues-submersion).

Hébergé sur le VPS Nocleus partagé (réseau shared-net, cache Valkey ACL info-canicule).

Stack

  • Astro 5 SSR (output: 'server', adapter @astrojs/node mode standalone)
  • TailwindCSS 3 (via @astrojs/tailwind) + @tailwindcss/typography
  • @astrojs/sitemap (statiques) + src/pages/sitemap-departements.xml.ts (les 96 dépts dynamiques)
  • @sentry/astro opt-in via SENTRY_DSN (cible GlitchTip, sample 0.1, source maps non uploadées). Si DSN absent, intégration omise.
  • ioredis pour le cache Valkey
  • TypeScript strict
  • Pas de DB Postgres (toutes les données vivent en cache Valkey, refresh à la demande)
  • Tests Playwright (tests/, pnpm test:e2e / test:e2e:local)
  • OG image générée au build par scripts/build-og-image.mjs (sharp, devDep)

Sources de données

Quatre flux indépendants, tous cachés en Valkey :

  • Vigilance (src/lib/vigilance.ts) — Opendatasoft weatherref-france-vigilance-meteo-departement (Licence Ouverte 2.0, pas d'auth, ~1 000 records par snapshot, dépts × phénomènes × échéances J/J1). Provider Météo France officiel utilisable en fallback si la clé est dispo. URL : https://public.opendatasoft.com/api/explore/v2.1/catalog/datasets/weatherref-france-vigilance-meteo-departement/records.
  • Observations horaires SYNOP (src/lib/observations.ts) — API Météo France, station SYNOP la plus proche du centroïde du dept, pré-calculé dans src/data/stations-synop.json. Alimente l'onglet « 24 h » sur les pages dept.
  • Climato quotidienne (src/lib/climato.ts) — fichiers CSV gzippés data.gouv.fr/meteofrance/.../QUOT (latest-2025-2026 pour l'année courante, previous-1950-2024 pour l'historique long). 365 jours glissants par dept. Cold-fetch optimisé : on skippe previous-1950-2024 si latest suffit (cf. commit ac46637).
  • Normales 1991-2020 (src/lib/normales.ts) — JSON statique src/data/normales.json (généré par scripts/build-normales.mjs), indexé par dept × day-of-year (1..366), lissage 7 jours.

Abstraction vigilance.ts à découper en deux implémentations (Opendatasoft / Météo France officiel) le jour où on bascule.

Schéma cache

Clé Valkey TTL (frais → hard) Contenu
vigilance:snapshot VIGILANCE_CACHE_TTL (défaut 900s) → ×6 VigilanceSnapshot (fetchedAt + productDatetime + alerts[])
vigilance:snapshot:fallback idem Snapshot Opendatasoft de secours si provider principal échoue
vigilance:snapshot:last-good 30 jours Dernier snapshot connu (filet de sécurité quand tous providers KO)
mf:hourly:<dept>:<hours> 30 min → ×6 HourlySeries (obs SYNOP sur fenêtre glissante)
climato:<dept> 24h → ×6 ClimatoSeries (365 jours)

Le prefix info-canicule: est injecté par ioredis (keyPrefix), donc la clé réelle côté Valkey est info-canicule:<clé> (l'ancien doc parlait d'un double prefix — c'était une erreur). Compatible avec l'ACL ~info-canicule:* côté VPS.

SWR : cacheOrFetch stocke {v, fu} (valeur + freshUntil). Au-delà de fu mais avant l'expiration Valkey (= ttl × 6), la valeur est servie stale et un refresh est déclenché en background (lock in-process pour éviter le stampede). Un blip MF de ~25 min reste invisible côté visiteur. Voir src/lib/cache.ts.

last-good : à chaque fetch réussi, le snapshot Vigilance est aussi persisté en :last-good (TTL 30j). Si MF + Opendatasoft échouent en même temps et que :snapshot/:fallback sont expirés, on sert :last-good plutôt que de planter la page. Le visiteur voit la date du bulletin dans l'UI (déjà affichée) et constate l'ancienneté.

Pages exposées

  • / — home + carte/grille des dépts
  • /departement/[code] — fiche dept avec onglets Vigilance / Observations SYNOP 24h / Climato 365j vs normales
  • /embed/ + /embed/dept/[code] — versions embarquables (iframe-friendly) pour intégration tierce
  • /conseils/ + /conseils/registre-canicule — fiches statiques
  • /api/health — JSON {status, cache, vigilance: {productDatetime, ageSeconds}}. Usage : UptimeRobot + cron HC.io de fraîcheur. Pas de CORS, Cache-Control: no-store, renvoie 503 si Valkey KO.
  • /sitemap-index.xml + /sitemap-departements.xml (les 96 dépts filtrés hors @astrojs/sitemap standard).

Cache HTTP (en plus du cache Valkey)

Home et pages dept renvoient Cache-Control: ... must-revalidate (cf. commit e72f25b) pour autoriser le revalidation des CDN/navigateurs tout en bornant la fraîcheur. À garder cohérent avec le TTL Valkey.

Pièges connus / à surveiller

  • product_datetime change 2× par jour (~06h et 16h Paris). Tant que Météo France n'a pas publié le suivant, le snapshot Opendatasoft renvoie les valeurs J→J1 du bulletin courant. Inutile de poll plus vite que 15 min.
  • Echéance "J" = aujourd'hui, "J1" = demain. Ne pas confondre avec un horizon en heures.
  • phenomenon_id : 1=vent, 2=pluie, 3=orages, 5=neige/verglas, 6=canicule, 8=avalanches, 9=vagues-submersion. Pas de 4 ni 7.
  • Andorre (domain_id = 99) est inclus dans le flux Vigilance mais n'est pas un département français. Mappé comme 99 — Andorre (zone Vigilance) côté front pour ne pas l'écarter silencieusement.
  • Corse : codes 2A et 2B, pas 20. Mais pour la climato previous-1950-2024, le fichier source est encore sous code 20 (split 2A/2B post-1976) — buildUrl() dans climato.ts gère ce cas.
  • SYNOP renvoie les températures en Kelvin. observations.ts les convertit via K2C(). Piège classique si on ajoute un nouveau champ température.
  • DROM : pas (encore) inclus dans le mapping departements.ts côté front. Vigilance Métropole only pour le MVP. Pour ajouter Outre-mer : étendre DEPARTEMENTS + tester si domain_id correspond aux codes 971-978.
  • Conseils par phénomène (advice.ts) : texte curated depuis sante.gouv.fr et meteofrance.fr. À relire / actualiser périodiquement (au moins 1× par an).
  • Normales 1991-2020 : fichier src/data/normales.json committé. Régénération via scripts/build-normales.mjs quand la décennie de référence Météo France change (~tous les 10 ans, prochain ~2031).
  • Cache miss au boot : si Valkey est down, cacheOrFetch log un warning mais re-fetch à chaque requête — pas de fallback persistant. Acceptable pour un service stateless, mais surveiller la latence Opendatasoft.

Déploiement

Pattern Reteno : push main → CI Forgejo SSH au VPS → git fetch && reset --hard && make env && docker compose up -d --build --wait.

Le .env.tmpl est commit, le .env réel est matérialisé par make env (pass-cli, vault Infra).

Météo France API — rotation API Key

  • Vault item : Infra/Météo France API, field api_key (hidden) + created_at + expires_at.
  • Le portail (portail-api.meteofrance.fr) propose une API Key longue durée (durée choisie à la création, max ~10 ans). Pas de flow OAuth2 client_credentials dispo gratuitement → on n'utilise QUE l'API Key, pas de refresh automatique.
  • Code : src/lib/meteofrance-auth.ts — header apikey: ${METEOFRANCE_API_KEY} (PAS Authorization: Bearer, qui est le format des tokens OAuth2 courts du portail).
  • Rotation : avant l'expiration notée dans le vault, regénérer côté portail, copier dans le vault, make env côté VPS, docker compose up -d --force-recreate app.
  • Si la clé expire / est révoquée : l'onglet « Observations 24 h » (SYNOP) sur les pages dept ne s'affiche plus (skip silencieux), pas d'impact sur Vigilance, Climato ni le reste du site. Erreur loggée côté serveur uniquement.
  • Si on veut être alerté : prévoir un cron HC.io qui curl /api/vigilance non, qui curl un endpoint test MF (à coder). Pour l'instant : se reposer sur la date expires_at du vault item.