diff --git a/backend/main.py b/backend/main.py
index 21faf35..fbdc2ff 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -227,6 +227,24 @@ async def get_system_metrics(limit: int = 60) -> list[dict]:
return list(reversed([dict(r) for r in rows]))
+@app.get("/api/active-anomalies", summary="Currently anomalous processes")
+async def get_active_anomalies() -> list[dict]:
+ """Latest snapshot per PID — only rows where is_anomaly=1."""
+ async with aiosqlite.connect(DB_PATH) as db:
+ db.row_factory = aiosqlite.Row
+ async with db.execute("""
+ SELECT m.*
+ FROM metrics m
+ INNER JOIN (
+ SELECT pid, MAX(id) AS max_id FROM metrics GROUP BY pid
+ ) latest ON m.id = latest.max_id
+ WHERE m.is_anomaly = 1
+ ORDER BY m.cpu_percent DESC
+ LIMIT 20
+ """) as cur:
+ return [dict(r) for r in await cur.fetchall()]
+
+
@app.get("/api/file-events", summary="Recent filesystem events")
async def get_file_events(limit: int = 100) -> list[dict]:
"""Return the most recent *limit* filesystem events."""
diff --git a/frontend/src/components/Overview.tsx b/frontend/src/components/Overview.tsx
index d5f975a..803caa5 100644
--- a/frontend/src/components/Overview.tsx
+++ b/frontend/src/components/Overview.tsx
@@ -1,9 +1,36 @@
import { useQuery } from '@tanstack/react-query'
-import { Activity, AlertTriangle, Bell, Cpu, Database } from 'lucide-react'
-import { api } from '@/lib/api'
+import { Activity, AlertTriangle, Bell, Cpu, Database, ShieldAlert } from 'lucide-react'
+import {
+ PieChart,
+ Pie,
+ Cell,
+ Tooltip as ChartTooltip,
+ BarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ ResponsiveContainer,
+} from 'recharts'
+import { api, type Alert, type ProcessMetric } from '@/lib/api'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Badge } from '@/components/ui/badge'
import { SystemChart } from '@/components/SystemChart'
+// ── shared tooltip style ──────────────────────────────────────────────────────
+
+const TOOLTIP_STYLE = {
+ contentStyle: {
+ background: '#111118',
+ border: '1px solid #00ff9d33',
+ borderRadius: 6,
+ fontSize: 12,
+ fontFamily: 'JetBrains Mono, monospace',
+ },
+ labelStyle: { color: '#6b7280' },
+}
+
+// ── stat card ─────────────────────────────────────────────────────────────────
+
function StatCard({
title,
value,
@@ -36,6 +63,186 @@ function StatCard({
)
}
+// ── active anomalies list ─────────────────────────────────────────────────────
+
+function ActiveAnomaliesList({ procs }: { procs: ProcessMetric[] }) {
+ return (
+
+
+
+
+
+ Активные угрозы
+
+ {procs.length > 0 && (
+
+ {procs.length}
+
+ )}
+
+
+
+ {procs.length === 0 ? (
+
+ ✓
+ Аномалий не обнаружено
+
+ ) : (
+
+ {procs.map((p) => (
+ -
+
+
+
+ {p.name}
+
+ PID {p.pid}
+
+
+
+ CPU {p.cpu_percent}%
+ RAM {Math.round(p.memory_mb)} МБ
+ {p.connections > 0 && (
+ {p.connections} TCP
+ )}
+
+
+
+ ))}
+
+ )}
+
+
+ )
+}
+
+// ── severity donut ────────────────────────────────────────────────────────────
+
+const SEV_META = [
+ { key: 'high', label: 'Высокий', color: '#ff3366' },
+ { key: 'medium', label: 'Средний', color: '#ff6b35' },
+ { key: 'low', label: 'Низкий', color: '#00d4ff' },
+] as const
+
+function SeverityDonut({ alerts }: { alerts: Alert[] }) {
+ const counts = { high: 0, medium: 0, low: 0 }
+ for (const a of alerts) counts[a.severity] = (counts[a.severity] || 0) + 1
+
+ const data = SEV_META.map((m) => ({ name: m.label, value: counts[m.key], color: m.color }))
+ .filter((d) => d.value > 0)
+
+ const renderLegend = () => (
+
+ {data.map((d) => (
+ -
+
+ {d.name}
+ {d.value}
+
+ ))}
+
+ )
+
+ return (
+
+
+ Алерты по серьёзности
+
+
+ {data.length === 0 ? (
+
+ Алертов нет
+
+ ) : (
+
+
+
+
+ {data.map((d) => (
+ |
+ ))}
+
+ [v, n]}
+ />
+
+
+
{renderLegend()}
+
+ )}
+
+
+ )
+}
+
+// ── top processes bar ─────────────────────────────────────────────────────────
+
+function TopProcessesBar({ alerts }: { alerts: Alert[] }) {
+ const counts: Record = {}
+ for (const a of alerts) {
+ counts[a.process_name] = (counts[a.process_name] || 0) + (a.count ?? 1)
+ }
+ const data = Object.entries(counts)
+ .sort(([, a], [, b]) => b - a)
+ .slice(0, 6)
+ .map(([name, value]) => ({ name, value }))
+
+ return (
+
+
+ Топ процессов по алертам
+
+
+ {data.length === 0 ? (
+
+ Нет данных
+
+ ) : (
+
+
+
+
+ [v, 'алертов']}
+ />
+
+
+
+ )}
+
+
+ )
+}
+
+// ── main component ────────────────────────────────────────────────────────────
+
export function Overview() {
const { data: stats } = useQuery({
queryKey: ['stats'],
@@ -43,22 +250,33 @@ export function Overview() {
refetchInterval: 5_000,
})
- // Системные метрики — одна точка на батч (каждые 5 с), хранятся в отдельной таблице
const { data: sysRows } = useQuery({
queryKey: ['system-metrics'],
queryFn: () => api.systemMetrics(60),
refetchInterval: 5_000,
})
+ const { data: activeProcs } = useQuery({
+ queryKey: ['active-anomalies'],
+ queryFn: api.activeAnomalies,
+ refetchInterval: 5_000,
+ })
+
+ const { data: alerts } = useQuery({
+ queryKey: ['alerts'],
+ queryFn: () => api.alerts(200),
+ refetchInterval: 10_000,
+ })
+
const chartData = (sysRows ?? []).map((r) => ({
- t: r.timestamp.slice(11, 19), // HH:MM:SS
+ t: r.timestamp.slice(11, 19),
cpu: Math.round(r.cpu_percent * 10) / 10,
ram: Math.round(r.ram_percent * 10) / 10,
}))
return (
-
- {/* Карточки */}
+
+ {/* Stat cards */}
- {/* График */}
-
-
- Нагрузка системы — реальное время (60 точек)
-
-
-
-
-
+ {/* Active anomalies + system chart */}
+
+
+
+
+ Нагрузка системы — реальное время (60 точек)
+
+
+
+
+
+
+
+ {/* Severity donut + top processes */}
+
)
}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index cd496a4..f19f8e5 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -70,5 +70,6 @@ export const api = {
alerts: (limit = 50) => get
(`${BASE}/alerts?limit=${limit}`),
fileEvents: (limit = 100) => get(`${BASE}/file-events?limit=${limit}`),
stats: () => get(`${BASE}/stats`),
- systemMetrics: (limit = 60) => get(`${BASE}/system-metrics?limit=${limit}`),
+ systemMetrics: (limit = 60) => get(`${BASE}/system-metrics?limit=${limit}`),
+ activeAnomalies: () => get(`${BASE}/active-anomalies`),
}