fix(dept): vert ≠ vigilance, conseils sur chaque alerte active
Some checks failed
Deploy info-canicule / deploy (push) Failing after 5s

Le flux Météo France renvoie parfois des phénomènes explicitement au vert
(colorId 1) : ils étaient traités comme des alertes, d'où un « Que faire
maintenant ? » affiché sur un phénomène vert et un dropdown limité aux
phénomènes absents du flux.

- "Active" = vigilance jaune ou plus (colorId >= 2) ; le vert n'est jamais
  une alerte
- Tout au vert → un seul encadré « Aucune vigilance active »
- Dropdown regroupe désormais tous les phénomènes au vert (présents au vert
  dans le flux + absents)
- Une vigilance présente → bloc complet « Que faire maintenant ? » sur
  chacune des alertes actives (au lieu de la première seulement)
- Tableau Aujourd'hui vs Demain et suite : inchangés

Au passage, fix typage FranceMap : Map par défaut typée pour ne pas perdre
VigilanceAlert (5 erreurs astro check), imports inutilisés retirés.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian 2026-06-01 16:56:04 +02:00
parent 9db5d4c204
commit c170bbca7d
2 changed files with 55 additions and 67 deletions

View file

@ -1,7 +1,7 @@
---
import franceMap from '../data/france-map.json';
import { COLORS, COLOR_LABEL, PHENOMENA } from '../lib/phenomena';
import type { ColorId, PhenomenonId } from '../lib/phenomena';
import { COLORS, PHENOMENA } from '../lib/phenomena';
import type { ColorId } from '../lib/phenomena';
import type { VigilanceAlert } from '../lib/vigilance';
import { getDepartement } from '../lib/departements';
@ -10,7 +10,7 @@ interface Props {
alertsByDept?: Map<string, VigilanceAlert[]>;
}
const { colorsByDept, alertsByDept = new Map() } = Astro.props;
const { colorsByDept, alertsByDept = new Map<string, VigilanceAlert[]>() } = Astro.props;
const entries = Object.entries(franceMap.paths) as [string, { d: string; name: string }][];
// Construire un objet { code: { name, color, alerts: [{phenLabel, colorName, colorId}] } }

View file

@ -147,16 +147,20 @@ const j1Available = snapshot ? (ech === 'J' && hasJ1Period(snapshot)) : false;
const today = snapshot ? alertsForDepartement(snapshot, dept.code, ech) : [];
const tomorrow = j1Available ? alertsForDepartement(snapshot!, dept.code, 'J1') : [];
// Sort by severity desc, pick top alert for the hero block
// Sort by severity desc. "Active" = vigilance jaune ou plus (colorId >= 2).
// Le flux MF renvoie parfois des phénomènes explicitement au vert (colorId 1) —
// ce ne sont PAS des alertes : ils rejoignent le dropdown "tous au vert", au même
// titre que les phénomènes absents du flux. topAlert = la plus sévère des actives
// (sert aux numéros d'urgence + carte "Kit" en bas de page).
const sortedToday = [...today].sort((a, b) => b.colorId - a.colorId);
const topAlert = sortedToday[0] ?? null;
const otherAlerts = sortedToday.slice(1);
const activeToday = sortedToday.filter((a) => a.colorId >= 2);
const topAlert = activeToday[0] ?? null;
const adviceFor = topAlert ? ADVICE[topAlert.phenomenonId] : null;
// First 3 action items as quick bullets inside the hero block
const quickActions = adviceFor
? adviceFor.blocks.flatMap((b) => b.items).slice(0, 3)
: [];
// First 3 action items, par phénomène, pour le bloc "Que faire maintenant ?"
function quickActionsFor(phenId: PhenomenonId): string[] {
const advice = ADVICE[phenId];
return advice ? advice.blocks.flatMap((b) => b.items).slice(0, 3) : [];
}
// Per-phenomenon color maps for the comparison table
const PHENOM_IDS: PhenomenonId[] = [1, 2, 3, 5, 6, 8, 9];
@ -179,9 +183,10 @@ const comparisonRows = PHENOM_IDS.map((id) => ({
changed: j1Available && (todayByPhenom.get(id) ?? 1) !== (tomorrowByPhenom.get(id) ?? 1),
}));
// Phenomena with no active alert today (green)
const activePhenomIds = new Set(today.map((a) => a.phenomenonId));
const inactivePhenomIds = PHENOM_IDS.filter((id) => !activePhenomIds.has(id));
// Phénomènes au vert aujourd'hui = ceux sans alerte active (jaune+). Inclut aussi
// les phénomènes absents du flux MF (réputés verts). Tous regroupés dans le dropdown.
const activePhenomIds = new Set(activeToday.map((a) => a.phenomenonId));
const greenPhenomIds = PHENOM_IDS.filter((id) => !activePhenomIds.has(id));
const productDate = snapshot?.productDatetime
? new Date(snapshot.productDatetime).toLocaleString('fr-FR', {
@ -204,10 +209,6 @@ const tomorrowLabel = tomorrow[0] ? labelDate(tomorrow[0].beginTime) : '';
// Glyphs matching the pill levels
const GLYPHS: Record<ColorId, string> = { 1: '●', 2: '▲', 3: '◆', 4: '■' };
// Pre-computed display values for the hero alert block
const topColorName = topAlert ? COLORS[topAlert.colorId].name : null;
const topPhen = topAlert ? PHENOMENA[topAlert.phenomenonId] : null;
---
<Base
@ -266,80 +267,67 @@ const topPhen = topAlert ? PHENOMENA[topAlert.phenomenonId] : null;
{!drom && !error && (
<>
{/* All-green state */}
{sortedToday.length === 0 && (
{/* All-green state — aucune vigilance active (jaune+) */}
{activeToday.length === 0 && (
<div class="v-block v-vert" style="margin-bottom: 24px;">
<h2 style="color: var(--v-vert-ink); font-size: clamp(1.2rem, 1rem + 0.8vw, 1.6rem);">
● Aucune vigilance active {todayLabel ? `pour ${todayLabel}` : "aujourd'hui"}
</h2>
<p style="color: var(--v-vert-ink); margin-top: 10px; line-height: 1.5;">
Tous les phénomènes sont au vert pour {dept.name}. Restez attentif aux mises à jour Météo France.
Tous les phénomènes sont au vert pour {dept.name} — rien de particulier à signaler.
Restez attentif aux mises à jour Météo France.
</p>
</div>
)}
{/* Hero — top alert */}
{topAlert && topColorName && topPhen && (
<div class={`v-block v-${topColorName}`} style="margin-bottom: 24px;">
{/* Active alerts (jaune+) — bloc complet "Que faire maintenant ?" sur chacune */}
{activeToday.map((a) => {
const colorName = COLORS[a.colorId].name;
const phen = PHENOMENA[a.phenomenonId];
const advice = ADVICE[a.phenomenonId];
const quickActions = quickActionsFor(a.phenomenonId);
return (
<div class={`v-block v-${colorName}`} style="margin-bottom: 24px;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px; flex-wrap: wrap;">
<span style="font-size: 2rem; line-height: 1;" aria-hidden="true">{topPhen.emoji}</span>
<h2 style={`color: var(--v-${topColorName}-ink); font-size: clamp(1.2rem, 1rem + 0.8vw, 1.6rem);`}>
{GLYPHS[topAlert.colorId]} {topPhen.label} — {COLOR_LABEL[topAlert.colorId]}
<span style="font-size: 2rem; line-height: 1;" aria-hidden="true">{phen.emoji}</span>
<h2 style={`color: var(--v-${colorName}-ink); font-size: clamp(1.2rem, 1rem + 0.8vw, 1.6rem);`}>
{GLYPHS[a.colorId]} {phen.label} — {COLOR_LABEL[a.colorId]}
</h2>
</div>
{adviceFor && (
{advice && (
<div style="margin-top: 4px; padding: 16px 20px; background: var(--paper-2); border-radius: var(--r-md); color: var(--ink);">
<strong style="font-size: 0.95rem;">Que faire maintenant ?</strong>
<ul style="margin-top: 10px; padding-left: 18px; color: var(--ink-2); line-height: 1.6;">
{quickActions.map((a) => <li>{a}</li>)}
{quickActions.map((item) => <li>{item}</li>)}
</ul>
<a
href={`/conseils/${topPhen.slug}`}
href={`/conseils/${phen.slug}`}
style="display: inline-flex; align-items: center; gap: 6px; margin-top: 12px; font-size: 0.88rem; font-weight: 600; color: var(--brand-deep); text-decoration: none;"
>
Voir le kit complet {topPhen.label} →
Voir le kit complet {phen.label} →
</a>
</div>
)}
<p style={`color: var(--v-${topColorName}-ink); margin-top: 14px; font-size: 0.85rem; opacity: 0.85;`}>
Valide du {fmtTime(topAlert.beginTime)} au {fmtTime(topAlert.endTime)} — heure de Paris.
</p>
</div>
)}
{/* Other active alerts */}
{otherAlerts.map((a) => {
const colorName = COLORS[a.colorId].name;
const phen = PHENOMENA[a.phenomenonId];
return (
<div class={`v-block v-${colorName}`} style="margin-bottom: 12px; padding: 16px;">
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 1.3rem;" aria-hidden="true">{phen.emoji}</span>
<strong style={`color: var(--v-${colorName}-ink);`}>{phen.label}</strong>
</div>
<VigilanceChip colorId={a.colorId} showLevel />
</div>
<p style={`color: var(--v-${colorName}-ink); margin-top: 8px; font-size: 0.82rem; opacity: 0.8;`}>
Du {fmtTime(a.beginTime)} au {fmtTime(a.endTime)}
<p style={`color: var(--v-${colorName}-ink); margin-top: 14px; font-size: 0.85rem; opacity: 0.85;`}>
Valide du {fmtTime(a.beginTime)} au {fmtTime(a.endTime)} — heure de Paris.
</p>
</div>
);
})}
{/* Collapsible: inactive phenomena (all green) */}
{inactivePhenomIds.length > 0 && (
{/* Collapsible: phénomènes au vert (sans vigilance) */}
{greenPhenomIds.length > 0 && (
<details class="ic-card" style="margin-bottom: 28px; padding: 16px;">
<summary style="cursor: pointer; font-weight: 600; color: var(--ink-2); display: flex; align-items: center; justify-content: space-between; list-style: none;">
<span>
{inactivePhenomIds.length} phénomène{inactivePhenomIds.length > 1 ? 's' : ''} — tous au vert
{greenPhenomIds.length} phénomène{greenPhenomIds.length > 1 ? 's' : ''} au vert — aucune vigilance
</span>
<span aria-hidden="true" style="color: var(--ink-mute); font-size: 0.8rem;">▼</span>
</summary>
<div class="grid gap-2" style="grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); margin-top: 14px;">
{inactivePhenomIds.map((id) => (
{greenPhenomIds.map((id) => (
<div class="v-block v-vert" style="padding: 10px 14px; font-size: 0.9rem; display: flex; align-items: center; gap: 8px;">
<span aria-hidden="true">{PHENOMENA[id].emoji}</span>
<span style="color: var(--v-vert-ink);">{PHENOMENA[id].label}</span>