Update service monitoring to display detailed status and health

Refactor the services page to dynamically fetch and display the status of various systemd services and timers, improving the observability of the application's backend components.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 48245392-3f34-4eac-aeaf-99e52684ddf2
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/s2eMVCL
This commit is contained in:
marco370 2026-02-17 09:09:26 +00:00
parent 6ce60ed5d3
commit 4118d60d6d
2 changed files with 419 additions and 393 deletions

View File

@ -2,25 +2,21 @@ import { useQuery, useMutation } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Activity, Brain, Database, FileText, Terminal, RefreshCw, AlertCircle, Play, Square, RotateCw } from "lucide-react"; import { Activity, Brain, Database, FileText, Terminal, RefreshCw, Play, Square, RotateCw, Shield, Trash2, ListChecks, GraduationCap, Server, Clock, Timer } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { queryClient, apiRequest } from "@/lib/queryClient"; import { queryClient, apiRequest } from "@/lib/queryClient";
interface ServiceStatus { interface ServiceStatus {
name: string; name: string;
status: "running" | "idle" | "offline" | "error" | "unknown"; status: string;
healthy: boolean; healthy: boolean;
details: any; details: any;
systemdUnit: string;
type: string;
} }
interface ServicesStatusResponse { interface ServicesStatusResponse {
services: { services: Record<string, ServiceStatus>;
mlBackend: ServiceStatus;
database: ServiceStatus;
syslogParser: ServiceStatus;
analyticsAggregator: ServiceStatus;
};
} }
export default function ServicesPage() { export default function ServicesPage() {
@ -28,10 +24,9 @@ export default function ServicesPage() {
const { data: servicesStatus, isLoading, refetch } = useQuery<ServicesStatusResponse>({ const { data: servicesStatus, isLoading, refetch } = useQuery<ServicesStatusResponse>({
queryKey: ["/api/services/status"], queryKey: ["/api/services/status"],
refetchInterval: 5000, // Refresh every 5s refetchInterval: 5000,
}); });
// Mutation for service control
const serviceControlMutation = useMutation({ const serviceControlMutation = useMutation({
mutationFn: async ({ service, action }: { service: string; action: string }) => { mutationFn: async ({ service, action }: { service: string; action: string }) => {
return apiRequest("POST", `/api/services/${service}/${action}`); return apiRequest("POST", `/api/services/${service}/${action}`);
@ -39,9 +34,8 @@ export default function ServicesPage() {
onSuccess: (data, variables) => { onSuccess: (data, variables) => {
toast({ toast({
title: "Operazione completata", title: "Operazione completata",
description: `Servizio ${variables.service}: ${variables.action} eseguito con successo`, description: `Servizio ${variables.service}: ${variables.action} eseguito`,
}); });
// Refresh status after 2 seconds
setTimeout(() => { setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ["/api/services/status"] }); queryClient.invalidateQueries({ queryKey: ["/api/services/status"] });
}, 2000); }, 2000);
@ -59,39 +53,260 @@ export default function ServicesPage() {
serviceControlMutation.mutate({ service, action }); serviceControlMutation.mutate({ service, action });
}; };
const getStatusBadge = (service: ServiceStatus) => { const getStatusBadge = (service: ServiceStatus, key: string) => {
if (service.healthy) { if (service.healthy) {
return <Badge variant="default" className="bg-green-600" data-testid={`badge-status-healthy`}>Online</Badge>; return <Badge variant="default" className="bg-green-600" data-testid={`badge-status-${key}-healthy`}>Online</Badge>;
} }
if (service.status === 'idle') { if (service.status === 'idle') {
return <Badge variant="secondary" data-testid={`badge-status-idle`}>In Attesa</Badge>; return <Badge variant="secondary" data-testid={`badge-status-${key}-idle`}>In Attesa</Badge>;
} }
if (service.status === 'offline') { if (service.status === 'offline') {
return <Badge variant="destructive" data-testid={`badge-status-offline`}>Offline</Badge>; return <Badge variant="destructive" data-testid={`badge-status-${key}-offline`}>Offline</Badge>;
} }
if (service.status === 'error') { if (service.status === 'error') {
return <Badge variant="destructive" data-testid={`badge-status-error`}>Errore</Badge>; return <Badge variant="destructive" data-testid={`badge-status-${key}-error`}>Errore</Badge>;
} }
return <Badge variant="outline" data-testid={`badge-status-unknown`}>Sconosciuto</Badge>; return <Badge variant="outline" data-testid={`badge-status-${key}-unknown`}>Sconosciuto</Badge>;
}; };
const getStatusIndicator = (service: ServiceStatus) => { const getStatusIndicator = (service: ServiceStatus) => {
if (service.healthy) { if (service.healthy) {
return <div className="h-3 w-3 rounded-full bg-green-500" />; return <div className="h-3 w-3 rounded-full bg-green-500 shrink-0" />;
} }
if (service.status === 'idle') { if (service.status === 'idle') {
return <div className="h-3 w-3 rounded-full bg-yellow-500" />; return <div className="h-3 w-3 rounded-full bg-yellow-500 shrink-0" />;
} }
return <div className="h-3 w-3 rounded-full bg-red-500" />; return <div className="h-3 w-3 rounded-full bg-red-500 shrink-0" />;
};
const getServiceIcon = (key: string) => {
const icons: Record<string, any> = {
nodeBackend: Server,
mlBackend: Brain,
database: Database,
syslogParser: FileText,
analyticsAggregator: Activity,
autoBlock: Shield,
cleanup: Trash2,
listFetcher: ListChecks,
mlTraining: GraduationCap,
};
const Icon = icons[key] || Activity;
return <Icon className="h-5 w-5" />;
};
const controllableServices = [
"ids-ml-backend", "ids-syslog-parser", "ids-backend",
"ids-analytics-aggregator", "ids-auto-block", "ids-cleanup",
"ids-list-fetcher", "ids-ml-training"
];
const getLogCommand = (key: string): string | null => {
const logs: Record<string, string> = {
nodeBackend: "tail -f /var/log/ids/backend.log",
mlBackend: "journalctl -u ids-ml-backend -f",
database: "sudo journalctl -u postgresql-16 -f",
syslogParser: "tail -f /var/log/ids/syslog_parser.log",
analyticsAggregator: "journalctl -u ids-analytics-aggregator -f",
autoBlock: "journalctl -u ids-auto-block -f",
cleanup: "journalctl -u ids-cleanup -f",
listFetcher: "journalctl -u ids-list-fetcher -f",
mlTraining: "journalctl -u ids-ml-training -f",
};
return logs[key] || null;
};
const renderDetailRow = (label: string, value: any, variant?: "default" | "destructive" | "secondary" | "outline") => {
if (value === undefined || value === null) return null;
return (
<div className="flex items-center justify-between gap-2 flex-wrap">
<span className="text-sm text-muted-foreground">{label}:</span>
{variant ? (
<Badge variant={variant} className="text-xs">{String(value)}</Badge>
) : (
<span className="text-sm font-mono">{String(value)}</span>
)}
</div>
);
};
const renderServiceDetails = (key: string, service: ServiceStatus) => {
const d = service.details;
if (!d) return null;
switch (key) {
case "nodeBackend":
return (
<>
{renderDetailRow("Porta", d.port)}
{renderDetailRow("Uptime", d.uptime)}
</>
);
case "mlBackend":
return (
<>
{d.modelLoaded !== undefined && renderDetailRow("Modello ML", d.modelLoaded ? "Caricato" : "Non Caricato", d.modelLoaded ? "default" : "secondary")}
{d.error && renderDetailRow("Errore", d.error, "destructive")}
</>
);
case "database":
return (
<>
{d.connected && renderDetailRow("Connessione", "Attiva", "default")}
{d.error && renderDetailRow("Errore", d.error, "destructive")}
</>
);
case "syslogParser":
return (
<>
{d.recentLogs30min !== undefined && renderDetailRow("Log ultimi 30min", d.recentLogs30min.toLocaleString())}
{d.lastLog && renderDetailRow("Ultimo log", typeof d.lastLog === 'string' ? d.lastLog : new Date(d.lastLog).toLocaleString('it-IT'))}
{d.warning && renderDetailRow("Avviso", d.warning, "destructive")}
</>
);
case "analyticsAggregator":
return (
<>
{d.lastRun && renderDetailRow("Ultima esecuzione", new Date(d.lastRun).toLocaleString('it-IT'))}
{d.hoursSinceLastRun && renderDetailRow("Ore dall'ultimo run", d.hoursSinceLastRun + "h", parseFloat(d.hoursSinceLastRun) < 2 ? "default" : "destructive")}
{d.warning && renderDetailRow("Avviso", d.warning, "destructive")}
</>
);
case "autoBlock":
return (
<>
{renderDetailRow("Blocchi ultimi 10min", d.recentBlocks10min)}
{renderDetailRow("Totale bloccati", d.totalBlocked)}
{d.lastBlock && renderDetailRow("Ultimo blocco", typeof d.lastBlock === 'string' && d.lastBlock !== 'Mai' ? new Date(d.lastBlock).toLocaleString('it-IT') : d.lastBlock)}
{renderDetailRow("Intervallo", d.interval)}
</>
);
case "cleanup":
return (
<>
{renderDetailRow("Detection vecchie (>48h)", d.oldDetections48h, d.oldDetections48h > 0 ? "destructive" : "default")}
{renderDetailRow("Detection totali", d.totalDetections)}
{renderDetailRow("Intervallo", d.interval)}
{d.warning && renderDetailRow("Avviso", d.warning, "destructive")}
</>
);
case "listFetcher":
return (
<>
{renderDetailRow("Liste totali", d.totalLists)}
{renderDetailRow("Liste attive", d.enabledLists)}
{d.lastFetched && renderDetailRow("Ultimo fetch", typeof d.lastFetched === 'string' && d.lastFetched !== 'Mai' ? new Date(d.lastFetched).toLocaleString('it-IT') : d.lastFetched)}
{d.hoursSinceLastFetch && renderDetailRow("Ore dall'ultimo fetch", d.hoursSinceLastFetch + "h", parseFloat(d.hoursSinceLastFetch) < 1 ? "default" : "destructive")}
{renderDetailRow("Intervallo", d.interval)}
</>
);
case "mlTraining":
return (
<>
{d.lastTraining && renderDetailRow("Ultimo training", typeof d.lastTraining === 'string' && d.lastTraining !== 'Mai' ? new Date(d.lastTraining).toLocaleString('it-IT') : d.lastTraining)}
{d.daysSinceLastTraining && renderDetailRow("Giorni dall'ultimo", d.daysSinceLastTraining, parseFloat(d.daysSinceLastTraining) < 8 ? "default" : "destructive")}
{d.lastStatus && renderDetailRow("Stato ultimo training", d.lastStatus, d.lastStatus === 'completed' ? "default" : "destructive")}
{d.lastModel && renderDetailRow("Modello", d.lastModel)}
{d.recordsProcessed && renderDetailRow("Record processati", d.recordsProcessed.toLocaleString())}
{renderDetailRow("Intervallo", d.interval)}
</>
);
default:
return d.error ? renderDetailRow("Errore", d.error, "destructive") : null;
}
};
const coreServices = ["nodeBackend", "mlBackend", "database", "syslogParser"];
const timerServices = ["autoBlock", "analyticsAggregator", "cleanup", "listFetcher", "mlTraining"];
const renderServiceCard = (key: string, service: ServiceStatus) => {
const isControllable = controllableServices.includes(service.systemdUnit);
const isTimer = service.type === "timer";
const logCmd = getLogCommand(key);
return (
<Card key={key} data-testid={`card-service-${key}`}>
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-2">
<CardTitle className="flex items-center gap-2 text-base">
{getServiceIcon(key)}
<span className="truncate">{service.name}</span>
</CardTitle>
<div className="flex items-center gap-2 shrink-0">
{isTimer && <Timer className="h-4 w-4 text-muted-foreground" />}
{getStatusIndicator(service)}
</div>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between gap-2">
<span className="text-sm text-muted-foreground">Stato:</span>
{getStatusBadge(service, key)}
</div>
<div className="flex items-center justify-between gap-2">
<span className="text-sm text-muted-foreground">Systemd:</span>
<Badge variant="outline" className="text-xs font-mono">
{service.systemdUnit}{isTimer ? '.timer' : '.service'}
</Badge>
</div>
{renderServiceDetails(key, service)}
{isControllable && (
<div className="space-y-2 pt-2 border-t">
<p className="text-xs font-medium text-muted-foreground">Controlli:</p>
<div className="flex gap-2 flex-wrap">
<Button
size="sm"
variant="outline"
onClick={() => handleServiceAction(service.systemdUnit, "restart")}
disabled={serviceControlMutation.isPending}
data-testid={`button-restart-${key}`}
>
<RotateCw className="h-3 w-3 mr-1" />
Restart
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleServiceAction(service.systemdUnit, "start")}
disabled={serviceControlMutation.isPending || service.status === 'running'}
data-testid={`button-start-${key}`}
>
<Play className="h-3 w-3 mr-1" />
Start
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleServiceAction(service.systemdUnit, "stop")}
disabled={serviceControlMutation.isPending || service.status === 'offline'}
data-testid={`button-stop-${key}`}
>
<Square className="h-3 w-3 mr-1" />
Stop
</Button>
</div>
</div>
)}
{logCmd && (
<div className="p-2 bg-muted rounded-md">
<p className="text-xs font-medium text-muted-foreground mb-1">Log:</p>
<code className="text-xs font-mono break-all" data-testid={`code-log-${key}`}>{logCmd}</code>
</div>
)}
</CardContent>
</Card>
);
}; };
return ( return (
<div className="flex flex-col gap-6 p-6" data-testid="page-services"> <div className="flex flex-col gap-6 p-6" data-testid="page-services">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between gap-4 flex-wrap">
<div> <div>
<h1 className="text-3xl font-semibold" data-testid="text-services-title">Gestione Servizi</h1> <h1 className="text-3xl font-semibold" data-testid="text-services-title">Gestione Servizi</h1>
<p className="text-muted-foreground" data-testid="text-services-subtitle"> <p className="text-muted-foreground" data-testid="text-services-subtitle">
Monitoraggio e controllo dei servizi IDS Monitoraggio e controllo di tutti i servizi IDS
</p> </p>
</div> </div>
<Button onClick={() => refetch()} variant="outline" data-testid="button-refresh"> <Button onClick={() => refetch()} variant="outline" data-testid="button-refresh">
@ -100,303 +315,40 @@ export default function ServicesPage() {
</Button> </Button>
</div> </div>
<Alert data-testid="alert-server-instructions"> {isLoading && (
<AlertCircle className="h-4 w-4" /> <div className="text-center py-8 text-muted-foreground">Caricamento stato servizi...</div>
<AlertTitle>Gestione Servizi Systemd</AlertTitle> )}
<AlertDescription>
I servizi IDS sono gestiti da systemd sul server AlmaLinux.
Usa i pulsanti qui sotto per controllarli oppure esegui i comandi systemctl direttamente sul server.
</AlertDescription>
</Alert>
{/* Services Grid */} {servicesStatus && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <>
{/* ML Backend Service */} <div>
<Card data-testid="card-ml-backend-service"> <h2 className="text-lg font-semibold mb-3 flex items-center gap-2">
<CardHeader> <Server className="h-5 w-5" />
<CardTitle className="flex items-center gap-2 text-lg"> Servizi Core
<Brain className="h-5 w-5" /> </h2>
ML Backend Python <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
{servicesStatus && getStatusIndicator(servicesStatus.services.mlBackend)} {coreServices.map((key) => {
</CardTitle> const service = (servicesStatus.services as any)[key];
</CardHeader> return service ? renderServiceCard(key, service) : null;
<CardContent className="space-y-4"> })}
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Stato:</span>
{servicesStatus && getStatusBadge(servicesStatus.services.mlBackend)}
</div> </div>
</div>
{servicesStatus?.services.mlBackend.details?.modelLoaded !== undefined && ( <div>
<div className="flex items-center justify-between"> <h2 className="text-lg font-semibold mb-3 flex items-center gap-2">
<span className="text-sm text-muted-foreground">Modello ML:</span> <Clock className="h-5 w-5" />
<Badge variant={servicesStatus.services.mlBackend.details.modelLoaded ? "default" : "secondary"}> Timer Systemd (Attivita Periodiche)
{servicesStatus.services.mlBackend.details.modelLoaded ? "Caricato" : "Non Caricato"} </h2>
</Badge> <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
</div> {timerServices.map((key) => {
)} const service = (servicesStatus.services as any)[key];
return service ? renderServiceCard(key, service) : null;
{/* Service Controls */} })}
<div className="mt-4 space-y-2">
<p className="text-xs font-medium mb-2">Controlli Servizio:</p>
<div className="flex gap-2 flex-wrap">
<Button
size="sm"
variant="outline"
onClick={() => handleServiceAction("ids-ml-backend", "start")}
disabled={serviceControlMutation.isPending || servicesStatus?.services.mlBackend.status === 'running'}
data-testid="button-start-ml"
>
<Play className="h-3 w-3 mr-1" />
Start
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleServiceAction("ids-ml-backend", "stop")}
disabled={serviceControlMutation.isPending || servicesStatus?.services.mlBackend.status === 'offline'}
data-testid="button-stop-ml"
>
<Square className="h-3 w-3 mr-1" />
Stop
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleServiceAction("ids-ml-backend", "restart")}
disabled={serviceControlMutation.isPending}
data-testid="button-restart-ml"
>
<RotateCw className="h-3 w-3 mr-1" />
Restart
</Button>
</div>
</div> </div>
</div>
</>
)}
{/* Manual Commands (fallback) */}
<div className="mt-4 p-3 bg-muted rounded-lg">
<p className="text-xs font-medium mb-2">Comando systemctl (sul server):</p>
<code className="text-xs bg-background p-2 rounded block font-mono" data-testid="code-systemctl-ml">
sudo systemctl {servicesStatus?.services.mlBackend.status === 'offline' ? 'start' : 'restart'} ids-ml-backend
</code>
</div>
<div className="mt-4 p-3 bg-muted rounded-lg">
<p className="text-xs font-medium mb-2">Log:</p>
<code className="text-xs bg-background p-2 rounded block font-mono" data-testid="code-log-ml">
tail -f /var/log/ids/backend.log
</code>
</div>
</CardContent>
</Card>
{/* Database Service */}
<Card data-testid="card-database-service">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Database className="h-5 w-5" />
PostgreSQL Database
{servicesStatus && getStatusIndicator(servicesStatus.services.database)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Stato:</span>
{servicesStatus && getStatusBadge(servicesStatus.services.database)}
</div>
{servicesStatus?.services.database.status === 'running' && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Connessione:</span>
<Badge variant="default" className="bg-green-600">Connesso</Badge>
</div>
)}
<div className="mt-4 p-3 bg-muted rounded-lg">
<p className="text-xs font-medium mb-2">Verifica status:</p>
<code className="text-xs bg-background p-2 rounded block font-mono" data-testid="code-status-db">
systemctl status postgresql-16
</code>
</div>
{servicesStatus?.services.database.status === 'error' && (
<div className="mt-4 p-3 bg-muted rounded-lg">
<p className="text-xs font-medium mb-2">Riavvia database:</p>
<code className="text-xs bg-background p-2 rounded block font-mono" data-testid="code-restart-db">
sudo systemctl restart postgresql-16
</code>
</div>
)}
<div className="mt-4 p-3 bg-muted rounded-lg">
<p className="text-xs font-medium mb-2">Log:</p>
<code className="text-xs bg-background p-2 rounded block font-mono" data-testid="code-log-db">
sudo journalctl -u postgresql-16 -f
</code>
</div>
</CardContent>
</Card>
{/* Syslog Parser Service */}
<Card data-testid="card-syslog-parser-service">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<FileText className="h-5 w-5" />
Syslog Parser
{servicesStatus && getStatusIndicator(servicesStatus.services.syslogParser)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Stato:</span>
{servicesStatus && getStatusBadge(servicesStatus.services.syslogParser)}
</div>
{servicesStatus?.services.syslogParser.details?.pid && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">PID Processo:</span>
<Badge variant="outline" className="font-mono">
{servicesStatus.services.syslogParser.details.pid}
</Badge>
</div>
)}
{servicesStatus?.services.syslogParser.details?.systemd_unit && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Systemd Unit:</span>
<Badge variant="outline" className="font-mono text-xs">
{servicesStatus.services.syslogParser.details.systemd_unit}
</Badge>
</div>
)}
{/* Service Controls */}
<div className="mt-4 space-y-2">
<p className="text-xs font-medium mb-2">Controlli Servizio:</p>
<div className="flex gap-2 flex-wrap">
<Button
size="sm"
variant="outline"
onClick={() => handleServiceAction("ids-syslog-parser", "start")}
disabled={serviceControlMutation.isPending || servicesStatus?.services.syslogParser.status === 'running'}
data-testid="button-start-parser"
>
<Play className="h-3 w-3 mr-1" />
Start
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleServiceAction("ids-syslog-parser", "stop")}
disabled={serviceControlMutation.isPending || servicesStatus?.services.syslogParser.status === 'offline'}
data-testid="button-stop-parser"
>
<Square className="h-3 w-3 mr-1" />
Stop
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleServiceAction("ids-syslog-parser", "restart")}
disabled={serviceControlMutation.isPending}
data-testid="button-restart-parser"
>
<RotateCw className="h-3 w-3 mr-1" />
Restart
</Button>
</div>
</div>
{/* Manual Commands (fallback) */}
<div className="mt-4 p-3 bg-muted rounded-lg">
<p className="text-xs font-medium mb-2">Comando systemctl (sul server):</p>
<code className="text-xs bg-background p-2 rounded block font-mono" data-testid="code-systemctl-parser">
sudo systemctl {servicesStatus?.services.syslogParser.status === 'offline' ? 'start' : 'restart'} ids-syslog-parser
</code>
</div>
<div className="mt-4 p-3 bg-muted rounded-lg">
<p className="text-xs font-medium mb-2">Log:</p>
<code className="text-xs bg-background p-2 rounded block font-mono" data-testid="code-log-parser">
tail -f /var/log/ids/syslog_parser.log
</code>
</div>
</CardContent>
</Card>
{/* Analytics Aggregator Service */}
<Card data-testid="card-analytics-aggregator-service">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Activity className="h-5 w-5" />
Analytics Aggregator
{servicesStatus && getStatusIndicator(servicesStatus.services.analyticsAggregator)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Stato:</span>
{servicesStatus && getStatusBadge(servicesStatus.services.analyticsAggregator)}
</div>
{servicesStatus?.services.analyticsAggregator.details?.lastRun && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Ultima Aggregazione:</span>
<Badge variant="outline" className="text-xs">
{new Date(servicesStatus.services.analyticsAggregator.details.lastRun).toLocaleString('it-IT')}
</Badge>
</div>
)}
{servicesStatus?.services.analyticsAggregator.details?.hoursSinceLastRun && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Ore dall'ultimo run:</span>
<Badge variant={parseFloat(servicesStatus.services.analyticsAggregator.details.hoursSinceLastRun) < 2 ? "default" : "destructive"}>
{servicesStatus.services.analyticsAggregator.details.hoursSinceLastRun}h
</Badge>
</div>
)}
{/* CRITICAL ALERT: Aggregator idle for too long */}
{servicesStatus?.services.analyticsAggregator.details?.hoursSinceLastRun &&
parseFloat(servicesStatus.services.analyticsAggregator.details.hoursSinceLastRun) > 2 && (
<Alert variant="destructive" className="mt-2" data-testid="alert-aggregator-idle">
<AlertCircle className="h-4 w-4" />
<AlertTitle className="text-sm font-semibold"> Timer Systemd Non Attivo</AlertTitle>
<AlertDescription className="text-xs mt-1">
<p className="mb-2">L'aggregatore non esegue da {servicesStatus.services.analyticsAggregator.details.hoursSinceLastRun}h! Dashboard e Analytics bloccati.</p>
<p className="font-semibold">Soluzione Immediata (sul server):</p>
<code className="block bg-destructive-foreground/10 p-2 rounded mt-1 font-mono text-xs">
sudo /opt/ids/deployment/setup_analytics_timer.sh
</code>
</AlertDescription>
</Alert>
)}
<div className="mt-4 p-3 bg-muted rounded-lg">
<p className="text-xs font-medium mb-2">Verifica timer:</p>
<code className="text-xs bg-background p-2 rounded block font-mono" data-testid="code-status-aggregator">
systemctl status ids-analytics-aggregator.timer
</code>
</div>
<div className="mt-4 p-3 bg-muted rounded-lg">
<p className="text-xs font-medium mb-2">Avvia aggregazione manualmente:</p>
<code className="text-xs bg-background p-2 rounded block font-mono" data-testid="code-run-aggregator">
cd /opt/ids && ./deployment/run_analytics.sh
</code>
</div>
<div className="mt-4 p-3 bg-muted rounded-lg">
<p className="text-xs font-medium mb-2">Log:</p>
<code className="text-xs bg-background p-2 rounded block font-mono" data-testid="code-log-aggregator">
journalctl -u ids-analytics-aggregator.timer -f
</code>
</div>
</CardContent>
</Card>
</div>
{/* Additional Commands */}
<Card data-testid="card-additional-commands"> <Card data-testid="card-additional-commands">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
@ -406,30 +358,27 @@ export default function ServicesPage() {
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div> <div>
<p className="text-sm font-medium mb-2">Verifica tutti i processi IDS attivi:</p> <p className="text-sm font-medium mb-2">Stato di tutti i servizi IDS:</p>
<code className="text-xs bg-muted p-2 rounded block font-mono" data-testid="code-check-processes"> <code className="text-xs bg-muted p-2 rounded-md block font-mono" data-testid="code-all-services">
ps aux | grep -E "python.*(main|syslog_parser)" | grep -v grep systemctl list-units 'ids-*' --all
</code>
</div>
<div>
<p className="text-sm font-medium mb-2">Stato di tutti i timer IDS:</p>
<code className="text-xs bg-muted p-2 rounded-md block font-mono" data-testid="code-all-timers">
systemctl list-timers 'ids-*' --all
</code> </code>
</div> </div>
<div> <div>
<p className="text-sm font-medium mb-2">Verifica log RSyslog (ricezione log MikroTik):</p> <p className="text-sm font-medium mb-2">Verifica log RSyslog (ricezione log MikroTik):</p>
<code className="text-xs bg-muted p-2 rounded block font-mono" data-testid="code-check-rsyslog"> <code className="text-xs bg-muted p-2 rounded-md block font-mono" data-testid="code-check-rsyslog">
tail -f /var/log/mikrotik/raw.log tail -f /var/log/mikrotik/raw.log
</code> </code>
</div> </div>
<div> <div>
<p className="text-sm font-medium mb-2">Esegui training manuale ML:</p> <p className="text-sm font-medium mb-2">Verifica processi IDS attivi:</p>
<code className="text-xs bg-muted p-2 rounded block font-mono" data-testid="code-manual-training"> <code className="text-xs bg-muted p-2 rounded-md block font-mono" data-testid="code-check-processes">
curl -X POST http://localhost:8000/train -H "Content-Type: application/json" -d '&#123;"max_records": 10000, "hours_back": 24&#125;' ps aux | grep -E "python.*(main|syslog_parser)" | grep -v grep
</code>
</div>
<div>
<p className="text-sm font-medium mb-2">Verifica storico training nel database:</p>
<code className="text-xs bg-muted p-2 rounded block font-mono" data-testid="code-check-training">
psql $DATABASE_URL -c "SELECT * FROM training_history ORDER BY trained_at DESC LIMIT 5;"
</code> </code>
</div> </div>
</CardContent> </CardContent>

View File

@ -780,39 +780,43 @@ export async function registerRoutes(app: Express): Promise<Server> {
// Services monitoring // Services monitoring
app.get("/api/services/status", async (req, res) => { app.get("/api/services/status", async (req, res) => {
try { try {
const mkService = (name: string) => ({ name, status: "unknown" as string, healthy: false, details: null as any, systemdUnit: "" as string, type: "service" as string });
const services = { const services = {
mlBackend: { name: "ML Backend Python", status: "unknown", healthy: false, details: null as any }, nodeBackend: { ...mkService("Node.js Backend"), systemdUnit: "ids-backend", type: "service" },
database: { name: "PostgreSQL Database", status: "unknown", healthy: false, details: null as any }, mlBackend: { ...mkService("ML Backend Python"), systemdUnit: "ids-ml-backend", type: "service" },
syslogParser: { name: "Syslog Parser", status: "unknown", healthy: false, details: null as any }, database: { ...mkService("PostgreSQL Database"), systemdUnit: "postgresql-16", type: "service" },
analyticsAggregator: { name: "Analytics Aggregator Timer", status: "unknown", healthy: false, details: null as any }, syslogParser: { ...mkService("Syslog Parser"), systemdUnit: "ids-syslog-parser", type: "service" },
analyticsAggregator: { ...mkService("Analytics Aggregator"), systemdUnit: "ids-analytics-aggregator", type: "timer" },
autoBlock: { ...mkService("Auto Block"), systemdUnit: "ids-auto-block", type: "timer" },
cleanup: { ...mkService("Cleanup Detections"), systemdUnit: "ids-cleanup", type: "timer" },
listFetcher: { ...mkService("Public Lists Fetcher"), systemdUnit: "ids-list-fetcher", type: "timer" },
mlTraining: { ...mkService("ML Training Settimanale"), systemdUnit: "ids-ml-training", type: "timer" },
}; };
// Node.js Backend - always running if this endpoint responds
services.nodeBackend.status = "running";
services.nodeBackend.healthy = true;
services.nodeBackend.details = { port: 5000, uptime: process.uptime().toFixed(0) + "s" };
// Check ML Backend Python // Check ML Backend Python
try { try {
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000); const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch(`${ML_BACKEND_URL}/health`, { signal: controller.signal });
const response = await fetch(`${ML_BACKEND_URL}/health`, {
signal: controller.signal,
});
clearTimeout(timeout); clearTimeout(timeout);
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
services.mlBackend.status = "running"; services.mlBackend.status = "running";
services.mlBackend.healthy = true; services.mlBackend.healthy = true;
services.mlBackend.details = { services.mlBackend.details = { modelLoaded: data.ml_model === "loaded", timestamp: data.timestamp };
modelLoaded: data.ml_model === "loaded",
timestamp: data.timestamp,
};
} else { } else {
services.mlBackend.status = "error"; services.mlBackend.status = "error";
services.mlBackend.details = { error: `HTTP ${response.status}` }; services.mlBackend.details = { error: `HTTP ${response.status}` };
} }
} catch (error: any) { } catch (error: any) {
services.mlBackend.status = "offline"; services.mlBackend.status = "offline";
services.mlBackend.details = { error: error.code === 'ECONNREFUSED' ? "Connection refused" : error.message }; services.mlBackend.details = { error: error.code === 'ECONNREFUSED' ? "Connessione rifiutata" : error.message };
} }
// Check Database // Check Database
@ -828,87 +832,156 @@ export async function registerRoutes(app: Express): Promise<Server> {
services.database.details = { error: error.message }; services.database.details = { error: error.message };
} }
// Check Syslog Parser via database (independent of ML Backend) // Check Syslog Parser via database
try { try {
const recentLogsResult = await db.execute( const recentLogsResult = await db.execute(
sql`SELECT COUNT(*) as count, MAX(timestamp) as last_log sql`SELECT COUNT(*) as count, MAX(timestamp) as last_log FROM network_logs WHERE timestamp > NOW() - INTERVAL '30 minutes'`
FROM network_logs
WHERE timestamp > NOW() - INTERVAL '30 minutes'`
); );
const logRows = (recentLogsResult as any).rows || recentLogsResult; const logRows = (recentLogsResult as any).rows || recentLogsResult;
const recentLogCount = parseInt(logRows[0]?.count || "0"); const recentLogCount = parseInt(logRows[0]?.count || "0");
const lastLogTime = logRows[0]?.last_log; const lastLogTime = logRows[0]?.last_log;
if (recentLogCount > 0) { if (recentLogCount > 0) {
services.syslogParser.status = "running"; services.syslogParser.status = "running";
services.syslogParser.healthy = true; services.syslogParser.healthy = true;
services.syslogParser.details = { services.syslogParser.details = { recentLogs30min: recentLogCount, lastLog: lastLogTime };
recentLogs30min: recentLogCount,
lastLog: lastLogTime,
};
} else { } else {
const lastLogEverResult = await db.execute( const lastLogEverResult = await db.execute(sql`SELECT MAX(timestamp) as last_log FROM network_logs`);
sql`SELECT MAX(timestamp) as last_log FROM network_logs`
);
const lastLogEverRows = (lastLogEverResult as any).rows || lastLogEverResult; const lastLogEverRows = (lastLogEverResult as any).rows || lastLogEverResult;
const lastLogEver = lastLogEverRows[0]?.last_log;
services.syslogParser.status = "offline"; services.syslogParser.status = "offline";
services.syslogParser.healthy = false; services.syslogParser.healthy = false;
services.syslogParser.details = { services.syslogParser.details = { recentLogs30min: 0, lastLog: lastLogEverRows[0]?.last_log || "Mai", warning: "Nessun log negli ultimi 30 minuti" };
recentLogs30min: 0,
lastLog: lastLogEver || "Never",
warning: "No logs received in last 30 minutes",
};
} }
} catch (error: any) { } catch (error: any) {
services.syslogParser.status = "error"; services.syslogParser.status = "error";
services.syslogParser.healthy = false;
services.syslogParser.details = { error: error.message }; services.syslogParser.details = { error: error.message };
} }
// Check Analytics Aggregator (via last record timestamp) // Check Analytics Aggregator (via last record timestamp)
try { try {
const latestAnalytics = await db const latestAnalytics = await db.select().from(networkAnalytics).orderBy(desc(networkAnalytics.date), desc(networkAnalytics.hour)).limit(1);
.select()
.from(networkAnalytics)
.orderBy(desc(networkAnalytics.date), desc(networkAnalytics.hour))
.limit(1);
if (latestAnalytics.length > 0) { if (latestAnalytics.length > 0) {
const lastRun = new Date(latestAnalytics[0].date); const lastRun = new Date(latestAnalytics[0].date);
const lastTimestamp = lastRun.toISOString(); const hoursSince = (Date.now() - lastRun.getTime()) / (1000 * 60 * 60);
const hoursSinceLastRun = (Date.now() - lastRun.getTime()) / (1000 * 60 * 60); if (hoursSince < 2) {
if (hoursSinceLastRun < 2) {
services.analyticsAggregator.status = "running"; services.analyticsAggregator.status = "running";
services.analyticsAggregator.healthy = true; services.analyticsAggregator.healthy = true;
services.analyticsAggregator.details = { services.analyticsAggregator.details = { lastRun: latestAnalytics[0].date, hoursSinceLastRun: hoursSince.toFixed(1) };
lastRun: latestAnalytics[0].date,
lastTimestamp,
hoursSinceLastRun: hoursSinceLastRun.toFixed(1),
};
} else { } else {
services.analyticsAggregator.status = "idle"; services.analyticsAggregator.status = "idle";
services.analyticsAggregator.healthy = false; services.analyticsAggregator.details = { lastRun: latestAnalytics[0].date, hoursSinceLastRun: hoursSince.toFixed(1), warning: "Nessuna aggregazione nelle ultime 2 ore" };
services.analyticsAggregator.details = {
lastRun: latestAnalytics[0].date,
lastTimestamp,
hoursSinceLastRun: hoursSinceLastRun.toFixed(1),
warning: "No aggregation in last 2 hours",
};
} }
} else { } else {
services.analyticsAggregator.status = "idle"; services.analyticsAggregator.status = "idle";
services.analyticsAggregator.healthy = false; services.analyticsAggregator.details = { error: "Nessun dato analytics trovato" };
services.analyticsAggregator.details = { error: "No analytics data found" };
} }
} catch (error: any) { } catch (error: any) {
services.analyticsAggregator.status = "error"; services.analyticsAggregator.status = "error";
services.analyticsAggregator.healthy = false;
services.analyticsAggregator.details = { error: error.message }; services.analyticsAggregator.details = { error: error.message };
} }
// Check Auto Block (via recent blocked detections)
try {
const recentBlockResult = await db.execute(
sql`SELECT COUNT(*) as count, MAX(detected_at) as last_block FROM detections WHERE blocked = true AND detected_at > NOW() - INTERVAL '10 minutes'`
);
const blockRows = (recentBlockResult as any).rows || recentBlockResult;
const recentBlocks = parseInt(blockRows[0]?.count || "0");
const lastBlock = blockRows[0]?.last_block;
const totalBlockedResult = await db.execute(sql`SELECT COUNT(*) as count FROM detections WHERE blocked = true`);
const totalBlockedRows = (totalBlockedResult as any).rows || totalBlockedResult;
const totalBlocked = parseInt(totalBlockedRows[0]?.count || "0");
services.autoBlock.status = recentBlocks > 0 ? "running" : "idle";
services.autoBlock.healthy = true;
services.autoBlock.details = {
recentBlocks10min: recentBlocks,
totalBlocked,
lastBlock: lastBlock || "Mai",
interval: "ogni 5 minuti"
};
} catch (error: any) {
services.autoBlock.status = "error";
services.autoBlock.details = { error: error.message };
}
// Check Cleanup (via absence of old detections)
try {
const oldDetResult = await db.execute(
sql`SELECT COUNT(*) as count FROM detections WHERE detected_at < NOW() - INTERVAL '48 hours'`
);
const oldRows = (oldDetResult as any).rows || oldDetResult;
const oldDetections = parseInt(oldRows[0]?.count || "0");
const totalDetResult = await db.execute(sql`SELECT COUNT(*) as count FROM detections`);
const totalRows = (totalDetResult as any).rows || totalDetResult;
const totalDetections = parseInt(totalRows[0]?.count || "0");
services.cleanup.status = oldDetections === 0 ? "running" : "idle";
services.cleanup.healthy = oldDetections === 0;
services.cleanup.details = {
oldDetections48h: oldDetections,
totalDetections,
interval: "ogni ora",
warning: oldDetections > 0 ? `${oldDetections} detection vecchie non ancora pulite` : undefined
};
} catch (error: any) {
services.cleanup.status = "error";
services.cleanup.details = { error: error.message };
}
// Check List Fetcher (via public lists last_updated)
try {
const listsResult = await db.execute(
sql`SELECT COUNT(*) as total,
COUNT(*) FILTER (WHERE enabled = true) as enabled,
MAX(last_fetch) as last_fetch
FROM public_lists`
);
const listRows = (listsResult as any).rows || listsResult;
const totalLists = parseInt(listRows[0]?.total || "0");
const enabledLists = parseInt(listRows[0]?.enabled || "0");
const lastFetched = listRows[0]?.last_fetch;
if (lastFetched) {
const hoursSince = (Date.now() - new Date(lastFetched).getTime()) / (1000 * 60 * 60);
services.listFetcher.status = hoursSince < 1 ? "running" : "idle";
services.listFetcher.healthy = hoursSince < 1;
services.listFetcher.details = { totalLists, enabledLists, lastFetched, hoursSinceLastFetch: hoursSince.toFixed(1), interval: "ogni 10 minuti" };
} else {
services.listFetcher.status = "idle";
services.listFetcher.details = { totalLists, enabledLists, lastFetched: "Mai", interval: "ogni 10 minuti" };
}
} catch (error: any) {
services.listFetcher.status = "error";
services.listFetcher.details = { error: error.message };
}
// Check ML Training (via training history)
try {
const latestTraining = await db.select().from(trainingHistory).orderBy(desc(trainingHistory.trainedAt)).limit(1);
if (latestTraining.length > 0) {
const lastTrainDate = new Date(latestTraining[0].trainedAt);
const daysSince = (Date.now() - lastTrainDate.getTime()) / (1000 * 60 * 60 * 24);
services.mlTraining.status = daysSince < 8 ? "running" : "idle";
services.mlTraining.healthy = daysSince < 8;
services.mlTraining.details = {
lastTraining: latestTraining[0].trainedAt,
daysSinceLastTraining: daysSince.toFixed(1),
lastStatus: latestTraining[0].status,
lastModel: latestTraining[0].modelVersion,
recordsProcessed: latestTraining[0].recordsProcessed,
interval: "settimanale"
};
} else {
services.mlTraining.status = "idle";
services.mlTraining.details = { lastTraining: "Mai", interval: "settimanale" };
}
} catch (error: any) {
services.mlTraining.status = "error";
services.mlTraining.details = { error: error.message };
}
res.json({ services }); res.json({ services });
} catch (error: any) { } catch (error: any) {
res.status(500).json({ error: "Failed to check services status" }); res.status(500).json({ error: "Failed to check services status" });
@ -916,7 +989,11 @@ export async function registerRoutes(app: Express): Promise<Server> {
}); });
// Service Control Endpoints (Secured - only allow specific systemd operations) // Service Control Endpoints (Secured - only allow specific systemd operations)
const ALLOWED_SERVICES = ["ids-ml-backend", "ids-syslog-parser"]; const ALLOWED_SERVICES = [
"ids-ml-backend", "ids-syslog-parser", "ids-backend",
"ids-analytics-aggregator", "ids-auto-block", "ids-cleanup",
"ids-list-fetcher", "ids-ml-training"
];
const ALLOWED_ACTIONS = ["start", "stop", "restart", "status"]; const ALLOWED_ACTIONS = ["start", "stop", "restart", "status"];
app.post("/api/services/:service/:action", async (req, res) => { app.post("/api/services/:service/:action", async (req, res) => {