diff --git a/.env.example b/.env.example
index 5448648..1c045cb 100644
--- a/.env.example
+++ b/.env.example
@@ -16,3 +16,11 @@ VIGILANCE_CACHE_TTL=900
# Umami analytics (optionnel — laisser vide pour désactiver)
UMAMI_WEBSITE_ID=
UMAMI_SRC=https://analytics.nocleus.com/script.js
+
+# Météo France API (OAuth2 sur portail-api.meteofrance.fr)
+# Choisir l'un OU l'autre :
+# OAuth2 (recommandé prod, refresh auto) :
+METEOFRANCE_CLIENT_ID=
+METEOFRANCE_CLIENT_SECRET=
+# Static token (test seulement, expire 1h) :
+METEOFRANCE_STATIC_TOKEN=
diff --git a/.env.tmpl b/.env.tmpl
index ab3f80f..b17dd2a 100644
--- a/.env.tmpl
+++ b/.env.tmpl
@@ -14,3 +14,7 @@ UMAMI_SRC=https://analytics.nocleus.com/script.js
# GlitchTip (optionnel — si vide, pas d'envoi Sentry)
SENTRY_DSN={{ pass://Infra/Info Canicule — secrets/SENTRY_DSN }}
+
+# Météo France API (portail-api.meteofrance.fr) — OAuth2 client_credentials pour hourly observations
+METEOFRANCE_CLIENT_ID={{ pass://Infra/Météo France API/client_id }}
+METEOFRANCE_CLIENT_SECRET={{ pass://Infra/Météo France API/client_secret }}
diff --git a/.gitignore b/.gitignore
index 4992d71..199b6e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,7 @@ dist
.vscode/*.local
.idea
data-sources/
+*:Zone.Identifier
+test-results/
+playwright-report/
+playwright/.cache/
diff --git a/Données_d’observation_swagger.json b/Données_d’observation_swagger.json
deleted file mode 100644
index 18dbff1..0000000
--- a/Données_d’observation_swagger.json
+++ /dev/null
@@ -1,460 +0,0 @@
-{
- "openapi": "3.0.1",
- "info": {
- "title": "DonneesPubliquesObservation",
- "version": "1"
- },
- "servers": [
- {
- "url": "https://public-api.meteofrance.fr/public/DPObs/v1"
- }
- ],
- "security": [
- {
- "default": []
- }
- ],
- "tags": [
- {
- "name": "Produits Obs",
- "description": "Services de téléchargement des données d'observation à la date demandée."
- },
- {
- "name": "Produits Synop",
- "description": "Services de téléchargement des messages internationaux d’observation en surface, SYNOP, à la date demandée."
- },
- {
- "name": "Produits Bouees",
- "description": "ProduitBouees - Service de téléchargement des données d'observations météorologiques effectuées par les bouées Météo-France, à la date demandée"
- }
- ],
- "paths": {
- "/station/infrahoraire-6m": {
- "get": {
- "tags": [
- "Produits Obs"
- ],
- "summary": "Télécharger le fichier TEXTE (CSV ou JSON ou GEOJSON) des données d'observation pour tous les paramètres disponibles, à la fréquence 6 minutes, pour une station, à la date la plus proche de la date demandée.",
- "description": "Renvoie tous les paramètres disponibles pour la station demandée et pour :\n- la date/heure la plus proche de la date demandée selon les données disponibles.",
- "parameters": [
- {
- "name": "id_station",
- "in": "query",
- "description": "Identifiant de la station (nomenclature : 8 chiffres selon DDCCCNNN = insee de la commune (DD département, CCC n° de la commune dans le département et NNN n° de la station dans la commune)",
- "required": true,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "date",
- "in": "query",
- "description": "Date demandée (au format ISO 8601 avec TZ UTC : AAAA-MM-JJThh:mm:ssZ).\n\nPar défaut = date courante",
- "required": false,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string",
- "format": "date-time"
- }
- },
- {
- "name": "format",
- "in": "query",
- "description": "Format de retour des données (JSON ou CSV ou GEOJSON)",
- "required": true,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string",
- "enum": [
- "json",
- "csv",
- "geojson"
- ]
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Si une liste vide est renvoyée : station absente ou inexistante\n \nSinon : OK"
- },
- "400": {
- "description": "Contrôle de paramètres en erreur"
- },
- "404": {
- "description": "Jeu de données inexistant"
- }
- },
- "security": [
- {
- "default": []
- }
- ],
- "x-auth-type": "Application & Application User",
- "x-throttling-tier": "Unlimited"
- }
- },
- "/station/horaire": {
- "get": {
- "tags": [
- "Produits Obs"
- ],
- "summary": "Télécharger le fichier TEXTE (CSV ou JSON ou GEOJSON) des données d’observation pour tous les paramètres disponibles, à la fréquence horaire, pour une station, à la date la plus proche de la date demandée.",
- "description": "Renvoie tous les paramètres disponibles pour la station demandée et pour :\n- la date/heure la plus proche de la date demandée selon les données disponibles.",
- "parameters": [
- {
- "name": "id_station",
- "in": "query",
- "description": "Identifiant de la station (nomenclature : 8 chiffres selon DDCCCNNN = insee de la commune (DD département, CCC n° de la commune dans le département et NNN n° de la station dans la commune)",
- "required": true,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "date",
- "in": "query",
- "description": "Date demandée (au format ISO 8601 avec TZ UTC : AAAA-MM-JJThh:mm:ssZ).\n\nPar défaut = date courante",
- "required": false,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string",
- "format": "date-time"
- }
- },
- {
- "name": "format",
- "in": "query",
- "description": "Format de retour des données (JSON ou CSV ou GEOJSON)",
- "required": true,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string",
- "enum": [
- "json",
- "csv",
- "geojson"
- ]
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Si une liste vide est renvoyée : station absente ou inexistante\n \nSinon : OK"
- },
- "400": {
- "description": "Contrôle de paramètres en erreur"
- },
- "404": {
- "description": "Jeu de données inexistant"
- }
- },
- "security": [
- {
- "default": []
- }
- ],
- "x-auth-type": "Application & Application User",
- "x-throttling-tier": "Unlimited"
- }
- },
- "/liste-stations": {
- "get": {
- "tags": [
- "Produits Obs"
- ],
- "summary": "Télécharger le fichier TEXTE (CSV) de la liste des stations d'observation.",
- "description": "Renvoie la liste des stations.",
- "parameters": [],
- "responses": {
- "200": {
- "description": "OK"
- },
- "404": {
- "description": "La liste est indisponible"
- }
- },
- "security": [
- {
- "default": []
- }
- ],
- "x-auth-type": "Application & Application User",
- "x-throttling-tier": "Unlimited"
- }
- },
- "/liste-stations-synop": {
- "get": {
- "tags": [
- "Produits Synop"
- ],
- "summary": "Télécharger le fichier TEXTE de la liste des stations d'observation.",
- "description": "Renvoie la liste des stations.",
- "parameters": [
- {
- "name": "format",
- "in": "query",
- "description": "Format de retour des données (JSON ou CSV ou GEOJSON)",
- "required": true,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string",
- "enum": [
- "json",
- "csv",
- "geojson"
- ]
- }
- }
- ],
- "responses": {
- "200": {
- "description": "OK"
- },
- "404": {
- "description": "La liste est indisponible"
- }
- },
- "security": [
- {
- "default": []
- }
- ],
- "x-auth-type": "Application & Application User",
- "x-throttling-tier": "Unlimited"
- }
- },
- "/synop": {
- "get": {
- "tags": [
- "Produits Synop"
- ],
- "summary": "Télécharger le fichier TEXTE (CSV ou JSON ou GEOJSON) des données SYNOP pour une station, à la date demandée.",
- "description": "Renvoie tous les paramètres disponibles pour la station et pour la date/heure demandées.",
- "parameters": [
- {
- "name": "format",
- "in": "query",
- "description": "Format de retour des données (JSON ou CSV ou GEOJSON)",
- "required": true,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string",
- "enum": [
- "json",
- "csv",
- "geojson"
- ]
- }
- },
- {
- "name": "id_station",
- "in": "query",
- "description": "Identifiant(s) de la station (séparés par des , par exemple id_station=07002,07003)",
- "required": false,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "date_debut",
- "in": "query",
- "description": "Date de début d'observation au format AAAA-MM-JJTHH:mm:00Z.",
- "required": false,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string",
- "format": "date-time"
- }
- },
- {
- "name": "date_fin",
- "in": "query",
- "description": "Date de fin d'observation au format AAAA-MM-JJTHH:mm:00Z.",
- "required": false,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string",
- "format": "date-time"
- }
- }
- ],
- "responses": {
- "200": {
- "description": "OK (liste vide si pas de donnée correspondant à la requête)"
- },
- "400": {
- "description": "Contrôle de paramètres en erreur"
- },
- "401": {
- "description": "Non autorisé - informations d'identification non valides"
- },
- "403": {
- "description": "Accès interdit"
- },
- "404": {
- "description": "Jeu de données inexistant et/ou jeu de données indisponible"
- },
- "500": {
- "description": "Erreur interne au serveur"
- },
- "502": {
- "description": "Erreur de passerelle"
- },
- "503": {
- "description": "Service indisponible"
- },
- "504": {
- "description": "Temps trop long"
- }
- },
- "security": [
- {
- "default": []
- }
- ],
- "x-auth-type": "Application & Application User",
- "x-throttling-tier": "Unlimited"
- }
- },
- "/liste-bouees": {
- "get": {
- "tags": [
- "Produits Bouees"
- ],
- "summary": "Télécharger le fichier TEXTE de la liste des stations d'observation.",
- "description": "Renvoie la liste des stations et bouées.",
- "parameters": [
- {
- "name": "format",
- "in": "query",
- "required": true,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string",
- "enum": [
- "json",
- "csv",
- "geojson"
- ]
- }
- }
- ],
- "responses": {
- "200": {
- "description": "ok"
- },
- "404": {
- "description": "La liste est indisponible"
- }
- },
- "security": [
- {
- "default": []
- }
- ],
- "x-auth-type": "Application & Application User",
- "x-throttling-tier": "Unlimited"
- }
- },
- "/bouees": {
- "get": {
- "tags": [
- "Produits Bouees"
- ],
- "summary": "Télécharger le fichier TEXTE (CSV ou JSON ou GEOJSON) des données d'observations pour une station, à la date demandée.",
- "description": "Renvoie tous les paramètres disponibles pour la station à la date heure demandée.",
- "parameters": [
- {
- "name": "format",
- "in": "query",
- "description": "Format de retour des données (JSON ou CSV ou GEOJSON)",
- "required": true,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string",
- "enum": [
- "json",
- "csv",
- "geojson"
- ]
- }
- },
- {
- "name": "date_debut",
- "in": "query",
- "description": "Date de début d'observation au format AAAA-MM-JJTHH:mm:00Z",
- "required": false,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "date_fin",
- "in": "query",
- "description": "Date de fin d'observation au format AAAA-MM-JJTHH:mm:00Z",
- "required": false,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "id_bouees",
- "in": "query",
- "description": "Identifiant(s) de la station (séparés par des , par exemple id_station=6100001,6100002)",
- "required": false,
- "style": "form",
- "explode": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "description": "ok (liste vide si aucune donnée ne correspondent à la requête)"
- }
- },
- "security": [
- {
- "default": []
- }
- ],
- "x-auth-type": "Application & Application User",
- "x-throttling-tier": "Unlimited"
- }
- }
- },
- "components": {
- "securitySchemes": {
- "default": {
- "type": "oauth2",
- "flows": {
- "implicit": {
- "authorizationUrl": "https://public-api.meteofrance.fr/authorize",
- "scopes": {}
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Données_d’observation_swagger.json:Zone.Identifier b/Données_d’observation_swagger.json:Zone.Identifier
deleted file mode 100644
index d6c1ec6..0000000
Binary files a/Données_d’observation_swagger.json:Zone.Identifier and /dev/null differ
diff --git a/package.json b/package.json
index 0378ed9..b6aab8a 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,9 @@
"start": "node ./dist/server/entry.mjs",
"astro": "astro",
"check": "astro check",
- "typecheck": "tsc --noEmit"
+ "typecheck": "tsc --noEmit",
+ "test:e2e": "playwright test",
+ "test:e2e:local": "E2E_BASE_URL=http://localhost:4321 playwright test"
},
"dependencies": {
"@astrojs/node": "^9.2.2",
@@ -25,6 +27,7 @@
},
"devDependencies": {
"@astrojs/check": "^0.9.4",
+ "@playwright/test": "^1.60.0",
"@types/node": "^22.10.5",
"sharp": "^0.34.5",
"typescript": "^5.7.2"
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 0000000..3ea847c
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,25 @@
+import { defineConfig, devices } from '@playwright/test';
+
+// Tests E2E pour info-canicule.
+// Par défaut : target la prod live (https://info-canicule.nocleus.com).
+// Pour tester un dev server local : E2E_BASE_URL=http://localhost:4321 pnpm test:e2e
+
+const BASE = process.env.E2E_BASE_URL ?? 'https://info-canicule.nocleus.com';
+
+export default defineConfig({
+ testDir: './tests/e2e',
+ timeout: 30_000,
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 1 : 0,
+ workers: process.env.CI ? 2 : undefined,
+ reporter: [['list'], process.env.CI ? ['github'] : ['html', { open: 'never' }]],
+ use: {
+ baseURL: BASE,
+ trace: 'retain-on-failure',
+ screenshot: 'only-on-failure',
+ },
+ projects: [
+ { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
+ ],
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0a1eabc..0a8b701 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -36,6 +36,9 @@ importers:
'@astrojs/check':
specifier: ^0.9.4
version: 0.9.9(prettier@3.8.3)(typescript@5.9.3)
+ '@playwright/test':
+ specifier: ^1.60.0
+ version: 1.60.0
'@types/node':
specifier: ^22.10.5
version: 22.19.19
@@ -863,6 +866,11 @@ packages:
'@oslojs/encoding@1.1.0':
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
+ '@playwright/test@1.60.0':
+ resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==}
+ engines: {node: '>=18'}
+ hasBin: true
+
'@prisma/instrumentation@7.6.0':
resolution: {integrity: sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ==}
peerDependencies:
@@ -1681,6 +1689,11 @@ packages:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -2216,6 +2229,16 @@ packages:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
+ playwright-core@1.60.0:
+ resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.60.0:
+ resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
postcss-import@15.1.0:
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'}
@@ -3738,6 +3761,10 @@ snapshots:
'@oslojs/encoding@1.1.0': {}
+ '@playwright/test@1.60.0':
+ dependencies:
+ playwright: 1.60.0
+
'@prisma/instrumentation@7.6.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
@@ -4658,6 +4685,9 @@ snapshots:
fresh@2.0.0: {}
+ fsevents@2.3.2:
+ optional: true
+
fsevents@2.3.3:
optional: true
@@ -5394,6 +5424,14 @@ snapshots:
pirates@4.0.7: {}
+ playwright-core@1.60.0: {}
+
+ playwright@1.60.0:
+ dependencies:
+ playwright-core: 1.60.0
+ optionalDependencies:
+ fsevents: 2.3.2
+
postcss-import@15.1.0(postcss@8.5.15):
dependencies:
postcss: 8.5.15
diff --git a/src/components/TemperatureChartInteractive.astro b/src/components/TemperatureChartInteractive.astro
new file mode 100644
index 0000000..f41fd02
--- /dev/null
+++ b/src/components/TemperatureChartInteractive.astro
@@ -0,0 +1,264 @@
+---
+import type { DayObservation } from '../lib/climato';
+import type { HourlyObservation, HourlySeries } from '../lib/observations';
+import type { MonthNormale } from '../lib/normales';
+
+interface Props {
+ hourly?: HourlySeries | null; // 24h
+ days7: DayObservation[];
+ days30: DayObservation[];
+ normale: MonthNormale | null;
+ stationLabel?: string | null;
+}
+
+const { hourly, days7, days30, normale, stationLabel } = Astro.props;
+
+// Sérialiser les 3 séries pour le JS client (toggle + hover).
+const serialize = {
+ hourly: hourly?.observations.map((o) => ({ t: o.time, tx: o.t, tn: null })) ?? [],
+ days7: days7.map((d) => ({ t: d.date, tx: d.tx, tn: d.tn })),
+ days30: days30.map((d) => ({ t: d.date, tx: d.tx, tn: d.tn })),
+ normale: normale ? { tx: normale.tx, tn: normale.tn } : null,
+};
+
+const hasHourly = (hourly?.observations.length ?? 0) > 0;
+const defaultPeriod = hasHourly ? '24h' : '7j';
+---
+
+
- Source : Météo France — données climatologiques de base quotidiennes, agrégées par moyenne - sur toutes les stations du département. Donnée brute, contrôle qualité Météo France. - Normales calculées sur 1991-2020 (référence WMO). + Sources : Météo France — observations SYNOP horaires (onglet 24 h) et données + climatologiques de base quotidiennes (7 j / 30 j), agrégées par moyenne sur les + stations du département. Normales calculées sur 1991-2020 (référence WMO).
diff --git a/src/pages/embed/dept/[code].astro b/src/pages/embed/dept/[code].astro new file mode 100644 index 0000000..d8c3c23 --- /dev/null +++ b/src/pages/embed/dept/[code].astro @@ -0,0 +1,79 @@ +--- +import Embed from '../../../layouts/Embed.astro'; +import VigilanceChip from '../../../components/VigilanceChip.astro'; +import { getVigilanceSnapshot, alertsForDepartement, maxColorByDepartement } from '../../../lib/vigilance'; +import { getDepartement, isDrom } from '../../../lib/departements'; +import { COLORS } from '../../../lib/phenomena'; + +export const prerender = false; + +const { code } = Astro.params; +const dept = code ? getDepartement(code.toUpperCase()) : undefined; +if (!dept) { + return new Response('not found', { status: 404 }); +} + +let alerts = [] as Awaited+ Intégrez la Vigilance Météo France de n'importe quel département sur votre site en une + ligne d'iframe. Gratuit, libre de réutilisation (Licence Ouverte 2.0). +
+{snippet}
+
+ Remplace {sample.code} par le code de ton
+ département (01-95, 2A, 2B, 971-976). La liste complète :
+ page conseils.
+
width et height.
+ Le widget s'adapte aux petits écrans grâce à max-width:100%.
+
+ Si tu préfères afficher la Vigilance avec ton propre design, l'endpoint
+ /api/vigilance/dept/[code]
+ renvoie un JSON CORS *, prêt à parser :
+
fetch('{baseUrl}/api/vigilance/dept/{sample.code}')
+ .then(r => r.json())
+ .then(data => console.log(data.today)); // [{ phenomenon, color, ... }]
+