- 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>
8 KiB
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/nodemode standalone) - TailwindCSS 3 (via
@astrojs/tailwind) +@tailwindcss/typography @astrojs/sitemap(statiques) +src/pages/sitemap-departements.xml.ts(les 96 dépts dynamiques)@sentry/astroopt-in viaSENTRY_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) — Opendatasoftweatherref-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é danssrc/data/stations-synop.json. Alimente l'onglet « 24 h » sur les pages dept. - Climato quotidienne (
src/lib/climato.ts) — fichiers CSV gzippésdata.gouv.fr/meteofrance/.../QUOT(latest-2025-2026pour l'année courante,previous-1950-2024pour l'historique long). 365 jours glissants par dept. Cold-fetch optimisé : on skippeprevious-1950-2024silatestsuffit (cf. commitac46637). - Normales 1991-2020 (
src/lib/normales.ts) — JSON statiquesrc/data/normales.json(généré parscripts/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/sitemapstandard).
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_datetimechange 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é comme99 — Andorre (zone Vigilance)côté front pour ne pas l'écarter silencieusement. - Corse : codes
2Aet2B, pas20. Mais pour la climatoprevious-1950-2024, le fichier source est encore sous code20(split 2A/2B post-1976) —buildUrl()dansclimato.tsgère ce cas. - SYNOP renvoie les températures en Kelvin.
observations.tsles convertit viaK2C(). Piège classique si on ajoute un nouveau champ température. - DROM : pas (encore) inclus dans le mapping
departements.tscôté front. Vigilance Métropole only pour le MVP. Pour ajouter Outre-mer : étendreDEPARTEMENTS+ tester sidomain_idcorrespond 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.jsoncommitté. Régénération viascripts/build-normales.mjsquand 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,
cacheOrFetchlog 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, fieldapi_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— headerapikey: ${METEOFRANCE_API_KEY}(PASAuthorization: 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 envcô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/vigilancenon, qui curl un endpoint testMF(à coder). Pour l'instant : se reposer sur la dateexpires_atdu vault item.