import { useState, useEffect } from 'react'; // ─── Types ──────────────────────────────────────────────────────────────────── interface HourlyEntry { hour: number; hits: number; unique_ips: number; max_rps: number; } interface TopHost { host: string; total_hits: number; unique_ips: number; unique_ja4s: number; hourly_hits: number[]; } interface HeatmapMatrix { hosts: string[]; matrix: number[][]; } // ─── Helpers ────────────────────────────────────────────────────────────────── function formatNumber(n: number): string { return n.toLocaleString('fr-FR'); } function heatmapCellStyle(value: number, maxValue: number): React.CSSProperties { if (maxValue === 0 || value === 0) return { backgroundColor: 'transparent' }; const ratio = value / maxValue; if (ratio >= 0.75) return { backgroundColor: 'rgba(239, 68, 68, 0.85)' }; if (ratio >= 0.5) return { backgroundColor: 'rgba(168, 85, 247, 0.7)' }; if (ratio >= 0.25) return { backgroundColor: 'rgba(59, 130, 246, 0.6)' }; if (ratio >= 0.05) return { backgroundColor: 'rgba(96, 165, 250, 0.35)' }; return { backgroundColor: 'rgba(147, 197, 253, 0.15)' }; } // ─── Sub-components ─────────────────────────────────────────────────────────── function StatCard({ label, value, accent }: { label: string; value: string | number; accent?: string }) { return (
{label} {value}
); } function LoadingSpinner() { return (
); } function ErrorMessage({ message }: { message: string }) { return (
⚠️ {message}
); } // Mini sparkline for a host row function Sparkline({ data }: { data: number[] }) { const max = Math.max(...data, 1); return (
{data.map((v, i) => { const pct = (v / max) * 100; return (
= 70 ? 'bg-threat-critical' : pct >= 30 ? 'bg-threat-medium' : 'bg-accent-primary/50'}`} /> ); })}
); } // ─── Main Component ─────────────────────────────────────────────────────────── export function HeatmapView() { const [hourly, setHourly] = useState([]); const [hourlyLoading, setHourlyLoading] = useState(true); const [hourlyError, setHourlyError] = useState(null); const [topHosts, setTopHosts] = useState([]); const [topHostsLoading, setTopHostsLoading] = useState(true); const [topHostsError, setTopHostsError] = useState(null); const [matrixData, setMatrixData] = useState(null); const [matrixLoading, setMatrixLoading] = useState(true); const [matrixError, setMatrixError] = useState(null); useEffect(() => { const fetchHourly = async () => { try { const res = await fetch('/api/heatmap/hourly'); if (!res.ok) throw new Error('Erreur chargement courbe horaire'); const data: { hours: HourlyEntry[] } = await res.json(); setHourly(data.hours ?? []); } catch (err) { setHourlyError(err instanceof Error ? err.message : 'Erreur inconnue'); } finally { setHourlyLoading(false); } }; const fetchTopHosts = async () => { try { const res = await fetch('/api/heatmap/top-hosts?limit=20'); if (!res.ok) throw new Error('Erreur chargement top hosts'); const data: { items: TopHost[] } = await res.json(); setTopHosts(data.items ?? []); } catch (err) { setTopHostsError(err instanceof Error ? err.message : 'Erreur inconnue'); } finally { setTopHostsLoading(false); } }; const fetchMatrix = async () => { try { const res = await fetch('/api/heatmap/matrix'); if (!res.ok) throw new Error('Erreur chargement heatmap matrix'); const data: HeatmapMatrix = await res.json(); setMatrixData(data); } catch (err) { setMatrixError(err instanceof Error ? err.message : 'Erreur inconnue'); } finally { setMatrixLoading(false); } }; fetchHourly(); fetchTopHosts(); fetchMatrix(); }, []); const peakHour = hourly.reduce((best, h) => (h.hits > best.hits ? h : best), { hour: 0, hits: 0, unique_ips: 0, max_rps: 0 }); const totalHits = hourly.reduce((s, h) => s + h.hits, 0); const maxHits = hourly.length > 0 ? Math.max(...hourly.map((h) => h.hits)) : 1; const matrixMax = matrixData ? Math.max(...matrixData.matrix.flatMap((row) => row), 1) : 1; const displayHosts = matrixData ? matrixData.hosts.slice(0, 15) : []; return (
{/* Header */}

⏱️ Heatmap Temporelle d'Attaques

Distribution horaire de l'activité malveillante par host cible.

{/* Stat cards */}
0 ? `${peakHour.hour}h (${formatNumber(peakHour.hits)} hits)` : '—'} accent="text-threat-critical" />
{/* Section 1: Courbe horaire */}

Activité horaire — 24h

{hourlyLoading ? ( ) : hourlyError ? ( ) : ( <>
{Array.from({ length: 24 }, (_, i) => { const entry = hourly.find((h) => h.hour === i) ?? { hour: i, hits: 0, unique_ips: 0, max_rps: 0 }; const pct = maxHits > 0 ? (entry.hits / maxHits) * 100 : 0; return (
= 70 ? 'bg-threat-critical' : pct >= 30 ? 'bg-threat-medium' : 'bg-accent-primary/60' }`} />
{i}
); })}
Élevé (≥70%) Moyen (≥30%) Faible
)}
{/* Section 2: Heatmap matrix */}

Heatmap Host × Heure

{matrixLoading ? ( ) : matrixError ? ( ) : !matrixData || displayHosts.length === 0 ? (

Aucune donnée disponible.

) : (
{/* Hour headers */}
{Array.from({ length: 24 }, (_, i) => (
{i}
))}
{/* Rows */} {displayHosts.map((host, rowIdx) => { const rowData = matrixData.matrix[rowIdx] ?? Array(24).fill(0); return (
{host}
{Array.from({ length: 24 }, (_, h) => { const val = rowData[h] ?? 0; return (
); })}
); })}
Intensité : {[ { label: 'Nul', style: { backgroundColor: 'transparent', border: '1px solid rgba(255,255,255,0.1)' } }, { label: 'Très faible', style: { backgroundColor: 'rgba(147, 197, 253, 0.15)' } }, { label: 'Faible', style: { backgroundColor: 'rgba(96, 165, 250, 0.35)' } }, { label: 'Moyen', style: { backgroundColor: 'rgba(59, 130, 246, 0.6)' } }, { label: 'Élevé', style: { backgroundColor: 'rgba(168, 85, 247, 0.7)' } }, { label: 'Critique', style: { backgroundColor: 'rgba(239, 68, 68, 0.85)' } }, ].map(({ label, style }) => ( {label} ))}
)}
{/* Section 3: Top hosts table */}

Top Hosts ciblés

{topHostsLoading ? ( ) : topHostsError ? (
) : ( {topHosts.map((h) => ( ))}
Host Total hits IPs uniques JA4 uniques Activité 24h
{h.host} {formatNumber(h.total_hits)} {formatNumber(h.unique_ips)} {formatNumber(h.unique_ja4s)}
)}
); }