Skip to content

Commit c4403e2

Browse files
committed
feat(monitoring): API /api/monitoring + pagina dashboard real-time CPU/mem/uptime, agenti, alert con polling 3s
2 parents fd6aca5 + 63bf462 commit c4403e2

2 files changed

Lines changed: 177 additions & 0 deletions

File tree

web/app/api/monitoring/route.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* API Monitoring — Metriche sistema, heartbeat agenti, alert
3+
*/
4+
import { NextResponse } from 'next/server';
5+
6+
let mod: typeof import('../../../../shared/monitoring/index.js') | null = null;
7+
async function load() {
8+
if (!mod) mod = await import('../../../../shared/monitoring/index.js');
9+
return mod;
10+
}
11+
12+
// GET — metriche correnti, agenti, alert attivi
13+
export async function GET(req: Request) {
14+
const m = await load();
15+
const { searchParams } = new URL(req.url);
16+
const section = searchParams.get('section');
17+
18+
if (section === 'history') {
19+
return NextResponse.json({ history: m.getMetricsHistory() });
20+
}
21+
22+
if (section === 'agents') {
23+
return NextResponse.json({ agents: m.getAllAgentStatuses() });
24+
}
25+
26+
if (section === 'alerts') {
27+
return NextResponse.json({ alerts: m.getActiveAlerts(), thresholds: m.getThresholds() });
28+
}
29+
30+
// Default: tutto
31+
const metrics = m.collectMetrics();
32+
const agents = m.getAllAgentStatuses();
33+
const alerts = m.checkThresholds(metrics);
34+
const unhealthy = m.checkHeartbeats();
35+
36+
return NextResponse.json({
37+
metrics,
38+
agents,
39+
alerts: m.getActiveAlerts(),
40+
unhealthyAgents: unhealthy,
41+
history: m.getMetricsHistory().slice(-10),
42+
});
43+
}
44+
45+
// POST — registra heartbeat agente o definisci soglia
46+
export async function POST(req: Request) {
47+
const m = await load();
48+
try {
49+
const body = await req.json();
50+
51+
if (body.type === 'heartbeat') {
52+
if (!body.agentId) return NextResponse.json({ error: 'agentId richiesto' }, { status: 400 });
53+
m.registerHeartbeat(body.agentId, body.metadata);
54+
return NextResponse.json({ ok: true, agentId: body.agentId });
55+
}
56+
57+
if (body.type === 'threshold') {
58+
if (!body.id || !body.metric || !body.operator || body.value === undefined) {
59+
return NextResponse.json({ error: 'Campi id, metric, operator, value richiesti' }, { status: 400 });
60+
}
61+
m.defineThreshold({
62+
id: body.id, metric: body.metric, operator: body.operator,
63+
value: body.value, description: body.description || '',
64+
});
65+
return NextResponse.json({ ok: true, thresholdId: body.id }, { status: 201 });
66+
}
67+
68+
return NextResponse.json({ error: 'type deve essere heartbeat o threshold' }, { status: 400 });
69+
} catch (err) {
70+
return NextResponse.json({ error: String(err) }, { status: 500 });
71+
}
72+
}

web/app/monitoring/page.tsx

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
'use client'
2+
3+
import Link from 'next/link'
4+
import { useEffect, useState, useCallback } from 'react'
5+
6+
type Metrics = { cpuUsage: number; memoryUsedMB: number; memoryTotalMB: number; memoryPercent: number; uptimeSeconds: number; loadAvg: number[] }
7+
type Agent = { agentId: string; lastSeen: number; status: 'alive' | 'stale' | 'dead'; metadata?: Record<string, unknown> }
8+
type Alert = { thresholdId: string; metric: string; currentValue: number; thresholdValue: number; description: string; triggeredAt: number }
9+
10+
const STATUS_CFG: Record<string, { color: string; bg: string }> = {
11+
alive: { color: 'var(--color-green)', bg: 'rgba(0,200,83,0.08)' },
12+
stale: { color: 'var(--color-yellow)', bg: 'rgba(245,197,24,0.08)' },
13+
dead: { color: 'var(--color-red)', bg: 'rgba(255,69,96,0.08)' },
14+
}
15+
16+
function MetricCard({ label, value, unit, warn }: { label: string; value: string; unit?: string; warn?: boolean }) {
17+
return (
18+
<div className="flex flex-col p-4 rounded-lg" style={{ background: 'var(--color-row)', border: `1px solid ${warn ? 'rgba(255,69,96,0.3)' : 'var(--color-border)'}` }}>
19+
<span className="text-[9px] font-bold tracking-widest text-[var(--color-dim)] uppercase">{label}</span>
20+
<span className="text-xl font-bold font-mono mt-1" style={{ color: warn ? 'var(--color-red)' : 'var(--color-bright)' }}>{value}<span className="text-[10px] text-[var(--color-dim)] ml-1">{unit}</span></span>
21+
</div>
22+
)
23+
}
24+
25+
function formatUptime(s: number): string {
26+
const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60);
27+
return d > 0 ? `${d}g ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m`;
28+
}
29+
30+
export default function MonitoringPage() {
31+
const [metrics, setMetrics] = useState<Metrics | null>(null)
32+
const [agents, setAgents] = useState<Agent[]>([])
33+
const [alerts, setAlerts] = useState<Alert[]>([])
34+
35+
const fetchData = useCallback(async () => {
36+
const res = await fetch('/api/monitoring').catch(() => null)
37+
if (!res?.ok) return
38+
const data = await res.json()
39+
setMetrics(data.metrics)
40+
setAgents(data.agents ?? [])
41+
setAlerts(data.alerts ?? [])
42+
}, [])
43+
44+
useEffect(() => { fetchData() }, [fetchData])
45+
useEffect(() => { const id = setInterval(fetchData, 3000); return () => clearInterval(id) }, [fetchData])
46+
47+
return (
48+
<div style={{ animation: 'fade-in 0.35s ease both' }}>
49+
<div className="mb-8 pb-6 border-b border-[var(--color-border)]">
50+
<div className="flex items-center gap-2 mb-1">
51+
<Link href="/dashboard" className="text-[10px] text-[var(--color-dim)] hover:text-[var(--color-muted)] no-underline transition-colors">Dashboard</Link>
52+
<span className="text-[var(--color-border)]">/</span>
53+
<span className="text-[10px] text-[var(--color-muted)]">Monitoring</span>
54+
</div>
55+
<h1 className="text-2xl font-bold tracking-tight text-[var(--color-white)] mt-3">Monitoring</h1>
56+
<p className="text-[var(--color-muted)] text-[11px] mt-1">Metriche sistema real-time · {agents.length} agenti · {alerts.length} alert attivi</p>
57+
</div>
58+
59+
{metrics && (
60+
<div className="grid grid-cols-4 gap-3 mb-6">
61+
<MetricCard label="CPU" value={`${metrics.cpuUsage}`} unit="%" warn={metrics.cpuUsage > 80} />
62+
<MetricCard label="Memoria" value={`${metrics.memoryUsedMB}/${metrics.memoryTotalMB}`} unit="MB" warn={metrics.memoryPercent > 85} />
63+
<MetricCard label="Mem %" value={`${metrics.memoryPercent}`} unit="%" warn={metrics.memoryPercent > 85} />
64+
<MetricCard label="Uptime" value={formatUptime(metrics.uptimeSeconds)} />
65+
</div>
66+
)}
67+
68+
{alerts.length > 0 && (
69+
<div className="mb-6">
70+
<h2 className="text-sm font-bold text-[var(--color-red)] mb-3">Alert Attivi</h2>
71+
<div className="border border-[rgba(255,69,96,0.3)] rounded-lg overflow-hidden">
72+
{alerts.map(a => (
73+
<div key={a.thresholdId} className="flex items-center gap-3 px-5 py-3 border-b border-[var(--color-border)]" style={{ background: 'rgba(255,69,96,0.05)' }}>
74+
<span className="text-[10px] font-bold text-[var(--color-red)]">{a.metric}</span>
75+
<span className="flex-1 text-[11px] text-[var(--color-muted)]">{a.description}</span>
76+
<span className="font-mono text-[10px] text-[var(--color-red)]">{Math.round(a.currentValue)} &gt; {a.thresholdValue}</span>
77+
</div>
78+
))}
79+
</div>
80+
</div>
81+
)}
82+
83+
<div>
84+
<h2 className="text-sm font-bold text-[var(--color-bright)] mb-3">Agenti</h2>
85+
<div className="border border-[var(--color-border)] rounded-lg overflow-hidden bg-[var(--color-panel)]">
86+
{agents.length === 0
87+
? <div className="py-12 text-center"><p className="text-[var(--color-dim)] text-[12px]">Nessun agente registrato.</p></div>
88+
: agents.map(a => {
89+
const cfg = STATUS_CFG[a.status] || STATUS_CFG.dead;
90+
const ago = Math.floor((Date.now() - a.lastSeen) / 1000);
91+
return (
92+
<div key={a.agentId} className="flex items-center gap-4 px-5 py-3 border-b border-[var(--color-border)] hover:bg-[var(--color-row)] transition-colors">
93+
<span className="text-[12px] font-semibold font-mono text-[var(--color-bright)]">{a.agentId}</span>
94+
<span className="badge text-[9px]" style={{ color: cfg.color, background: cfg.bg, border: `1px solid ${cfg.color}40` }}>{a.status}</span>
95+
<span className="flex-1" />
96+
<span className="text-[10px] text-[var(--color-dim)]">{ago < 60 ? `${ago}s fa` : `${Math.floor(ago / 60)}m fa`}</span>
97+
</div>
98+
);
99+
})
100+
}
101+
</div>
102+
</div>
103+
</div>
104+
)
105+
}

0 commit comments

Comments
 (0)