import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { InfoTip } from './ui/Tooltip'; import { TIPS } from './ui/tooltips'; interface IncidentCluster { id: string; score: number; severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; total_detections: number; unique_ips: number; subnet?: string; sample_ip?: string; ja4?: string; primary_ua?: string; primary_target?: string; countries: { code: string; percentage: number }[]; asn?: string; first_seen: string; last_seen: string; trend: 'up' | 'down' | 'stable'; trend_percentage: number; hits_per_second?: number; } interface MetricsSummary { total_detections: number; critical_count: number; high_count: number; medium_count: number; low_count: number; unique_ips: number; known_bots_count: number; anomalies_count: number; } interface BaselineMetric { today: number; yesterday: number; pct_change: number; } interface BaselineData { total_detections: BaselineMetric; unique_ips: BaselineMetric; critical_alerts: BaselineMetric; } export function IncidentsView() { const navigate = useNavigate(); const [clusters, setClusters] = useState([]); const [metrics, setMetrics] = useState(null); const [baseline, setBaseline] = useState(null); const [loading, setLoading] = useState(true); const [selectedClusters, setSelectedClusters] = useState>(new Set()); useEffect(() => { const fetchIncidents = async () => { setLoading(true); try { const metricsResponse = await fetch('/api/metrics'); if (metricsResponse.ok) { const metricsData = await metricsResponse.json(); setMetrics(metricsData.summary); } const baselineResponse = await fetch('/api/metrics/baseline'); if (baselineResponse.ok) { setBaseline(await baselineResponse.json()); } const clustersResponse = await fetch('/api/incidents/clusters'); if (clustersResponse.ok) { const clustersData = await clustersResponse.json(); setClusters(clustersData.items || []); } } catch (error) { console.error('Error fetching incidents:', error); } finally { setLoading(false); } }; fetchIncidents(); const interval = setInterval(fetchIncidents, 60000); return () => clearInterval(interval); }, []); const toggleCluster = (id: string) => { const newSelected = new Set(selectedClusters); if (newSelected.has(id)) { newSelected.delete(id); } else { newSelected.add(id); } setSelectedClusters(newSelected); }; const selectAll = () => { if (selectedClusters.size === clusters.length) { setSelectedClusters(new Set()); } else { setSelectedClusters(new Set(clusters.map(c => c.id))); } }; const getSeverityColor = (severity: string) => { switch (severity) { case 'CRITICAL': return 'border-red-500 bg-red-500/10'; case 'HIGH': return 'border-orange-500 bg-orange-500/10'; case 'MEDIUM': return 'border-yellow-500 bg-yellow-500/10'; case 'LOW': return 'border-green-500 bg-green-500/10'; default: return 'border-gray-500 bg-gray-500/10'; } }; const getSeverityBadgeColor = (severity: string) => { switch (severity) { case 'CRITICAL': return 'bg-red-500 text-white'; case 'HIGH': return 'bg-orange-500 text-white'; case 'MEDIUM': return 'bg-yellow-500 text-white'; case 'LOW': return 'bg-green-500 text-white'; default: return 'bg-gray-500 text-white'; } }; const getCountryFlag = (code: string) => { return code.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)); }; if (loading) { return (
Chargement...
); } return (
{/* Header */}

SOC Dashboard

Surveillance en temps réel · 24 dernières heures

{/* Stats unifiées — 6 cartes compact */}
{/* Total détections avec comparaison hier */}
navigate('/detections')} >
📊 Total 24h
{(metrics?.total_detections ?? 0).toLocaleString()}
{baseline && (() => { const m = baseline.total_detections; const up = m.pct_change > 0; const neutral = m.pct_change === 0; return (
{neutral ? '= même' : up ? `▲ +${m.pct_change}%` : `▼ ${m.pct_change}%`} vs hier
); })()}
{/* IPs uniques */}
navigate('/detections')} >
🖥️ IPs uniques
{(metrics?.unique_ips ?? 0).toLocaleString()}
{baseline && (() => { const m = baseline.unique_ips; const up = m.pct_change > 0; const neutral = m.pct_change === 0; return (
{neutral ? '= même' : up ? `▲ +${m.pct_change}%` : `▼ ${m.pct_change}%`} vs hier
); })()}
{/* BOT connus */}
navigate('/detections?score_type=BOT')} >
🤖 BOT nommés
{(metrics?.known_bots_count ?? 0).toLocaleString()}
{metrics ? Math.round((metrics.known_bots_count / metrics.total_detections) * 100) : 0}% du total
{/* Anomalies ML */}
navigate('/detections?score_type=SCORE')} >
🔬 Anomalies ML
{(metrics?.anomalies_count ?? 0).toLocaleString()}
{metrics ? Math.round((metrics.anomalies_count / metrics.total_detections) * 100) : 0}% du total
{/* HIGH */}
navigate('/detections?threat_level=HIGH')} >
⚠️ HIGH
{(metrics?.high_count ?? 0).toLocaleString()}
Menaces élevées
{/* MEDIUM */}
navigate('/detections?threat_level=MEDIUM')} >
📊 MEDIUM
{(metrics?.medium_count ?? 0).toLocaleString()}
Menaces moyennes
{/* Bulk Actions */} {selectedClusters.size > 0 && (
{selectedClusters.size} incidents sélectionnés
)} {/* Main content: incidents list (2/3) + top threats table (1/3) */}
{/* Incidents list — 2/3 */}

Incidents Prioritaires

{clusters.map((cluster) => (
{/* Checkbox */} toggleCluster(cluster.id)} className="mt-1 w-4 h-4 rounded bg-background-card border-background-card text-accent-primary focus:ring-accent-primary" /> {/* Content */}
{cluster.severity} {cluster.id} | {cluster.subnet || ''}
{cluster.score}/100
Score de risque
IPs
{cluster.unique_ips}
Détections
{cluster.total_detections}
Pays
{cluster.countries[0] && ( <> {getCountryFlag(cluster.countries[0].code)} {cluster.countries[0].code} )}
ASN
AS{cluster.asn || '?'}
Tendance
{cluster.trend === 'up' ? '↑' : cluster.trend === 'down' ? '↓' : '→'} {cluster.trend_percentage}%
{cluster.ja4 && (
JA4 Principal
{cluster.ja4}
)}
))} {clusters.length === 0 && (

Aucun incident actif

Le système ne détecte aucun incident prioritaire en ce moment.

)}
{/* end col-span-2 */} {/* Top threats sidebar — 1/3 */}

🔥 Top Menaces

{clusters.slice(0, 12).map((cluster, index) => (
navigate(`/investigation/${cluster.sample_ip || cluster.subnet?.split('/')[0] || ''}`)} > {index + 1}
{cluster.sample_ip || cluster.subnet?.split('/')[0] || 'Unknown'}
{cluster.countries[0] && ( {getCountryFlag(cluster.countries[0].code)} {cluster.countries[0].code} )} AS{cluster.asn || '?'}
80 ? 'bg-red-500 text-white' : cluster.score > 60 ? 'bg-orange-500 text-white' : cluster.score > 40 ? 'bg-yellow-500 text-white' : 'bg-green-500 text-white' }`}> {cluster.score} {cluster.trend === 'up' ? '↑' : cluster.trend === 'down' ? '↓' : '→'}
))} {clusters.length === 0 && (
Aucune menace active
)}
{/* end grid */}
); } // ─── Mini Heatmap ───────────────────────────────────────────────────────────── interface HeatmapHour { hour: number; hits: number; unique_ips: number; } function MiniHeatmap() { const [data, setData] = useState([]); useEffect(() => { fetch('/api/heatmap/hourly') .then(r => r.ok ? r.json() : null) .then(d => { if (d) setData(d.hours ?? d.items ?? []); }) .catch(() => {}); }, []); if (data.length === 0) return null; const maxHits = Math.max(...data.map(d => d.hits), 1); const barColor = (hits: number) => { const pct = (hits / maxHits) * 100; if (pct >= 75) return 'bg-red-500/70'; if (pct >= 50) return 'bg-purple-500/60'; if (pct >= 25) return 'bg-blue-500/50'; if (pct >= 5) return 'bg-blue-400/30'; return 'bg-slate-700/30'; }; return (
⏱️ Activité par heure (72h)
{data.map((d, i) => (
{d.hits.toLocaleString()} hits — {d.unique_ips} IPs
{[0, 6, 12, 18].includes(d.hour) ? `${d.hour}h` : '\u00a0'}
))}
); }