ids.alfacom.it/client/src/pages/Dashboard.tsx
marco370 4a2d7f9c5c Add service monitoring and status indicators to the dashboard
Introduce a new services page, integrate real-time status monitoring for ML backend, database, and syslog parser, and update the dashboard to display service health indicators.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: cde95c60-908b-48a0-b7b9-38e5e924b3b3
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/n4Q2eeE
2025-11-22 09:24:10 +00:00

372 lines
16 KiB
TypeScript

import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Activity, Shield, Server, AlertTriangle, CheckCircle2, TrendingUp, Database, FileText, Brain } from "lucide-react";
import { format } from "date-fns";
import { it } from "date-fns/locale";
import type { Detection, Router, TrainingHistory } from "@shared/schema";
interface StatsResponse {
routers: { total: number; enabled: number };
detections: { total: number; blocked: number; critical: number; high: number };
logs: { recent: number };
whitelist: { total: number };
latestTraining: TrainingHistory | null;
}
interface ServiceStatus {
name: string;
status: "running" | "idle" | "offline" | "error" | "unknown";
healthy: boolean;
details: any;
}
interface ServicesStatusResponse {
services: {
mlBackend: ServiceStatus;
database: ServiceStatus;
syslogParser: ServiceStatus;
};
}
export default function Dashboard() {
const { data: stats } = useQuery<StatsResponse>({
queryKey: ["/api/stats"],
refetchInterval: 10000, // Refresh every 10s
});
const { data: recentDetections } = useQuery<Detection[]>({
queryKey: ["/api/detections"],
refetchInterval: 5000, // Refresh every 5s
});
const { data: routers } = useQuery<Router[]>({
queryKey: ["/api/routers"],
});
const { data: servicesStatus } = useQuery<ServicesStatusResponse>({
queryKey: ["/api/services/status"],
refetchInterval: 5000, // Refresh every 5s
});
const getRiskBadge = (riskScore: string) => {
const score = parseFloat(riskScore);
if (score >= 85) return <Badge variant="destructive" data-testid={`badge-risk-critical`}>CRITICO</Badge>;
if (score >= 70) return <Badge className="bg-orange-500" data-testid={`badge-risk-high`}>ALTO</Badge>;
if (score >= 60) return <Badge className="bg-yellow-500" data-testid={`badge-risk-medium`}>MEDIO</Badge>;
if (score >= 40) return <Badge variant="secondary" data-testid={`badge-risk-low`}>BASSO</Badge>;
return <Badge variant="outline" data-testid={`badge-risk-normal`}>NORMALE</Badge>;
};
return (
<div className="flex flex-col gap-6 p-6" data-testid="page-dashboard">
<div>
<h1 className="text-3xl font-semibold" data-testid="text-dashboard-title">IDS Dashboard</h1>
<p className="text-muted-foreground" data-testid="text-dashboard-subtitle">
Monitoring real-time del sistema di rilevamento intrusioni
</p>
</div>
{/* Services Status */}
<Card data-testid="card-services-status">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Stato Servizi
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* ML Backend */}
<div className="flex items-center gap-3 p-3 rounded-lg border" data-testid="service-ml-backend">
<div className={`h-3 w-3 rounded-full ${servicesStatus?.services.mlBackend.healthy ? 'bg-green-500' : 'bg-red-500'}`} data-testid="status-indicator-ml-backend" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<Brain className="h-4 w-4 text-muted-foreground" />
<p className="font-medium text-sm">ML Backend</p>
</div>
<p className="text-xs text-muted-foreground">
{servicesStatus?.services.mlBackend.status === 'running' && 'In esecuzione'}
{servicesStatus?.services.mlBackend.status === 'offline' && 'Offline'}
{servicesStatus?.services.mlBackend.status === 'error' && 'Errore'}
{!servicesStatus && 'Caricamento...'}
</p>
{servicesStatus?.services.mlBackend.details?.modelLoaded !== undefined && (
<p className="text-xs text-muted-foreground mt-1">
Modello: {servicesStatus.services.mlBackend.details.modelLoaded ? '✓ Caricato' : '✗ Non caricato'}
</p>
)}
</div>
</div>
{/* Database */}
<div className="flex items-center gap-3 p-3 rounded-lg border" data-testid="service-database">
<div className={`h-3 w-3 rounded-full ${servicesStatus?.services.database.healthy ? 'bg-green-500' : 'bg-red-500'}`} data-testid="status-indicator-database" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-muted-foreground" />
<p className="font-medium text-sm">Database</p>
</div>
<p className="text-xs text-muted-foreground">
{servicesStatus?.services.database.status === 'running' && 'Connesso'}
{servicesStatus?.services.database.status === 'error' && 'Errore connessione'}
{!servicesStatus && 'Caricamento...'}
</p>
</div>
</div>
{/* Syslog Parser */}
<div className="flex items-center gap-3 p-3 rounded-lg border" data-testid="service-syslog-parser">
<div className={`h-3 w-3 rounded-full ${servicesStatus?.services.syslogParser.healthy ? 'bg-green-500' : 'bg-yellow-500'}`} data-testid="status-indicator-syslog-parser" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<FileText className="h-4 w-4 text-muted-foreground" />
<p className="font-medium text-sm">Syslog Parser</p>
</div>
<p className="text-xs text-muted-foreground">
{servicesStatus?.services.syslogParser.status === 'running' && 'Attivo'}
{servicesStatus?.services.syslogParser.status === 'idle' && 'In attesa log'}
{servicesStatus?.services.syslogParser.status === 'error' && 'Errore'}
{!servicesStatus && 'Caricamento...'}
</p>
{servicesStatus?.services.syslogParser.details?.logsLast5Min !== undefined && (
<p className="text-xs text-muted-foreground mt-1">
{servicesStatus.services.syslogParser.details.logsLast5Min} log (5min)
</p>
)}
</div>
</div>
</div>
<div className="mt-4">
<Button variant="outline" size="sm" asChild data-testid="button-view-services">
<a href="/services">Gestisci Servizi</a>
</Button>
</div>
</CardContent>
</Card>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card data-testid="card-routers">
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Router Attivi</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold" data-testid="text-routers-count">
{stats?.routers.enabled || 0}/{stats?.routers.total || 0}
</div>
<p className="text-xs text-muted-foreground">
Router configurati
</p>
</CardContent>
</Card>
<Card data-testid="card-detections">
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Rilevamenti</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold" data-testid="text-detections-count">
{stats?.detections.total || 0}
</div>
<p className="text-xs text-muted-foreground">
{stats?.detections.critical || 0} critici, {stats?.detections.high || 0} alti
</p>
</CardContent>
</Card>
<Card data-testid="card-blocked">
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-2">
<CardTitle className="text-sm font-medium">IP Bloccati</CardTitle>
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold" data-testid="text-blocked-count">
{stats?.detections.blocked || 0}
</div>
<p className="text-xs text-muted-foreground">
IP attualmente bloccati
</p>
</CardContent>
</Card>
<Card data-testid="card-logs">
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Log Recenti</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold" data-testid="text-logs-count">
{stats?.logs.recent || 0}
</div>
<p className="text-xs text-muted-foreground">
Ultimi 1000 log analizzati
</p>
</CardContent>
</Card>
</div>
{/* Training Status */}
{stats?.latestTraining && (
<Card data-testid="card-training">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
Ultimo Training
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<p className="text-sm text-muted-foreground">Data</p>
<p className="font-medium" data-testid="text-training-date">
{format(new Date(stats.latestTraining.trainedAt), "dd/MM/yyyy HH:mm")}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Record</p>
<p className="font-medium" data-testid="text-training-records">
{stats.latestTraining.recordsProcessed.toLocaleString()}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Feature</p>
<p className="font-medium" data-testid="text-training-features">
{stats.latestTraining.featuresCount}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Stato</p>
<Badge variant={stats.latestTraining.status === 'success' ? 'default' : 'destructive'} data-testid="badge-training-status">
{stats.latestTraining.status}
</Badge>
</div>
</div>
{stats.latestTraining.notes && (
<p className="text-sm text-muted-foreground mt-2" data-testid="text-training-notes">
{stats.latestTraining.notes}
</p>
)}
</CardContent>
</Card>
)}
{/* Recent Detections */}
<Card data-testid="card-recent-detections">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5" />
Rilevamenti Recenti
</span>
<Button variant="outline" size="sm" asChild data-testid="button-view-all-detections">
<a href="/detections">Vedi Tutti</a>
</Button>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{recentDetections && recentDetections.length > 0 ? (
recentDetections.slice(0, 5).map((detection) => (
<div
key={detection.id}
className="flex items-center justify-between p-3 rounded-lg border hover-elevate"
data-testid={`detection-item-${detection.sourceIp}`}
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<code className="font-mono font-semibold" data-testid={`text-ip-${detection.sourceIp}`}>
{detection.sourceIp}
</code>
{getRiskBadge(detection.riskScore)}
<Badge variant="outline" data-testid={`badge-type-${detection.sourceIp}`}>
{detection.anomalyType}
</Badge>
</div>
<p className="text-sm text-muted-foreground truncate mt-1" data-testid={`text-reason-${detection.sourceIp}`}>
{detection.reason}
</p>
<div className="flex items-center gap-4 mt-1 text-xs text-muted-foreground">
<span data-testid={`text-score-${detection.sourceIp}`}>
Risk: {parseFloat(detection.riskScore).toFixed(1)}
</span>
<span data-testid={`text-confidence-${detection.sourceIp}`}>
Confidence: {parseFloat(detection.confidence).toFixed(1)}%
</span>
<span data-testid={`text-logs-${detection.sourceIp}`}>
{detection.logCount} log
</span>
</div>
</div>
<div className="flex items-center gap-2 ml-4">
{detection.blocked ? (
<Badge variant="destructive" className="flex items-center gap-1" data-testid={`badge-blocked-${detection.sourceIp}`}>
<Shield className="h-3 w-3" />
Bloccato
</Badge>
) : (
<Badge variant="outline" data-testid={`badge-active-${detection.sourceIp}`}>
Attivo
</Badge>
)}
</div>
</div>
))
) : (
<div className="text-center py-8 text-muted-foreground" data-testid="text-no-detections">
<CheckCircle2 className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>Nessun rilevamento recente</p>
<p className="text-sm">Il sistema sta monitorando il traffico</p>
</div>
)}
</div>
</CardContent>
</Card>
{/* Routers Status */}
{routers && routers.length > 0 && (
<Card data-testid="card-routers-status">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Server className="h-5 w-5" />
Router MikroTik
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{routers.map((router) => (
<div
key={router.id}
className="p-4 rounded-lg border hover-elevate"
data-testid={`router-card-${router.name}`}
>
<div className="flex items-center justify-between mb-2">
<p className="font-medium" data-testid={`text-router-name-${router.name}`}>{router.name}</p>
<Badge
variant={router.enabled ? "default" : "secondary"}
data-testid={`badge-router-status-${router.name}`}
>
{router.enabled ? "Attivo" : "Disabilitato"}
</Badge>
</div>
<p className="text-sm text-muted-foreground font-mono" data-testid={`text-router-ip-${router.name}`}>
{router.ipAddress}:{router.apiPort}
</p>
{router.lastSync && (
<p className="text-xs text-muted-foreground mt-1" data-testid={`text-router-sync-${router.name}`}>
Ultima sync: {format(new Date(router.lastSync), "HH:mm:ss")}
</p>
)}
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
);
}