feat: ensemble ML, alert deduplication, larger synthetic dataset
- Replace IsolationForest with voting ensemble (IF + LOF + OC-SVM), anomaly flagged when ≥ 2/3 models agree - Normalise per-process cpu_percent by CPU count to prevent false positives from multi-threaded processes (e.g. Firefox 130% → 65%) - Deduplicate alerts: same PID anomalous within 30s updates last_seen and count instead of inserting a new row; WS broadcast only on first occurrence - Show Duration column in Alerts table (e.g. "2м 30с ×28") - Increase synthetic training data from 2000 to 10000 rows - Add DB migration for last_seen/count columns on existing databases
This commit is contained in:
@@ -26,6 +26,16 @@ function severityLabel(s: Alert['severity']): string {
|
||||
return 'низкий'
|
||||
}
|
||||
|
||||
function fmtDuration(first: string, last: string | null): string | null {
|
||||
if (!last) return null
|
||||
const secs = Math.round((new Date(last).getTime() - new Date(first).getTime()) / 1000)
|
||||
if (secs < 5) return null
|
||||
if (secs < 60) return `${secs}с`
|
||||
const m = Math.floor(secs / 60)
|
||||
const s = secs % 60
|
||||
return s > 0 ? `${m}м ${s}с` : `${m}м`
|
||||
}
|
||||
|
||||
interface Props {
|
||||
wsConnected: boolean
|
||||
}
|
||||
@@ -70,7 +80,8 @@ export function Alerts({ wsConnected }: Props) {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-40">Время</TableHead>
|
||||
<TableHead className="w-40">Начало</TableHead>
|
||||
<TableHead className="w-28">Длительность</TableHead>
|
||||
<TableHead className="w-16">PID</TableHead>
|
||||
<TableHead>Процесс</TableHead>
|
||||
<TableHead>Причина</TableHead>
|
||||
@@ -78,21 +89,34 @@ export function Alerts({ wsConnected }: Props) {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((a) => (
|
||||
<TableRow key={a.id}>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground whitespace-nowrap">
|
||||
{fmtFull(a.timestamp)}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-muted-foreground">{a.pid}</TableCell>
|
||||
<TableCell className="font-mono">{a.process_name}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">{a.reason}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant={severityVariant(a.severity)}>
|
||||
{severityLabel(a.severity)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{data.map((a) => {
|
||||
const duration = fmtDuration(a.timestamp, a.last_seen)
|
||||
return (
|
||||
<TableRow key={a.id}>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground whitespace-nowrap">
|
||||
{fmtFull(a.timestamp)}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs whitespace-nowrap">
|
||||
{duration ? (
|
||||
<span className="text-yellow-400">{duration}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">—</span>
|
||||
)}
|
||||
{(a.count ?? 1) > 1 && (
|
||||
<span className="ml-1 text-muted-foreground">×{a.count}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-muted-foreground">{a.pid}</TableCell>
|
||||
<TableCell className="font-mono">{a.process_name}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">{a.reason}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant={severityVariant(a.severity)}>
|
||||
{severityLabel(a.severity)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface ProcessMetric {
|
||||
export interface Alert {
|
||||
id: number
|
||||
timestamp: string
|
||||
last_seen: string | null
|
||||
count: number
|
||||
pid: number
|
||||
process_name: string
|
||||
reason: string
|
||||
|
||||
Reference in New Issue
Block a user