ids.alfacom.it/client/src/pages/Training.tsx
marco370 9761ee6036 Add visual indicators for the Hybrid ML model version
Update the UI to display badges indicating the use of Hybrid ML v2.0.0 on both the Training and Anomaly Detection cards, and refine descriptive text for clarity.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 7abf54ed-5574-4967-a851-0590e80d6ad1
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/jFtLBWL
2025-11-25 17:24:29 +00:00

474 lines
19 KiB
TypeScript

import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient, apiRequest } from "@/lib/queryClient";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Brain, Play, Search, CheckCircle2, XCircle, Clock, TrendingUp } from "lucide-react";
import { format } from "date-fns";
import type { TrainingHistory } from "@shared/schema";
import { useToast } from "@/hooks/use-toast";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
interface MLStatsResponse {
logs?: { total: number; last_hour: number };
detections?: { total: number; blocked: number };
routers?: { active: number };
latest_training?: any;
}
const trainFormSchema = z.object({
max_records: z.coerce.number().min(1, "Minimo 1 record").max(1000000, "Massimo 1M record"),
hours_back: z.coerce.number().min(1, "Minimo 1 ora").max(720, "Massimo 720 ore (30 giorni)"),
});
const detectFormSchema = z.object({
max_records: z.coerce.number().min(1, "Minimo 1 record").max(1000000, "Massimo 1M record"),
hours_back: z.coerce.number().min(1, "Minimo 1 ora").max(720, "Massimo 720 ore"),
risk_threshold: z.coerce.number().min(0, "Minimo 0").max(100, "Massimo 100"),
auto_block: z.boolean().default(true),
});
export default function TrainingPage() {
const { toast } = useToast();
const [isTrainDialogOpen, setIsTrainDialogOpen] = useState(false);
const [isDetectDialogOpen, setIsDetectDialogOpen] = useState(false);
const trainForm = useForm<z.infer<typeof trainFormSchema>>({
resolver: zodResolver(trainFormSchema),
defaultValues: {
max_records: 100000,
hours_back: 24,
},
});
const detectForm = useForm<z.infer<typeof detectFormSchema>>({
resolver: zodResolver(detectFormSchema),
defaultValues: {
max_records: 50000,
hours_back: 1,
risk_threshold: 75,
auto_block: true,
},
});
const { data: history, isLoading } = useQuery<TrainingHistory[]>({
queryKey: ["/api/training-history"],
refetchInterval: 10000,
});
const { data: mlStats } = useQuery<MLStatsResponse>({
queryKey: ["/api/ml/stats"],
refetchInterval: 10000,
});
const trainMutation = useMutation({
mutationFn: async (params: z.infer<typeof trainFormSchema>) => {
return await apiRequest("POST", "/api/ml/train", params);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/training-history"] });
queryClient.invalidateQueries({ queryKey: ["/api/ml/stats"] });
toast({
title: "Training avviato",
description: "Il modello ML è in addestramento. Controlla lo storico tra qualche minuto.",
});
setIsTrainDialogOpen(false);
trainForm.reset();
},
onError: (error: any) => {
toast({
title: "Errore",
description: error.message || "Impossibile avviare il training",
variant: "destructive",
});
},
});
const detectMutation = useMutation({
mutationFn: async (params: z.infer<typeof detectFormSchema>) => {
return await apiRequest("POST", "/api/ml/detect", params);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/detections"] });
queryClient.invalidateQueries({ queryKey: ["/api/stats"] });
toast({
title: "Detection avviata",
description: "Analisi anomalie in corso. Controlla i rilevamenti tra qualche secondo.",
});
setIsDetectDialogOpen(false);
detectForm.reset();
},
onError: (error: any) => {
toast({
title: "Errore",
description: error.message || "Impossibile avviare la detection",
variant: "destructive",
});
},
});
const onTrainSubmit = (data: z.infer<typeof trainFormSchema>) => {
trainMutation.mutate(data);
};
const onDetectSubmit = (data: z.infer<typeof detectFormSchema>) => {
detectMutation.mutate(data);
};
return (
<div className="flex flex-col gap-6 p-6" data-testid="page-training">
<div>
<h1 className="text-3xl font-semibold" data-testid="text-page-title">Machine Learning</h1>
<p className="text-muted-foreground" data-testid="text-page-subtitle">
Training e detection del modello Isolation Forest
</p>
</div>
{/* ML Stats */}
{mlStats && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card data-testid="card-ml-logs">
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Log Totali</CardTitle>
<Brain className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold" data-testid="text-ml-logs-total">
{mlStats.logs?.total?.toLocaleString() || 0}
</div>
<p className="text-xs text-muted-foreground mt-1">
Ultima ora: {mlStats.logs?.last_hour?.toLocaleString() || 0}
</p>
</CardContent>
</Card>
<Card data-testid="card-ml-detections">
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Detection Totali</CardTitle>
<Search className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold" data-testid="text-ml-detections-total">
{mlStats.detections?.total || 0}
</div>
<p className="text-xs text-muted-foreground mt-1">
Bloccati: {mlStats.detections?.blocked || 0}
</p>
</CardContent>
</Card>
<Card data-testid="card-ml-routers">
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Router Attivi</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold" data-testid="text-ml-routers-active">
{mlStats.routers?.active || 0}
</div>
</CardContent>
</Card>
</div>
)}
{/* Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card data-testid="card-train-action">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<Brain className="h-5 w-5" />
Addestramento Modello
</CardTitle>
<Badge variant="secondary" className="bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300" data-testid="badge-model-version">
Hybrid ML v2.0.0
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
Addestra il modello Hybrid ML (Isolation Forest + Ensemble Classifier) analizzando i log recenti per rilevare pattern di traffico normale.
</p>
<Dialog open={isTrainDialogOpen} onOpenChange={setIsTrainDialogOpen}>
<DialogTrigger asChild>
<Button className="w-full" data-testid="button-start-training">
<Play className="h-4 w-4 mr-2" />
Avvia Training
</Button>
</DialogTrigger>
<DialogContent data-testid="dialog-training">
<DialogHeader>
<DialogTitle>Avvia Training ML</DialogTitle>
<DialogDescription>
Configura i parametri per l'addestramento del modello
</DialogDescription>
</DialogHeader>
<Form {...trainForm}>
<form onSubmit={trainForm.handleSubmit(onTrainSubmit)} className="space-y-4 py-4">
<FormField
control={trainForm.control}
name="max_records"
render={({ field }) => (
<FormItem>
<FormLabel>Numero Record</FormLabel>
<FormControl>
<Input type="number" {...field} data-testid="input-train-records" />
</FormControl>
<FormDescription>Consigliato: 100000</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={trainForm.control}
name="hours_back"
render={({ field }) => (
<FormItem>
<FormLabel>Ore Precedenti</FormLabel>
<FormControl>
<Input type="number" {...field} data-testid="input-train-hours" />
</FormControl>
<FormDescription>Consigliato: 24</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setIsTrainDialogOpen(false)}
data-testid="button-cancel-training"
>
Annulla
</Button>
<Button type="submit" disabled={trainMutation.isPending} data-testid="button-confirm-training">
{trainMutation.isPending ? "Avvio..." : "Avvia Training"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
</CardContent>
</Card>
<Card data-testid="card-detect-action">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<Search className="h-5 w-5" />
Rilevamento Anomalie
</CardTitle>
<Badge variant="secondary" className="bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300" data-testid="badge-detection-version">
Hybrid ML v2.0.0
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
Analizza i log recenti per rilevare anomalie e IP sospetti con il modello Hybrid ML. Blocca automaticamente gli IP critici (risk_score 80).
</p>
<Dialog open={isDetectDialogOpen} onOpenChange={setIsDetectDialogOpen}>
<DialogTrigger asChild>
<Button variant="secondary" className="w-full" data-testid="button-start-detection">
<Search className="h-4 w-4 mr-2" />
Avvia Detection
</Button>
</DialogTrigger>
<DialogContent data-testid="dialog-detection">
<DialogHeader>
<DialogTitle>Avvia Detection Anomalie</DialogTitle>
<DialogDescription>
Configura i parametri per il rilevamento anomalie
</DialogDescription>
</DialogHeader>
<Form {...detectForm}>
<form onSubmit={detectForm.handleSubmit(onDetectSubmit)} className="space-y-4 py-4">
<FormField
control={detectForm.control}
name="max_records"
render={({ field }) => (
<FormItem>
<FormLabel>Numero Record</FormLabel>
<FormControl>
<Input type="number" {...field} data-testid="input-detect-records" />
</FormControl>
<FormDescription>Consigliato: 50000</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={detectForm.control}
name="hours_back"
render={({ field }) => (
<FormItem>
<FormLabel>Ore Precedenti</FormLabel>
<FormControl>
<Input type="number" {...field} data-testid="input-detect-hours" />
</FormControl>
<FormDescription>Consigliato: 1</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={detectForm.control}
name="risk_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>Soglia Rischio (%)</FormLabel>
<FormControl>
<Input type="number" min="0" max="100" {...field} data-testid="input-detect-threshold" />
</FormControl>
<FormDescription>Consigliato: 75</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={detectForm.control}
name="auto_block"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
data-testid="checkbox-auto-block"
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Blocco automatico IP critici (80)</FormLabel>
</div>
</FormItem>
)}
/>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setIsDetectDialogOpen(false)}
data-testid="button-cancel-detection"
>
Annulla
</Button>
<Button type="submit" disabled={detectMutation.isPending} data-testid="button-confirm-detection">
{detectMutation.isPending ? "Avvio..." : "Avvia Detection"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
</CardContent>
</Card>
</div>
{/* Training History */}
<Card data-testid="card-training-history">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="h-5 w-5" />
Storico Training ({history?.length || 0})
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8 text-muted-foreground" data-testid="text-loading">
Caricamento...
</div>
) : history && history.length > 0 ? (
<div className="space-y-3">
{history.map((item) => (
<div
key={item.id}
className="p-4 rounded-lg border hover-elevate"
data-testid={`training-item-${item.id}`}
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-1">
<div className="flex items-center gap-2">
<p className="font-medium" data-testid={`text-version-${item.id}`}>
Versione {item.modelVersion}
</p>
{item.status === "success" ? (
<Badge variant="outline" className="bg-green-50" data-testid={`badge-status-${item.id}`}>
<CheckCircle2 className="h-3 w-3 mr-1" />
Successo
</Badge>
) : (
<Badge variant="destructive" data-testid={`badge-status-${item.id}`}>
<XCircle className="h-3 w-3 mr-1" />
Fallito
</Badge>
)}
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm text-muted-foreground">
<div>
<span className="font-medium">Record:</span> {item.recordsProcessed.toLocaleString()}
</div>
<div>
<span className="font-medium">Feature:</span> {item.featuresCount}
</div>
{item.accuracy && (
<div>
<span className="font-medium">Accuracy:</span> {item.accuracy}%
</div>
)}
{item.trainingDuration && (
<div>
<span className="font-medium">Durata:</span> {item.trainingDuration}s
</div>
)}
</div>
<p className="text-xs text-muted-foreground" data-testid={`text-date-${item.id}`}>
{format(new Date(item.trainedAt), "dd/MM/yyyy HH:mm:ss")}
</p>
{item.notes && (
<p className="text-sm text-muted-foreground" data-testid={`text-notes-${item.id}`}>
{item.notes}
</p>
)}
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12 text-muted-foreground" data-testid="text-empty">
<Brain className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p className="font-medium">Nessun training eseguito</p>
<p className="text-sm mt-2">Avvia il primo training per addestrare il modello ML</p>
</div>
)}
</CardContent>
</Card>
</div>
);
}