Add a destructive alert to the services page indicating when the analytics aggregator has been idle for too long, along with immediate solution instructions. Also creates a deployment checklist detailing the critical step of setting up the analytics aggregator timer. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 618f6e47-fbdc-49e2-b076-7366edc904a6 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/F6DiMv4
440 lines
19 KiB
TypeScript
440 lines
19 KiB
TypeScript
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 { useToast } from "@/hooks/use-toast";
|
|
import { queryClient, apiRequest } from "@/lib/queryClient";
|
|
|
|
interface ServiceStatus {
|
|
name: string;
|
|
status: "running" | "idle" | "offline" | "error" | "unknown";
|
|
healthy: boolean;
|
|
details: any;
|
|
}
|
|
|
|
interface ServicesStatusResponse {
|
|
services: {
|
|
mlBackend: ServiceStatus;
|
|
database: ServiceStatus;
|
|
syslogParser: ServiceStatus;
|
|
analyticsAggregator: ServiceStatus;
|
|
};
|
|
}
|
|
|
|
export default function ServicesPage() {
|
|
const { toast } = useToast();
|
|
|
|
const { data: servicesStatus, isLoading, refetch } = useQuery<ServicesStatusResponse>({
|
|
queryKey: ["/api/services/status"],
|
|
refetchInterval: 5000, // Refresh every 5s
|
|
});
|
|
|
|
// Mutation for service control
|
|
const serviceControlMutation = useMutation({
|
|
mutationFn: async ({ service, action }: { service: string; action: string }) => {
|
|
return apiRequest("POST", `/api/services/${service}/${action}`);
|
|
},
|
|
onSuccess: (data, variables) => {
|
|
toast({
|
|
title: "Operazione completata",
|
|
description: `Servizio ${variables.service}: ${variables.action} eseguito con successo`,
|
|
});
|
|
// Refresh status after 2 seconds
|
|
setTimeout(() => {
|
|
queryClient.invalidateQueries({ queryKey: ["/api/services/status"] });
|
|
}, 2000);
|
|
},
|
|
onError: (error: any, variables) => {
|
|
toast({
|
|
title: "Errore operazione",
|
|
description: error.message || `Impossibile eseguire ${variables.action} su ${variables.service}`,
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
const handleServiceAction = (service: string, action: string) => {
|
|
serviceControlMutation.mutate({ service, action });
|
|
};
|
|
|
|
const getStatusBadge = (service: ServiceStatus) => {
|
|
if (service.healthy) {
|
|
return <Badge variant="default" className="bg-green-600" data-testid={`badge-status-healthy`}>Online</Badge>;
|
|
}
|
|
if (service.status === 'idle') {
|
|
return <Badge variant="secondary" data-testid={`badge-status-idle`}>In Attesa</Badge>;
|
|
}
|
|
if (service.status === 'offline') {
|
|
return <Badge variant="destructive" data-testid={`badge-status-offline`}>Offline</Badge>;
|
|
}
|
|
if (service.status === 'error') {
|
|
return <Badge variant="destructive" data-testid={`badge-status-error`}>Errore</Badge>;
|
|
}
|
|
return <Badge variant="outline" data-testid={`badge-status-unknown`}>Sconosciuto</Badge>;
|
|
};
|
|
|
|
const getStatusIndicator = (service: ServiceStatus) => {
|
|
if (service.healthy) {
|
|
return <div className="h-3 w-3 rounded-full bg-green-500" />;
|
|
}
|
|
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-red-500" />;
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6 p-6" data-testid="page-services">
|
|
<div className="flex items-center justify-between">
|
|
<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
|
|
</p>
|
|
</div>
|
|
<Button onClick={() => refetch()} variant="outline" data-testid="button-refresh">
|
|
<RefreshCw className="h-4 w-4 mr-2" />
|
|
Aggiorna
|
|
</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>
|
|
)}
|
|
|
|
{/* 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>
|
|
|
|
{/* 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">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Terminal className="h-5 w-5" />
|
|
Comandi Utili
|
|
</CardTitle>
|
|
</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
|
|
</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">
|
|
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;"
|
|
</code>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|