From 42535528eb3a897512e41c6fda55e7a1c6b337a9 Mon Sep 17 00:00:00 2001 From: kilyabin <65072190+kilyabin@users.noreply.github.com> Date: Thu, 28 May 2026 14:43:03 +0400 Subject: [PATCH] feat: redesign Overview with active threats, severity donut, top processes bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add /api/active-anomalies endpoint — latest snapshot per PID where is_anomaly=1 - Active threats card with pulsing indicator, CPU/RAM/TCP per process - Alerts by severity donut chart (high/medium/low) - Top processes horizontal bar chart weighted by alert count --- backend/main.py | 18 ++ frontend/src/components/Overview.tsx | 259 +++++++++++++++++++++++++-- frontend/src/lib/api.ts | 3 +- 3 files changed, 264 insertions(+), 16 deletions(-) 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 ? ( +
+ + Аномалий не обнаружено +
+ ) : ( + + )} +
+
+ ) +} + +// ── 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 = () => ( + + ) + + 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`), }