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) => {