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:
parent
6ce60ed5d3
commit
4118d60d6d
@ -2,25 +2,21 @@ import { useQuery, useMutation } 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, Brain, Database, FileText, Terminal, RefreshCw, AlertCircle, Play, Square, RotateCw } from "lucide-react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Activity, Brain, Database, FileText, Terminal, RefreshCw, Play, Square, RotateCw, Shield, Trash2, ListChecks, GraduationCap, Server, Clock, Timer } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { queryClient, apiRequest } from "@/lib/queryClient";
|
||||
|
||||
interface ServiceStatus {
|
||||
name: string;
|
||||
status: "running" | "idle" | "offline" | "error" | "unknown";
|
||||
status: string;
|
||||
healthy: boolean;
|
||||
details: any;
|
||||
systemdUnit: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface ServicesStatusResponse {
|
||||
services: {
|
||||
mlBackend: ServiceStatus;
|
||||
database: ServiceStatus;
|
||||
syslogParser: ServiceStatus;
|
||||
analyticsAggregator: ServiceStatus;
|
||||
};
|
||||
services: Record<string, ServiceStatus>;
|
||||
}
|
||||
|
||||
export default function ServicesPage() {
|
||||
@ -28,10 +24,9 @@ export default function ServicesPage() {
|
||||
|
||||
const { data: servicesStatus, isLoading, refetch } = useQuery<ServicesStatusResponse>({
|
||||
queryKey: ["/api/services/status"],
|
||||
refetchInterval: 5000, // Refresh every 5s
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
// Mutation for service control
|
||||
const serviceControlMutation = useMutation({
|
||||
mutationFn: async ({ service, action }: { service: string; action: string }) => {
|
||||
return apiRequest("POST", `/api/services/${service}/${action}`);
|
||||
@ -39,9 +34,8 @@ export default function ServicesPage() {
|
||||
onSuccess: (data, variables) => {
|
||||
toast({
|
||||
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(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/services/status"] });
|
||||
}, 2000);
|
||||
@ -59,39 +53,260 @@ export default function ServicesPage() {
|
||||
serviceControlMutation.mutate({ service, action });
|
||||
};
|
||||
|
||||
const getStatusBadge = (service: ServiceStatus) => {
|
||||
const getStatusBadge = (service: ServiceStatus, key: string) => {
|
||||
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') {
|
||||
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') {
|
||||
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') {
|
||||
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) => {
|
||||
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') {
|
||||
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 (
|
||||
<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>
|
||||
<h1 className="text-3xl font-semibold" data-testid="text-services-title">Gestione Servizi</h1>
|
||||
<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>
|
||||
</div>
|
||||
<Button onClick={() => refetch()} variant="outline" data-testid="button-refresh">
|
||||
@ -100,303 +315,40 @@ export default function ServicesPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Alert data-testid="alert-server-instructions">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<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 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* ML Backend Service */}
|
||||
<Card data-testid="card-ml-backend-service">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Brain className="h-5 w-5" />
|
||||
ML Backend Python
|
||||
{servicesStatus && getStatusIndicator(servicesStatus.services.mlBackend)}
|
||||
</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.mlBackend)}
|
||||
</div>
|
||||
|
||||
{servicesStatus?.services.mlBackend.details?.modelLoaded !== undefined && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Modello ML:</span>
|
||||
<Badge variant={servicesStatus.services.mlBackend.details.modelLoaded ? "default" : "secondary"}>
|
||||
{servicesStatus.services.mlBackend.details.modelLoaded ? "Caricato" : "Non Caricato"}
|
||||
</Badge>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="text-center py-8 text-muted-foreground">Caricamento stato servizi...</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-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>
|
||||
{servicesStatus && (
|
||||
<>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-3 flex items-center gap-2">
|
||||
<Server className="h-5 w-5" />
|
||||
Servizi Core
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
{coreServices.map((key) => {
|
||||
const service = (servicesStatus.services as any)[key];
|
||||
return service ? renderServiceCard(key, service) : null;
|
||||
})}
|
||||
</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>
|
||||
<h2 className="text-lg font-semibold mb-3 flex items-center gap-2">
|
||||
<Clock className="h-5 w-5" />
|
||||
Timer Systemd (Attivita Periodiche)
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{timerServices.map((key) => {
|
||||
const service = (servicesStatus.services as any)[key];
|
||||
return service ? renderServiceCard(key, service) : null;
|
||||
})}
|
||||
</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">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
@ -406,30 +358,27 @@ export default function ServicesPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-2">Verifica tutti i processi IDS attivi:</p>
|
||||
<code className="text-xs bg-muted p-2 rounded block font-mono" data-testid="code-check-processes">
|
||||
ps aux | grep -E "python.*(main|syslog_parser)" | grep -v grep
|
||||
<p className="text-sm font-medium mb-2">Stato di tutti i servizi IDS:</p>
|
||||
<code className="text-xs bg-muted p-2 rounded-md block font-mono" data-testid="code-all-services">
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-2">Esegui training manuale ML:</p>
|
||||
<code className="text-xs bg-muted p-2 rounded block font-mono" data-testid="code-manual-training">
|
||||
curl -X POST http://localhost:8000/train -H "Content-Type: application/json" -d '{"max_records": 10000, "hours_back": 24}'
|
||||
</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;"
|
||||
<p className="text-sm font-medium mb-2">Verifica processi IDS attivi:</p>
|
||||
<code className="text-xs bg-muted p-2 rounded-md block font-mono" data-testid="code-check-processes">
|
||||
ps aux | grep -E "python.*(main|syslog_parser)" | grep -v grep
|
||||
</code>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
199
server/routes.ts
199
server/routes.ts
@ -780,39 +780,43 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Services monitoring
|
||||
app.get("/api/services/status", async (req, res) => {
|
||||
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 = {
|
||||
mlBackend: { name: "ML Backend Python", status: "unknown", healthy: false, details: null as any },
|
||||
database: { name: "PostgreSQL Database", status: "unknown", healthy: false, details: null as any },
|
||||
syslogParser: { name: "Syslog Parser", status: "unknown", healthy: false, details: null as any },
|
||||
analyticsAggregator: { name: "Analytics Aggregator Timer", status: "unknown", healthy: false, details: null as any },
|
||||
nodeBackend: { ...mkService("Node.js Backend"), systemdUnit: "ids-backend", type: "service" },
|
||||
mlBackend: { ...mkService("ML Backend Python"), systemdUnit: "ids-ml-backend", type: "service" },
|
||||
database: { ...mkService("PostgreSQL Database"), systemdUnit: "postgresql-16", type: "service" },
|
||||
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
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
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);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
services.mlBackend.status = "running";
|
||||
services.mlBackend.healthy = true;
|
||||
services.mlBackend.details = {
|
||||
modelLoaded: data.ml_model === "loaded",
|
||||
timestamp: data.timestamp,
|
||||
};
|
||||
services.mlBackend.details = { modelLoaded: data.ml_model === "loaded", timestamp: data.timestamp };
|
||||
} else {
|
||||
services.mlBackend.status = "error";
|
||||
services.mlBackend.details = { error: `HTTP ${response.status}` };
|
||||
}
|
||||
} catch (error: any) {
|
||||
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
|
||||
@ -828,87 +832,156 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
services.database.details = { error: error.message };
|
||||
}
|
||||
|
||||
// Check Syslog Parser via database (independent of ML Backend)
|
||||
// Check Syslog Parser via database
|
||||
try {
|
||||
const recentLogsResult = await db.execute(
|
||||
sql`SELECT COUNT(*) as count, MAX(timestamp) as last_log
|
||||
FROM network_logs
|
||||
WHERE timestamp > NOW() - INTERVAL '30 minutes'`
|
||||
sql`SELECT COUNT(*) as count, MAX(timestamp) as last_log FROM network_logs WHERE timestamp > NOW() - INTERVAL '30 minutes'`
|
||||
);
|
||||
const logRows = (recentLogsResult as any).rows || recentLogsResult;
|
||||
const recentLogCount = parseInt(logRows[0]?.count || "0");
|
||||
const lastLogTime = logRows[0]?.last_log;
|
||||
|
||||
if (recentLogCount > 0) {
|
||||
services.syslogParser.status = "running";
|
||||
services.syslogParser.healthy = true;
|
||||
services.syslogParser.details = {
|
||||
recentLogs30min: recentLogCount,
|
||||
lastLog: lastLogTime,
|
||||
};
|
||||
services.syslogParser.details = { recentLogs30min: recentLogCount, lastLog: lastLogTime };
|
||||
} else {
|
||||
const lastLogEverResult = await db.execute(
|
||||
sql`SELECT MAX(timestamp) as last_log FROM network_logs`
|
||||
);
|
||||
const lastLogEverResult = await db.execute(sql`SELECT MAX(timestamp) as last_log FROM network_logs`);
|
||||
const lastLogEverRows = (lastLogEverResult as any).rows || lastLogEverResult;
|
||||
const lastLogEver = lastLogEverRows[0]?.last_log;
|
||||
|
||||
services.syslogParser.status = "offline";
|
||||
services.syslogParser.healthy = false;
|
||||
services.syslogParser.details = {
|
||||
recentLogs30min: 0,
|
||||
lastLog: lastLogEver || "Never",
|
||||
warning: "No logs received in last 30 minutes",
|
||||
};
|
||||
services.syslogParser.details = { recentLogs30min: 0, lastLog: lastLogEverRows[0]?.last_log || "Mai", warning: "Nessun log negli ultimi 30 minuti" };
|
||||
}
|
||||
} catch (error: any) {
|
||||
services.syslogParser.status = "error";
|
||||
services.syslogParser.healthy = false;
|
||||
services.syslogParser.details = { error: error.message };
|
||||
}
|
||||
|
||||
// Check Analytics Aggregator (via last record timestamp)
|
||||
try {
|
||||
const latestAnalytics = await db
|
||||
.select()
|
||||
.from(networkAnalytics)
|
||||
.orderBy(desc(networkAnalytics.date), desc(networkAnalytics.hour))
|
||||
.limit(1);
|
||||
|
||||
const latestAnalytics = await db.select().from(networkAnalytics).orderBy(desc(networkAnalytics.date), desc(networkAnalytics.hour)).limit(1);
|
||||
if (latestAnalytics.length > 0) {
|
||||
const lastRun = new Date(latestAnalytics[0].date);
|
||||
const lastTimestamp = lastRun.toISOString();
|
||||
const hoursSinceLastRun = (Date.now() - lastRun.getTime()) / (1000 * 60 * 60);
|
||||
|
||||
if (hoursSinceLastRun < 2) {
|
||||
const hoursSince = (Date.now() - lastRun.getTime()) / (1000 * 60 * 60);
|
||||
if (hoursSince < 2) {
|
||||
services.analyticsAggregator.status = "running";
|
||||
services.analyticsAggregator.healthy = true;
|
||||
services.analyticsAggregator.details = {
|
||||
lastRun: latestAnalytics[0].date,
|
||||
lastTimestamp,
|
||||
hoursSinceLastRun: hoursSinceLastRun.toFixed(1),
|
||||
};
|
||||
services.analyticsAggregator.details = { lastRun: latestAnalytics[0].date, hoursSinceLastRun: hoursSince.toFixed(1) };
|
||||
} else {
|
||||
services.analyticsAggregator.status = "idle";
|
||||
services.analyticsAggregator.healthy = false;
|
||||
services.analyticsAggregator.details = {
|
||||
lastRun: latestAnalytics[0].date,
|
||||
lastTimestamp,
|
||||
hoursSinceLastRun: hoursSinceLastRun.toFixed(1),
|
||||
warning: "No aggregation in last 2 hours",
|
||||
};
|
||||
services.analyticsAggregator.details = { lastRun: latestAnalytics[0].date, hoursSinceLastRun: hoursSince.toFixed(1), warning: "Nessuna aggregazione nelle ultime 2 ore" };
|
||||
}
|
||||
} else {
|
||||
services.analyticsAggregator.status = "idle";
|
||||
services.analyticsAggregator.healthy = false;
|
||||
services.analyticsAggregator.details = { error: "No analytics data found" };
|
||||
services.analyticsAggregator.details = { error: "Nessun dato analytics trovato" };
|
||||
}
|
||||
} catch (error: any) {
|
||||
services.analyticsAggregator.status = "error";
|
||||
services.analyticsAggregator.healthy = false;
|
||||
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 });
|
||||
} catch (error: any) {
|
||||
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)
|
||||
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"];
|
||||
|
||||
app.post("/api/services/:service/:action", async (req, res) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user