feat(phase-c): drift-adaptive anomaly detection + smarter eviction scoring
serve_v2: Welford online stats per node (hot_pct, hit_rate, fs_pct, chunk_miss_rate) updated on every poller tick. Anomaly score = sum of z²; warn at 9, alert at 16. Exposed via GET /api/v2/anomalies. Score embedded in MetricSample so sparklines can also surface it. hot: Replace pure-LRU gc_by_space with size×age eviction scoring. Candidates are ranked by ln(size) × ln(age_secs); pinned projects immune. Evicts the most space with the least cost rather than just the oldest entry. dashboard: Fleet anomaly banner in CommandCenter (red/amber) + per-node "anomaly"/"drift" badge on NodeCard. Anomalies polled every 10s alongside the fleet endpoint. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
3fdf46d416
commit
70ad863006
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api, FleetSnapshot, fmtBytes, fmtAge } from '../lib/api';
|
||||
import { api, FleetSnapshot, AnomalyStatus, fmtBytes, fmtAge } from '../lib/api';
|
||||
import { NodeCard } from '../components/NodeCard';
|
||||
import { ProjectsPanel } from '../components/ProjectsPanel';
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ProjectsPanel } from '../components/ProjectsPanel';
|
||||
// Polls the aggregator's /api/v2/fleet every 10 s.
|
||||
export function CommandCenter() {
|
||||
const [fleet, setFleet] = useState<FleetSnapshot | null>(null);
|
||||
const [anomalies, setAnomalies] = useState<AnomalyStatus[]>([]);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const [tick, setTick] = useState(0);
|
||||
|
||||
@@ -18,6 +19,7 @@ export function CommandCenter() {
|
||||
setErr(null);
|
||||
})
|
||||
.catch((e) => setErr(String(e)));
|
||||
api.anomalies().then(setAnomalies).catch(() => {});
|
||||
}, [tick]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -39,6 +41,13 @@ export function CommandCenter() {
|
||||
)
|
||||
: null;
|
||||
|
||||
// Build a node→level lookup for NodeCard props.
|
||||
const anomalyMap = Object.fromEntries(
|
||||
anomalies.map((a) => [a.node, a.level] as const)
|
||||
);
|
||||
const alertNodes = anomalies.filter((a) => a.level === 'alert');
|
||||
const warnNodes = anomalies.filter((a) => a.level === 'warn');
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
@@ -65,6 +74,31 @@ export function CommandCenter() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Anomaly banners (only shown once the ring buffer has 10+ samples) */}
|
||||
{alertNodes.length > 0 && (
|
||||
<div className="rounded border border-red-800 bg-red-950/40 px-4 py-3 text-sm flex items-start gap-3">
|
||||
<span className="text-red-400 font-bold mt-0.5">●</span>
|
||||
<div>
|
||||
<span className="text-red-300 font-semibold">Metric anomaly detected — </span>
|
||||
<span className="text-red-200">
|
||||
{alertNodes.map((a) => a.node).join(', ')} deviating >2σ from baseline
|
||||
{alertNodes.length === 1 && ` (score ${alertNodes[0].score.toFixed(1)})`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{alertNodes.length === 0 && warnNodes.length > 0 && (
|
||||
<div className="rounded border border-amber-800 bg-amber-950/40 px-4 py-3 text-sm flex items-start gap-3">
|
||||
<span className="text-amber-400 font-bold mt-0.5">●</span>
|
||||
<div>
|
||||
<span className="text-amber-300 font-semibold">Metric drift — </span>
|
||||
<span className="text-amber-200">
|
||||
{warnNodes.map((a) => a.node).join(', ')} showing unusual patterns
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{totals && totals.diskTotal > 0 && (
|
||||
<div className="rounded-lg border border-slate-800 bg-slate-900/60 p-4 flex items-center justify-between">
|
||||
<div>
|
||||
@@ -89,7 +123,11 @@ export function CommandCenter() {
|
||||
<h2 className="text-lg font-semibold text-slate-100 mb-3">Nodes</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{fleet?.nodes.map((n) => (
|
||||
<NodeCard key={n.node_name} node={n} />
|
||||
<NodeCard
|
||||
key={n.node_name}
|
||||
node={n}
|
||||
anomalyLevel={anomalyMap[n.node_name]}
|
||||
/>
|
||||
))}
|
||||
{!fleet &&
|
||||
[1, 2, 3].map((i) => (
|
||||
|
||||
Reference in New Issue
Block a user