From 4118d60d6dd42504c9742ec6377c1ec4d4fb27f3 Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Tue, 17 Feb 2026 09:09:26 +0000 Subject: [PATCH] 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 --- client/src/pages/Services.tsx | 613 ++++++++++++++++------------------ server/routes.ts | 199 +++++++---- 2 files changed, 419 insertions(+), 393 deletions(-) diff --git a/client/src/pages/Services.tsx b/client/src/pages/Services.tsx index 90f6a4b..e70af87 100644 --- a/client/src/pages/Services.tsx +++ b/client/src/pages/Services.tsx @@ -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; } export default function ServicesPage() { @@ -28,10 +24,9 @@ export default function ServicesPage() { const { data: servicesStatus, isLoading, refetch } = useQuery({ 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 Online; + return Online; } if (service.status === 'idle') { - return In Attesa; + return In Attesa; } if (service.status === 'offline') { - return Offline; + return Offline; } if (service.status === 'error') { - return Errore; + return Errore; } - return Sconosciuto; + return Sconosciuto; }; const getStatusIndicator = (service: ServiceStatus) => { if (service.healthy) { - return
; + return
; } if (service.status === 'idle') { - return
; + return
; } - return
; + return
; + }; + + const getServiceIcon = (key: string) => { + const icons: Record = { + nodeBackend: Server, + mlBackend: Brain, + database: Database, + syslogParser: FileText, + analyticsAggregator: Activity, + autoBlock: Shield, + cleanup: Trash2, + listFetcher: ListChecks, + mlTraining: GraduationCap, + }; + const Icon = icons[key] || Activity; + return ; + }; + + 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 = { + 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 ( +
+ {label}: + {variant ? ( + {String(value)} + ) : ( + {String(value)} + )} +
+ ); + }; + + 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 ( + + + + {getServiceIcon(key)} + {service.name} + +
+ {isTimer && } + {getStatusIndicator(service)} +
+
+ +
+ Stato: + {getStatusBadge(service, key)} +
+ +
+ Systemd: + + {service.systemdUnit}{isTimer ? '.timer' : '.service'} + +
+ + {renderServiceDetails(key, service)} + + {isControllable && ( +
+

Controlli:

+
+ + + +
+
+ )} + + {logCmd && ( +
+

Log:

+ {logCmd} +
+ )} +
+
+ ); }; return (
-
+

Gestione Servizi

- Monitoraggio e controllo dei servizi IDS + Monitoraggio e controllo di tutti i servizi IDS

- - - Gestione Servizi Systemd - - 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. - - + {isLoading && ( +
Caricamento stato servizi...
+ )} - {/* Services Grid */} -
- {/* ML Backend Service */} - - - - - ML Backend Python - {servicesStatus && getStatusIndicator(servicesStatus.services.mlBackend)} - - - -
- Stato: - {servicesStatus && getStatusBadge(servicesStatus.services.mlBackend)} + {servicesStatus && ( + <> +
+

+ + Servizi Core +

+
+ {coreServices.map((key) => { + const service = (servicesStatus.services as any)[key]; + return service ? renderServiceCard(key, service) : null; + })}
+
- {servicesStatus?.services.mlBackend.details?.modelLoaded !== undefined && ( -
- Modello ML: - - {servicesStatus.services.mlBackend.details.modelLoaded ? "Caricato" : "Non Caricato"} - -
- )} - - {/* Service Controls */} -
-

Controlli Servizio:

-
- - - -
+
+

+ + Timer Systemd (Attivita Periodiche) +

+
+ {timerServices.map((key) => { + const service = (servicesStatus.services as any)[key]; + return service ? renderServiceCard(key, service) : null; + })}
+
+ + )} - {/* Manual Commands (fallback) */} -
-

Comando systemctl (sul server):

- - sudo systemctl {servicesStatus?.services.mlBackend.status === 'offline' ? 'start' : 'restart'} ids-ml-backend - -
- -
-

Log:

- - tail -f /var/log/ids/backend.log - -
- - - - {/* Database Service */} - - - - - PostgreSQL Database - {servicesStatus && getStatusIndicator(servicesStatus.services.database)} - - - -
- Stato: - {servicesStatus && getStatusBadge(servicesStatus.services.database)} -
- - {servicesStatus?.services.database.status === 'running' && ( -
- Connessione: - Connesso -
- )} - -
-

Verifica status:

- - systemctl status postgresql-16 - -
- - {servicesStatus?.services.database.status === 'error' && ( -
-

Riavvia database:

- - sudo systemctl restart postgresql-16 - -
- )} - -
-

Log:

- - sudo journalctl -u postgresql-16 -f - -
-
-
- - {/* Syslog Parser Service */} - - - - - Syslog Parser - {servicesStatus && getStatusIndicator(servicesStatus.services.syslogParser)} - - - -
- Stato: - {servicesStatus && getStatusBadge(servicesStatus.services.syslogParser)} -
- - {servicesStatus?.services.syslogParser.details?.pid && ( -
- PID Processo: - - {servicesStatus.services.syslogParser.details.pid} - -
- )} - - {servicesStatus?.services.syslogParser.details?.systemd_unit && ( -
- Systemd Unit: - - {servicesStatus.services.syslogParser.details.systemd_unit} - -
- )} - - {/* Service Controls */} -
-

Controlli Servizio:

-
- - - -
-
- - {/* Manual Commands (fallback) */} -
-

Comando systemctl (sul server):

- - sudo systemctl {servicesStatus?.services.syslogParser.status === 'offline' ? 'start' : 'restart'} ids-syslog-parser - -
- -
-

Log:

- - tail -f /var/log/ids/syslog_parser.log - -
-
-
- - {/* Analytics Aggregator Service */} - - - - - Analytics Aggregator - {servicesStatus && getStatusIndicator(servicesStatus.services.analyticsAggregator)} - - - -
- Stato: - {servicesStatus && getStatusBadge(servicesStatus.services.analyticsAggregator)} -
- - {servicesStatus?.services.analyticsAggregator.details?.lastRun && ( -
- Ultima Aggregazione: - - {new Date(servicesStatus.services.analyticsAggregator.details.lastRun).toLocaleString('it-IT')} - -
- )} - - {servicesStatus?.services.analyticsAggregator.details?.hoursSinceLastRun && ( -
- Ore dall'ultimo run: - - {servicesStatus.services.analyticsAggregator.details.hoursSinceLastRun}h - -
- )} - - {/* CRITICAL ALERT: Aggregator idle for too long */} - {servicesStatus?.services.analyticsAggregator.details?.hoursSinceLastRun && - parseFloat(servicesStatus.services.analyticsAggregator.details.hoursSinceLastRun) > 2 && ( - - - ⚠️ Timer Systemd Non Attivo - -

L'aggregatore non esegue da {servicesStatus.services.analyticsAggregator.details.hoursSinceLastRun}h! Dashboard e Analytics bloccati.

-

Soluzione Immediata (sul server):

- - sudo /opt/ids/deployment/setup_analytics_timer.sh - -
-
- )} - -
-

Verifica timer:

- - systemctl status ids-analytics-aggregator.timer - -
- -
-

Avvia aggregazione manualmente:

- - cd /opt/ids && ./deployment/run_analytics.sh - -
- -
-

Log:

- - journalctl -u ids-analytics-aggregator.timer -f - -
-
-
-
- - {/* Additional Commands */} @@ -406,30 +358,27 @@ export default function ServicesPage() {
-

Verifica tutti i processi IDS attivi:

- - ps aux | grep -E "python.*(main|syslog_parser)" | grep -v grep +

Stato di tutti i servizi IDS:

+ + systemctl list-units 'ids-*' --all + +
+
+

Stato di tutti i timer IDS:

+ + systemctl list-timers 'ids-*' --all
-

Verifica log RSyslog (ricezione log MikroTik):

- + tail -f /var/log/mikrotik/raw.log
-
-

Esegui training manuale ML:

- - curl -X POST http://localhost:8000/train -H "Content-Type: application/json" -d '{"max_records": 10000, "hours_back": 24}' - -
- -
-

Verifica storico training nel database:

- - psql $DATABASE_URL -c "SELECT * FROM training_history ORDER BY trained_at DESC LIMIT 5;" +

Verifica processi IDS attivi:

+ + ps aux | grep -E "python.*(main|syslog_parser)" | grep -v grep
diff --git a/server/routes.ts b/server/routes.ts index 97c02f7..b87d923 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -780,39 +780,43 @@ export async function registerRoutes(app: Express): Promise { // 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 { 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 { }); // 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) => {