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
372 lines
16 KiB
TypeScript
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>
|
|
);
|
|
}
|