feat: tooltip carte + tri/group alertes + safelist couleurs + legal Nocleus
Some checks are pending
Deploy info-canicule / deploy (push) Waiting to run

- FranceMap : tooltip riche au hover (HTML overlay), liste les phénomènes
  + niveaux du département. Touch-friendly (1er tap = preview, 2e = clic).
- index.astro : layout refactored, carte toujours visible full-width centrée,
  liste par région en details collapsible sous (plus de side-by-side cassé sur PC).
- Alertes actives groupées par département, triées par numéro asc (2A/2B après 19).
- Tailwind safelist vigilance-chip-{1..4} : les classes générées dynamiquement
  n'étaient pas captées par le scanner statique → CSS absent en prod.
- Mentions légales : distinction explicite entre Nocleus (micro-entreprise
  commerciale) et Info Canicule (projet perso non lucratif, hors cadre pro).
- Liens code source git.nocleus.com retirés partout (autres repos privés y sont
  visibles) → code "disponible sur demande" par mail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian 2026-05-25 20:23:02 +02:00
parent 58053b72ed
commit 89e48c18e4
8 changed files with 287 additions and 81 deletions

View file

@ -77,9 +77,9 @@ export const prerender = false;
<h2>Code source</h2>
<p>
Le site est entièrement open source. Le code est disponible sur
<a href="https://git.nocleus.com/florian/info-canicule" rel="noopener">git.nocleus.com/florian/info-canicule</a>
(instance Forgejo personnelle). Contributions, signalements de bugs et améliorations bienvenus.
Le code est disponible sur demande à
<a href="mailto:florian@nocleus.com">florian@nocleus.com</a> (contributions, signalements de
bugs et améliorations bienvenus).
</p>
<h2>Contact</h2>

View file

@ -206,10 +206,8 @@ const INFRA = [
</div>
<p class="text-sm text-slate-500">
Code source du site :
<a href="https://git.nocleus.com/florian/info-canicule" rel="noopener">
git.nocleus.com/florian/info-canicule
</a>
Code source du site disponible sur demande à
<a href="mailto:florian@nocleus.com">florian@nocleus.com</a>.
</p>
</section>
</Base>

View file

@ -6,7 +6,7 @@ import VigilanceLegend from '../components/VigilanceLegend.astro';
import VigilanceChip from '../components/VigilanceChip.astro';
import { getVigilanceSnapshot, maxColorByDepartement, activeAlerts } from '../lib/vigilance';
import { getDepartement } from '../lib/departements';
import { PHENOMENA, COLORS } from '../lib/phenomena';
import type { VigilanceAlert } from '../lib/vigilance';
export const prerender = false;
@ -20,6 +20,29 @@ try {
const colorsByDept = snapshot ? maxColorByDepartement(snapshot, 'J') : new Map();
const alertsToday = snapshot ? activeAlerts(snapshot, 2) : [];
// Group all today's alerts (any level) by department, for tooltip + active alerts list.
const alertsByDept = new Map<string, VigilanceAlert[]>();
if (snapshot) {
for (const a of snapshot.alerts) {
if (a.echeance !== 'J') continue;
if (a.colorId < 2) continue;
const list = alertsByDept.get(a.departement) ?? [];
list.push(a);
alertsByDept.set(a.departement, list);
}
}
// Tri des départements actifs par code (2A/2B inséré après 19).
function deptSortKey(code: string): string {
if (code === '2A') return '19.1';
if (code === '2B') return '19.2';
return code;
}
const activeDeptCodes = [...alertsByDept.keys()].sort((a, b) =>
deptSortKey(a).localeCompare(deptSortKey(b), 'en', { numeric: true }),
);
const canicule = alertsToday.filter((a) => a.phenomenonId === 6).length;
const orages = alertsToday.filter((a) => a.phenomenonId === 3).length;
const orange = alertsToday.filter((a) => a.colorId >= 3).length;
@ -82,44 +105,54 @@ const productDate = snapshot?.productDatetime
<h2 class="text-xl font-semibold text-slate-900">Niveau par département (aujourd'hui)</h2>
<VigilanceLegend />
</div>
<div class="grid gap-8 lg:grid-cols-[2fr_1fr]">
<div class="flex justify-center">
<FranceMap colorsByDept={colorsByDept} />
</div>
<details class="rounded border border-slate-200 bg-white p-4">
<summary class="cursor-pointer font-medium text-slate-700">
Vue par région (liste)
</summary>
<div class="mt-4">
<DepartementGrid colorsByDept={colorsByDept} />
</div>
</details>
<p class="mb-3 text-sm text-slate-500">
Survolez un département pour voir le détail des alertes, cliquez pour la page complète.
</p>
<div class="flex justify-center">
<FranceMap colorsByDept={colorsByDept} alertsByDept={alertsByDept} />
</div>
<details class="mt-6 rounded border border-slate-200 bg-white p-4">
<summary class="cursor-pointer font-medium text-slate-700">
Vue par région (liste)
</summary>
<div class="mt-4">
<DepartementGrid colorsByDept={colorsByDept} />
</div>
</details>
</section>
)
}
{
!error && alertsToday.length > 0 && (
!error && activeDeptCodes.length > 0 && (
<section class="border-t border-slate-200 bg-white">
<div class="container-tight py-8">
<h2 class="mb-4 text-xl font-semibold text-slate-900">Alertes actives</h2>
<h2 class="mb-4 text-xl font-semibold text-slate-900">
Départements en alerte ({activeDeptCodes.length})
</h2>
<p class="mb-4 text-sm text-slate-500">
Triés par numéro de département. Un département peut cumuler plusieurs phénomènes (ex: canicule + orages).
</p>
<ul class="space-y-2">
{alertsToday
.sort((a, b) => b.colorId - a.colorId)
.slice(0, 50)
.map((a) => {
const dept = getDepartement(a.departement);
if (!dept) return null;
return (
<li class="flex flex-wrap items-center justify-between gap-3 rounded border border-slate-200 px-3 py-2">
<a href={`/departement/${a.departement}`} class="font-medium no-underline">
{dept.name} ({a.departement})
{activeDeptCodes.map((code) => {
const dept = getDepartement(code);
if (!dept) return null;
const alerts = (alertsByDept.get(code) ?? []).slice().sort((a, b) => b.colorId - a.colorId);
return (
<li class="rounded border border-slate-200 px-3 py-2">
<div class="flex flex-wrap items-center justify-between gap-3">
<a href={`/departement/${code}`} class="font-medium no-underline">
<span class="font-mono text-slate-500">{code}</span> · {dept.name}
</a>
<VigilanceChip colorId={a.colorId} phenomenonId={a.phenomenonId} />
</li>
);
})}
<div class="flex flex-wrap gap-1.5">
{alerts.map((a) => (
<VigilanceChip colorId={a.colorId} phenomenonId={a.phenomenonId} />
))}
</div>
</div>
</li>
);
})}
</ul>
</div>
</section>

View file

@ -18,16 +18,22 @@ export const prerender = false;
<section class="container-tight py-8 prose prose-slate max-w-none">
<h2>Éditeur du site</h2>
<p>
Site édité à titre personnel, <strong>sans but lucratif</strong>, par :
Site édité <strong>à titre personnel, sans but lucratif</strong>, par :
</p>
<ul>
<li>Florian Bouchet — développeur indépendant</li>
<li>Florian Bouchet — personne physique</li>
<li>Contact : <a href="mailto:florian@nocleus.com">florian@nocleus.com</a></li>
<li>Statut : personne physique (pas d'entreprise immatriculée, pas d'association)</li>
</ul>
<p>
Le site n'a aucune vocation commerciale. Aucun chiffre d'affaires, aucune publicité, aucune
collecte de données à des fins de monétisation.
Info Canicule n'a <strong>aucune vocation commerciale</strong> : aucune publicité, aucun chiffre
d'affaires, aucune collecte de données à des fins de monétisation.
</p>
<p>
<strong>Distinction importante</strong> : l'éditeur exerce par ailleurs une activité de
développement sous le statut de micro-entreprise « <em>Nocleus</em> ». Cette activité commerciale
est <strong>totalement indépendante</strong> du site Info Canicule, qui est édité hors cadre
professionnel, sur fonds propres, à titre purement personnel et bénévole. Aucun service de la
micro-entreprise n'est financé, promu ou rattaché à ce site.
</p>
<h2>Hébergement</h2>
@ -100,10 +106,10 @@ export const prerender = false;
<h2>Propriété intellectuelle</h2>
<p>
Le code source du site est sous licence libre (
<a href="https://git.nocleus.com/florian/info-canicule" rel="noopener">repo Forgejo</a>
). Les données affichées sont sous Licence Ouverte 2.0 et réutilisables librement, y compris
commercialement, à condition de citer Météo France et la licence.
Le code source du site est disponible sur demande à
<a href="mailto:florian@nocleus.com">florian@nocleus.com</a>. Les données affichées sont sous
Licence Ouverte 2.0 et réutilisables librement, y compris commercialement, à condition de citer
Météo France et la licence.
</p>
<p>
L'endpoint <code>/api/vigilance</code> diffuse le snapshot courant en JSON (CORS *), pour

View file

@ -52,8 +52,7 @@ export const prerender = false;
<ul>
<li>
<strong>Signaler un bug ou une typo</strong> : par mail à
<a href="mailto:florian@nocleus.com">florian@nocleus.com</a> ou via une issue sur le
<a href="https://git.nocleus.com/florian/info-canicule" rel="noopener">repo Forgejo</a>.
<a href="mailto:florian@nocleus.com">florian@nocleus.com</a>.
</li>
<li>
<strong>Partager le site</strong> autour de vous, en particulier auprès de personnes fragiles