feat: graph T° interactif + widget iframe + MF auth + E2E Playwright
Some checks are pending
Deploy info-canicule / deploy (push) Waiting to run
Some checks are pending
Deploy info-canicule / deploy (push) Waiting to run
Graph T° (TemperatureChartInteractive.astro) : - Onglets 24 h / 7 j / 30 j (toggle JS, séries serialisées au SSR) - Hover vertical line + tooltip valeurs - Overlay normales mois en pointillé (TX orange, TN bleu) - Onglet 24 h dispo seulement si l'API MF a répondu (best-effort) Météo France OAuth2 (lib/meteofrance-auth.ts + observations.ts) : - client_credentials avec refresh auto, cache token Valkey - Fallback METEOFRANCE_STATIC_TOKEN pour debug - /synop endpoint pour 24h horaires par station SYNOP du dept - Mapping dept → station SYNOP la plus proche (src/data/stations-synop.json) - En attente de creds : SDK skip silencieusement, l'onglet 24h n'apparaît pas Widget iframe (/embed/dept/[code] + /embed doc) : - Layout minimal sans header/footer global - Réutilisable via iframe avec une ligne - Page /embed avec snippet copier-coller + aperçu live Tests E2E Playwright (tests/e2e/) : - home (carte 96 paths, tooltip dept, navigation) - api (health, vigilance, vigilance/dept) - departement (tabs période, DROM notice, 404) - static pages (a-propos, mentions, dependances, soutenir, conseils, embed) - embed widget (rendu minimal, headers X-Frame OK) - 20+ tests, run via pnpm test:e2e (live) ou test:e2e:local Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9cfd4f8385
commit
2c4d91ce2f
20 changed files with 922 additions and 467 deletions
|
|
@ -7,8 +7,9 @@ import { getDepartement, isDrom } from '../../lib/departements';
|
|||
import { PHENOMENA, COLOR_LABEL } from '../../lib/phenomena';
|
||||
import { ADVICE, EMERGENCY_NUMBERS } from '../../lib/advice';
|
||||
import { getClimatoForDepartement } from '../../lib/climato';
|
||||
import { computeAnomaly } from '../../lib/normales';
|
||||
import ClimatoChart from '../../components/ClimatoChart.astro';
|
||||
import { computeAnomaly, normaleForMonth } from '../../lib/normales';
|
||||
import { getHourlyForDepartement } from '../../lib/observations';
|
||||
import TemperatureChartInteractive from '../../components/TemperatureChartInteractive.astro';
|
||||
import AnomalyBadge from '../../components/AnomalyBadge.astro';
|
||||
|
||||
export const prerender = false;
|
||||
|
|
@ -34,17 +35,32 @@ if (!drom) {
|
|||
|
||||
let climato = null;
|
||||
let anomaly = null;
|
||||
let hourly = null;
|
||||
let normale = null;
|
||||
if (!drom) {
|
||||
try {
|
||||
climato = await getClimatoForDepartement(dept.code);
|
||||
if (climato?.days?.length) {
|
||||
anomaly = await computeAnomaly(dept.code, climato.days);
|
||||
// Mois représentatif = mois du dernier jour climato dispo
|
||||
const lastDate = climato.days[climato.days.length - 1].date;
|
||||
normale = await normaleForMonth(dept.code, parseInt(lastDate.slice(5, 7), 10));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('climato fetch failed for', dept.code, (e as Error).message);
|
||||
}
|
||||
// Hourly est best-effort : si MF API down ou pas de creds, on n'affiche juste pas l'onglet 24h
|
||||
try {
|
||||
hourly = await getHourlyForDepartement(dept.code, 24);
|
||||
} catch (e) {
|
||||
console.warn('hourly fetch failed for', dept.code, (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
const last7 = climato?.days?.slice(-7) ?? [];
|
||||
const last30 = climato?.days?.slice(-30) ?? [];
|
||||
const stationLabel = hourly ? `${hourly.stationName} (${hourly.distKm} km)` : null;
|
||||
|
||||
const today = snapshot ? alertsForDepartement(snapshot, dept.code, 'J') : [];
|
||||
const tomorrow = snapshot ? alertsForDepartement(snapshot, dept.code, 'J1') : [];
|
||||
const highest = today[0];
|
||||
|
|
@ -158,11 +174,17 @@ const adviceFor = highest && ADVICE[highest.phenomenonId];
|
|||
<AnomalyBadge anomaly={anomaly} />
|
||||
</div>
|
||||
)}
|
||||
<ClimatoChart days={climato.days} />
|
||||
<TemperatureChartInteractive
|
||||
hourly={hourly}
|
||||
days7={last7}
|
||||
days30={last30}
|
||||
normale={normale}
|
||||
stationLabel={stationLabel}
|
||||
/>
|
||||
<p class="mt-2 text-xs text-slate-500">
|
||||
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).
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
79
src/pages/embed/dept/[code].astro
Normal file
79
src/pages/embed/dept/[code].astro
Normal file
|
|
@ -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<ReturnType<typeof alertsForDepartement>>;
|
||||
let maxColor = 1;
|
||||
let error: string | null = null;
|
||||
if (!isDrom(dept.code)) {
|
||||
try {
|
||||
const snap = await getVigilanceSnapshot();
|
||||
alerts = alertsForDepartement(snap, dept.code, 'J').sort((a, b) => b.colorId - a.colorId);
|
||||
maxColor = maxColorByDepartement(snap, 'J').get(dept.code) ?? 1;
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
}
|
||||
}
|
||||
|
||||
const color = COLORS[maxColor as 1 | 2 | 3 | 4];
|
||||
const siteUrl = `https://info-canicule.nocleus.com/departement/${dept.code}`;
|
||||
---
|
||||
|
||||
<Embed title={`Vigilance ${dept.name} — Info Canicule`}>
|
||||
<article class="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<header class="flex items-center justify-between gap-3 border-b border-slate-100 pb-2">
|
||||
<div>
|
||||
<h1 class="text-base font-semibold text-slate-900">{dept.name}</h1>
|
||||
<p class="text-xs text-slate-500">Vigilance Météo France · dept {dept.code}</p>
|
||||
</div>
|
||||
<div
|
||||
class="rounded px-2 py-1 text-xs font-semibold"
|
||||
style={`background:${color.hex}; color:${maxColor >= 3 ? '#fff' : '#1e293b'}`}
|
||||
>
|
||||
{color.name.toUpperCase()}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{error && (
|
||||
<p class="mt-2 text-xs text-red-700">Données indisponibles.</p>
|
||||
)}
|
||||
|
||||
{isDrom(dept.code) && (
|
||||
<p class="mt-2 text-xs text-amber-800">
|
||||
Vigilance Outre-mer non couverte par cette source.
|
||||
<a href="https://vigilance.meteofrance.fr/" rel="noopener" class="text-canicule-700">Source officielle ↗</a>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!error && !isDrom(dept.code) && alerts.length === 0 && (
|
||||
<p class="mt-2 text-xs text-green-700">Aucune vigilance particulière aujourd'hui.</p>
|
||||
)}
|
||||
|
||||
{alerts.length > 0 && (
|
||||
<ul class="mt-2 space-y-1">
|
||||
{alerts.map((a) => (
|
||||
<li>
|
||||
<VigilanceChip colorId={a.colorId} phenomenonId={a.phenomenonId} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<footer class="mt-3 border-t border-slate-100 pt-2 text-right text-xs">
|
||||
<a href={siteUrl} target="_top" rel="noopener" class="text-canicule-700">
|
||||
Détail sur info-canicule.nocleus.com →
|
||||
</a>
|
||||
</footer>
|
||||
</article>
|
||||
</Embed>
|
||||
94
src/pages/embed/index.astro
Normal file
94
src/pages/embed/index.astro
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
---
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import { DEPARTEMENTS } from '../../lib/departements';
|
||||
|
||||
export const prerender = false;
|
||||
const sample = DEPARTEMENTS.find((d) => d.code === '13')!;
|
||||
const baseUrl = 'https://info-canicule.nocleus.com';
|
||||
const snippet = `<iframe
|
||||
src="${baseUrl}/embed/dept/${sample.code}"
|
||||
width="380" height="260"
|
||||
style="border:0; max-width:100%"
|
||||
loading="lazy"
|
||||
title="Vigilance ${sample.name} — Info Canicule"
|
||||
></iframe>`;
|
||||
---
|
||||
|
||||
<Base
|
||||
title="Widget intégrable — Info Canicule"
|
||||
description="Intégrez la Vigilance Météo France de n'importe quel département sur votre site en une ligne d'iframe."
|
||||
>
|
||||
<section class="bg-gradient-to-b from-canicule-50 to-white">
|
||||
<div class="container-tight py-10">
|
||||
<h1 class="text-3xl font-bold sm:text-4xl">Widget intégrable</h1>
|
||||
<p class="mt-2 max-w-2xl text-slate-600">
|
||||
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).
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container-tight py-8 space-y-8">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Aperçu live ({sample.name})</h2>
|
||||
<div class="mt-3 inline-block rounded border border-slate-200 bg-slate-100 p-4">
|
||||
<iframe
|
||||
src={`/embed/dept/${sample.code}`}
|
||||
width="380"
|
||||
height="260"
|
||||
style="border:0; max-width:100%; background:transparent"
|
||||
loading="lazy"
|
||||
title={`Vigilance ${sample.name}`}
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Code à copier-coller</h2>
|
||||
<pre class="mt-3 overflow-x-auto rounded bg-slate-900 p-4 text-xs text-slate-100"><code>{snippet}</code></pre>
|
||||
<p class="mt-2 text-sm text-slate-600">
|
||||
Remplace <code class="rounded bg-slate-100 px-1">{sample.code}</code> par le code de ton
|
||||
département (01-95, 2A, 2B, 971-976). La liste complète :
|
||||
<a href="/conseils">page conseils</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Personnalisation</h2>
|
||||
<ul class="mt-2 list-inside list-disc space-y-1 text-sm text-slate-700">
|
||||
<li>
|
||||
<strong>Dimensions</strong> : ajustables via <code>width</code> et <code>height</code>.
|
||||
Le widget s'adapte aux petits écrans grâce à <code>max-width:100%</code>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Fond transparent</strong> : pose-le sur n'importe quelle couleur de page.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Rafraîchissement</strong> : la page se met à jour automatiquement à chaque
|
||||
rechargement (cache 15 min côté serveur).
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">API JSON brute (alternative)</h2>
|
||||
<p class="mt-2 text-sm text-slate-700">
|
||||
Si tu préfères afficher la Vigilance avec ton propre design, l'endpoint
|
||||
<code class="rounded bg-slate-100 px-1">/api/vigilance/dept/[code]</code>
|
||||
renvoie un JSON CORS *, prêt à parser :
|
||||
</p>
|
||||
<pre class="mt-2 overflow-x-auto rounded bg-slate-900 p-4 text-xs text-slate-100"><code>fetch('{baseUrl}/api/vigilance/dept/{sample.code}')
|
||||
.then(r => r.json())
|
||||
.then(data => console.log(data.today)); // [{ phenomenon, color, ... }]</code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Conditions de réutilisation</h2>
|
||||
<ul class="mt-2 list-inside list-disc space-y-1 text-sm text-slate-700">
|
||||
<li>Données sous <a href="https://www.etalab.gouv.fr/licence-ouverte-open-licence/" rel="noopener">Licence Ouverte 2.0</a> (mention obligatoire de la source : Météo France).</li>
|
||||
<li>Ce service ne remplace pas la source officielle. En cas d'urgence, suivre <a href="https://vigilance.meteofrance.fr/" rel="noopener">vigilance.meteofrance.fr</a> et appeler le 112.</li>
|
||||
<li>Pas de garantie de disponibilité. Pour un usage critique, contractualiser directement avec Météo France.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
Loading…
Add table
Add a link
Reference in a new issue