- 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>
88 lines
8 KiB
Markdown
88 lines
8 KiB
Markdown
# 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.
|