Implement form validation and improve error handling for ML features

Refactor Training and Whitelist pages to use react-hook-form and Zod for validation, and enhance ML backend API routes with timeouts, input validation, and better error reporting.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 95d9d0e3-3da7-43ff-b8d3-d9d5d8fd6f6f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/Aqah4U9
This commit is contained in:
marco370 2025-11-21 09:08:38 +00:00
parent 7171c3d607
commit 7b514f470f
4 changed files with 353 additions and 211 deletions

View File

@ -15,8 +15,8 @@ localPort = 5000
externalPort = 80 externalPort = 80
[[ports]] [[ports]]
localPort = 43079 localPort = 41303
externalPort = 3001 externalPort = 3002
[[ports]] [[ports]]
localPort = 43803 localPort = 43803

View File

@ -8,8 +8,9 @@ import { format } from "date-fns";
import type { TrainingHistory } from "@shared/schema"; import type { TrainingHistory } from "@shared/schema";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useState } from "react"; import { useState } from "react";
import { Label } from "@/components/ui/label"; import { useForm } from "react-hook-form";
import { Input } from "@/components/ui/input"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -19,6 +20,17 @@ import {
DialogTrigger, DialogTrigger,
DialogFooter, DialogFooter,
} from "@/components/ui/dialog"; } 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 { interface MLStatsResponse {
logs?: { total: number; last_hour: number }; logs?: { total: number; last_hour: number };
@ -27,16 +39,40 @@ interface MLStatsResponse {
latest_training?: any; 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() { export default function TrainingPage() {
const { toast } = useToast(); const { toast } = useToast();
const [isTrainDialogOpen, setIsTrainDialogOpen] = useState(false); const [isTrainDialogOpen, setIsTrainDialogOpen] = useState(false);
const [isDetectDialogOpen, setIsDetectDialogOpen] = useState(false); const [isDetectDialogOpen, setIsDetectDialogOpen] = useState(false);
const [trainRecords, setTrainRecords] = useState("100000");
const [trainHours, setTrainHours] = useState("24"); const trainForm = useForm<z.infer<typeof trainFormSchema>>({
const [detectRecords, setDetectRecords] = useState("50000"); resolver: zodResolver(trainFormSchema),
const [detectHours, setDetectHours] = useState("1"); defaultValues: {
const [detectThreshold, setDetectThreshold] = useState("75"); max_records: 100000,
const [detectAutoBlock, setDetectAutoBlock] = useState(true); 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[]>({ const { data: history, isLoading } = useQuery<TrainingHistory[]>({
queryKey: ["/api/training-history"], queryKey: ["/api/training-history"],
@ -49,7 +85,7 @@ export default function TrainingPage() {
}); });
const trainMutation = useMutation({ const trainMutation = useMutation({
mutationFn: async (params: { max_records: number; hours_back: number }) => { mutationFn: async (params: z.infer<typeof trainFormSchema>) => {
return await apiRequest("POST", "/api/ml/train", params); return await apiRequest("POST", "/api/ml/train", params);
}, },
onSuccess: () => { onSuccess: () => {
@ -60,18 +96,19 @@ export default function TrainingPage() {
description: "Il modello ML è in addestramento. Controlla lo storico tra qualche minuto.", description: "Il modello ML è in addestramento. Controlla lo storico tra qualche minuto.",
}); });
setIsTrainDialogOpen(false); setIsTrainDialogOpen(false);
trainForm.reset();
}, },
onError: () => { onError: (error: any) => {
toast({ toast({
title: "Errore", title: "Errore",
description: "Impossibile avviare il training", description: error.message || "Impossibile avviare il training",
variant: "destructive", variant: "destructive",
}); });
}, },
}); });
const detectMutation = useMutation({ const detectMutation = useMutation({
mutationFn: async (params: { max_records: number; hours_back: number; risk_threshold: number; auto_block: boolean }) => { mutationFn: async (params: z.infer<typeof detectFormSchema>) => {
return await apiRequest("POST", "/api/ml/detect", params); return await apiRequest("POST", "/api/ml/detect", params);
}, },
onSuccess: () => { onSuccess: () => {
@ -82,52 +119,23 @@ export default function TrainingPage() {
description: "Analisi anomalie in corso. Controlla i rilevamenti tra qualche secondo.", description: "Analisi anomalie in corso. Controlla i rilevamenti tra qualche secondo.",
}); });
setIsDetectDialogOpen(false); setIsDetectDialogOpen(false);
detectForm.reset();
}, },
onError: () => { onError: (error: any) => {
toast({ toast({
title: "Errore", title: "Errore",
description: "Impossibile avviare la detection", description: error.message || "Impossibile avviare la detection",
variant: "destructive", variant: "destructive",
}); });
}, },
}); });
const handleTrain = () => { const onTrainSubmit = (data: z.infer<typeof trainFormSchema>) => {
const records = parseInt(trainRecords); trainMutation.mutate(data);
const hours = parseInt(trainHours);
if (isNaN(records) || records <= 0) {
toast({
title: "Errore",
description: "Inserisci un numero valido di record",
variant: "destructive",
});
return;
}
trainMutation.mutate({ max_records: records, hours_back: hours });
}; };
const handleDetect = () => { const onDetectSubmit = (data: z.infer<typeof detectFormSchema>) => {
const records = parseInt(detectRecords); detectMutation.mutate(data);
const hours = parseInt(detectHours);
const threshold = parseInt(detectThreshold);
if (isNaN(records) || records <= 0 || isNaN(threshold) || threshold < 0 || threshold > 100) {
toast({
title: "Errore",
description: "Inserisci valori validi",
variant: "destructive",
});
return;
}
detectMutation.mutate({
max_records: records,
hours_back: hours,
risk_threshold: threshold,
auto_block: detectAutoBlock,
});
}; };
return ( return (
@ -213,38 +221,51 @@ export default function TrainingPage() {
Configura i parametri per l'addestramento del modello Configura i parametri per l'addestramento del modello
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <Form {...trainForm}>
<div className="space-y-2"> <form onSubmit={trainForm.handleSubmit(onTrainSubmit)} className="space-y-4 py-4">
<Label htmlFor="train-records">Numero Record</Label> <FormField
<Input control={trainForm.control}
id="train-records" name="max_records"
type="number" render={({ field }) => (
value={trainRecords} <FormItem>
onChange={(e) => setTrainRecords(e.target.value)} <FormLabel>Numero Record</FormLabel>
data-testid="input-train-records" <FormControl>
<Input type="number" {...field} data-testid="input-train-records" />
</FormControl>
<FormDescription>Consigliato: 100000</FormDescription>
<FormMessage />
</FormItem>
)}
/> />
<p className="text-xs text-muted-foreground">Consigliato: 100000</p> <FormField
</div> control={trainForm.control}
<div className="space-y-2"> name="hours_back"
<Label htmlFor="train-hours">Ore Precedenti</Label> render={({ field }) => (
<Input <FormItem>
id="train-hours" <FormLabel>Ore Precedenti</FormLabel>
type="number" <FormControl>
value={trainHours} <Input type="number" {...field} data-testid="input-train-hours" />
onChange={(e) => setTrainHours(e.target.value)} </FormControl>
data-testid="input-train-hours" <FormDescription>Consigliato: 24</FormDescription>
<FormMessage />
</FormItem>
)}
/> />
<p className="text-xs text-muted-foreground">Consigliato: 24</p>
</div>
</div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setIsTrainDialogOpen(false)} data-testid="button-cancel-training"> <Button
type="button"
variant="outline"
onClick={() => setIsTrainDialogOpen(false)}
data-testid="button-cancel-training"
>
Annulla Annulla
</Button> </Button>
<Button onClick={handleTrain} disabled={trainMutation.isPending} data-testid="button-confirm-training"> <Button type="submit" disabled={trainMutation.isPending} data-testid="button-confirm-training">
{trainMutation.isPending ? "Avvio..." : "Avvia Training"} {trainMutation.isPending ? "Avvio..." : "Avvia Training"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form>
</Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</CardContent> </CardContent>
@ -275,62 +296,83 @@ export default function TrainingPage() {
Configura i parametri per il rilevamento anomalie Configura i parametri per il rilevamento anomalie
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <Form {...detectForm}>
<div className="space-y-2"> <form onSubmit={detectForm.handleSubmit(onDetectSubmit)} className="space-y-4 py-4">
<Label htmlFor="detect-records">Numero Record</Label> <FormField
<Input control={detectForm.control}
id="detect-records" name="max_records"
type="number" render={({ field }) => (
value={detectRecords} <FormItem>
onChange={(e) => setDetectRecords(e.target.value)} <FormLabel>Numero Record</FormLabel>
data-testid="input-detect-records" <FormControl>
<Input type="number" {...field} data-testid="input-detect-records" />
</FormControl>
<FormDescription>Consigliato: 50000</FormDescription>
<FormMessage />
</FormItem>
)}
/> />
<p className="text-xs text-muted-foreground">Consigliato: 50000</p> <FormField
</div> control={detectForm.control}
<div className="space-y-2"> name="hours_back"
<Label htmlFor="detect-hours">Ore Precedenti</Label> render={({ field }) => (
<Input <FormItem>
id="detect-hours" <FormLabel>Ore Precedenti</FormLabel>
type="number" <FormControl>
value={detectHours} <Input type="number" {...field} data-testid="input-detect-hours" />
onChange={(e) => setDetectHours(e.target.value)} </FormControl>
data-testid="input-detect-hours" <FormDescription>Consigliato: 1</FormDescription>
<FormMessage />
</FormItem>
)}
/> />
<p className="text-xs text-muted-foreground">Consigliato: 1</p> <FormField
</div> control={detectForm.control}
<div className="space-y-2"> name="risk_threshold"
<Label htmlFor="detect-threshold">Soglia Rischio (%)</Label> render={({ field }) => (
<Input <FormItem>
id="detect-threshold" <FormLabel>Soglia Rischio (%)</FormLabel>
type="number" <FormControl>
min="0" <Input type="number" min="0" max="100" {...field} data-testid="input-detect-threshold" />
max="100" </FormControl>
value={detectThreshold} <FormDescription>Consigliato: 75</FormDescription>
onChange={(e) => setDetectThreshold(e.target.value)} <FormMessage />
data-testid="input-detect-threshold" </FormItem>
)}
/> />
<p className="text-xs text-muted-foreground">Consigliato: 75</p> <FormField
</div> control={detectForm.control}
<div className="flex items-center space-x-2"> name="auto_block"
<input render={({ field }) => (
type="checkbox" <FormItem className="flex flex-row items-start space-x-3 space-y-0">
id="auto-block" <FormControl>
checked={detectAutoBlock} <Checkbox
onChange={(e) => setDetectAutoBlock(e.target.checked)} checked={field.value}
className="rounded border-gray-300" onCheckedChange={field.onChange}
data-testid="checkbox-auto-block" data-testid="checkbox-auto-block"
/> />
<Label htmlFor="auto-block">Blocco automatico IP critici (80)</Label> </FormControl>
</div> <div className="space-y-1 leading-none">
<FormLabel>Blocco automatico IP critici (80)</FormLabel>
</div> </div>
</FormItem>
)}
/>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setIsDetectDialogOpen(false)} data-testid="button-cancel-detection"> <Button
type="button"
variant="outline"
onClick={() => setIsDetectDialogOpen(false)}
data-testid="button-cancel-detection"
>
Annulla Annulla
</Button> </Button>
<Button onClick={handleDetect} disabled={detectMutation.isPending} data-testid="button-confirm-detection"> <Button type="submit" disabled={detectMutation.isPending} data-testid="button-confirm-detection">
{detectMutation.isPending ? "Avvio..." : "Avvia Detection"} {detectMutation.isPending ? "Avvio..." : "Avvia Detection"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form>
</Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</CardContent> </CardContent>

View File

@ -2,13 +2,14 @@ import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient, apiRequest } from "@/lib/queryClient"; import { queryClient, apiRequest } from "@/lib/queryClient";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Shield, Plus, Trash2, CheckCircle2, XCircle } from "lucide-react"; import { Shield, Plus, Trash2, CheckCircle2, XCircle } from "lucide-react";
import { format } from "date-fns"; import { format } from "date-fns";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import type { Whitelist } from "@shared/schema"; import type { Whitelist } from "@shared/schema";
import { insertWhitelistSchema } from "@shared/schema";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { import {
Dialog, Dialog,
@ -19,20 +20,47 @@ import {
DialogTrigger, DialogTrigger,
DialogFooter, DialogFooter,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
const whitelistFormSchema = insertWhitelistSchema.extend({
ipAddress: z.string()
.min(7, "Inserisci un IP valido")
.regex(/^(\d{1,3}\.){3}\d{1,3}$/, "Formato IP non valido")
.refine((ip) => {
const parts = ip.split('.').map(Number);
return parts.every(part => part >= 0 && part <= 255);
}, "Ogni ottetto deve essere tra 0 e 255"),
});
export default function WhitelistPage() { export default function WhitelistPage() {
const { toast } = useToast(); const { toast } = useToast();
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
const [newIp, setNewIp] = useState("");
const [newComment, setNewComment] = useState(""); const form = useForm<z.infer<typeof whitelistFormSchema>>({
const [newReason, setNewReason] = useState(""); resolver: zodResolver(whitelistFormSchema),
defaultValues: {
ipAddress: "",
comment: "",
reason: "",
active: true,
},
});
const { data: whitelist, isLoading } = useQuery<Whitelist[]>({ const { data: whitelist, isLoading } = useQuery<Whitelist[]>({
queryKey: ["/api/whitelist"], queryKey: ["/api/whitelist"],
}); });
const addMutation = useMutation({ const addMutation = useMutation({
mutationFn: async (data: { ipAddress: string; comment?: string; reason?: string }) => { mutationFn: async (data: z.infer<typeof whitelistFormSchema>) => {
return await apiRequest("POST", "/api/whitelist", data); return await apiRequest("POST", "/api/whitelist", data);
}, },
onSuccess: () => { onSuccess: () => {
@ -42,14 +70,12 @@ export default function WhitelistPage() {
description: "L'indirizzo IP è stato aggiunto alla whitelist", description: "L'indirizzo IP è stato aggiunto alla whitelist",
}); });
setIsAddDialogOpen(false); setIsAddDialogOpen(false);
setNewIp(""); form.reset();
setNewComment("");
setNewReason("");
}, },
onError: () => { onError: (error: any) => {
toast({ toast({
title: "Errore", title: "Errore",
description: "Impossibile aggiungere l'IP alla whitelist", description: error.message || "Impossibile aggiungere l'IP alla whitelist",
variant: "destructive", variant: "destructive",
}); });
}, },
@ -66,30 +92,17 @@ export default function WhitelistPage() {
description: "L'indirizzo IP è stato rimosso dalla whitelist", description: "L'indirizzo IP è stato rimosso dalla whitelist",
}); });
}, },
onError: () => { onError: (error: any) => {
toast({ toast({
title: "Errore", title: "Errore",
description: "Impossibile rimuovere l'IP dalla whitelist", description: error.message || "Impossibile rimuovere l'IP dalla whitelist",
variant: "destructive", variant: "destructive",
}); });
}, },
}); });
const handleAdd = () => { const onSubmit = (data: z.infer<typeof whitelistFormSchema>) => {
if (!newIp.trim()) { addMutation.mutate(data);
toast({
title: "Errore",
description: "Inserisci un indirizzo IP valido",
variant: "destructive",
});
return;
}
addMutation.mutate({
ipAddress: newIp.trim(),
comment: newComment.trim() || undefined,
reason: newReason.trim() || undefined,
});
}; };
return ( return (
@ -116,46 +129,62 @@ export default function WhitelistPage() {
Inserisci l'indirizzo IP che vuoi proteggere dal blocco automatico Inserisci l'indirizzo IP che vuoi proteggere dal blocco automatico
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <Form {...form}>
<div className="space-y-2"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 py-4">
<Label htmlFor="ip">Indirizzo IP *</Label> <FormField
<Input control={form.control}
id="ip" name="ipAddress"
placeholder="192.168.1.100" render={({ field }) => (
value={newIp} <FormItem>
onChange={(e) => setNewIp(e.target.value)} <FormLabel>Indirizzo IP *</FormLabel>
data-testid="input-ip" <FormControl>
<Input placeholder="192.168.1.100" {...field} data-testid="input-ip" />
</FormControl>
<FormMessage />
</FormItem>
)}
/> />
</div> <FormField
<div className="space-y-2"> control={form.control}
<Label htmlFor="reason">Motivo</Label> name="reason"
<Input render={({ field }) => (
id="reason" <FormItem>
placeholder="Es: Server backup" <FormLabel>Motivo</FormLabel>
value={newReason} <FormControl>
onChange={(e) => setNewReason(e.target.value)} <Input placeholder="Es: Server backup" {...field} data-testid="input-reason" />
data-testid="input-reason" </FormControl>
<FormMessage />
</FormItem>
)}
/> />
</div> <FormField
<div className="space-y-2"> control={form.control}
<Label htmlFor="comment">Note</Label> name="comment"
<Textarea render={({ field }) => (
id="comment" <FormItem>
placeholder="Note aggiuntive..." <FormLabel>Note</FormLabel>
value={newComment} <FormControl>
onChange={(e) => setNewComment(e.target.value)} <Textarea placeholder="Note aggiuntive..." {...field} data-testid="input-comment" />
data-testid="input-comment" </FormControl>
<FormMessage />
</FormItem>
)}
/> />
</div>
</div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setIsAddDialogOpen(false)} data-testid="button-cancel"> <Button
type="button"
variant="outline"
onClick={() => setIsAddDialogOpen(false)}
data-testid="button-cancel"
>
Annulla Annulla
</Button> </Button>
<Button onClick={handleAdd} disabled={addMutation.isPending} data-testid="button-confirm-add"> <Button type="submit" disabled={addMutation.isPending} data-testid="button-confirm-add">
{addMutation.isPending ? "Aggiunta..." : "Aggiungi"} {addMutation.isPending ? "Aggiunta..." : "Aggiungi"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form>
</Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </div>

View File

@ -167,24 +167,51 @@ export async function registerRoutes(app: Express): Promise<Server> {
}); });
// ML Actions - Trigger training/detection on Python backend // ML Actions - Trigger training/detection on Python backend
const ML_BACKEND_URL = process.env.ML_BACKEND_URL || "http://localhost:8000";
const ML_TIMEOUT = 120000; // 2 minutes timeout
app.post("/api/ml/train", async (req, res) => { app.post("/api/ml/train", async (req, res) => {
try { try {
const { max_records = 100000, hours_back = 24 } = req.body; const { max_records = 100000, hours_back = 24 } = req.body;
const response = await fetch("http://localhost:8000/train", { // Validate input
if (typeof max_records !== 'number' || max_records <= 0 || max_records > 1000000) {
return res.status(400).json({ error: "max_records must be between 1 and 1000000" });
}
if (typeof hours_back !== 'number' || hours_back <= 0 || hours_back > 720) {
return res.status(400).json({ error: "hours_back must be between 1 and 720" });
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), ML_TIMEOUT);
const response = await fetch(`${ML_BACKEND_URL}/train`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ max_records, hours_back }), body: JSON.stringify({ max_records, hours_back }),
signal: controller.signal,
}); });
clearTimeout(timeout);
if (!response.ok) { if (!response.ok) {
throw new Error("Python backend training failed"); const errorData = await response.json().catch(() => ({}));
return res.status(response.status).json({
error: errorData.detail || "Training failed",
status: response.status,
});
} }
const data = await response.json(); const data = await response.json();
res.json(data); res.json(data);
} catch (error) { } catch (error: any) {
res.status(500).json({ error: "Failed to trigger training" }); if (error.name === 'AbortError') {
return res.status(504).json({ error: "Training timeout - operation took too long" });
}
if (error.code === 'ECONNREFUSED') {
return res.status(503).json({ error: "ML backend not available - is Python server running?" });
}
res.status(500).json({ error: error.message || "Failed to trigger training" });
} }
}); });
@ -192,35 +219,79 @@ export async function registerRoutes(app: Express): Promise<Server> {
try { try {
const { max_records = 50000, hours_back = 1, risk_threshold = 75, auto_block = false } = req.body; const { max_records = 50000, hours_back = 1, risk_threshold = 75, auto_block = false } = req.body;
const response = await fetch("http://localhost:8000/detect", { // Validate input
if (typeof max_records !== 'number' || max_records <= 0 || max_records > 1000000) {
return res.status(400).json({ error: "max_records must be between 1 and 1000000" });
}
if (typeof hours_back !== 'number' || hours_back <= 0 || hours_back > 720) {
return res.status(400).json({ error: "hours_back must be between 1 and 720" });
}
if (typeof risk_threshold !== 'number' || risk_threshold < 0 || risk_threshold > 100) {
return res.status(400).json({ error: "risk_threshold must be between 0 and 100" });
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), ML_TIMEOUT);
const response = await fetch(`${ML_BACKEND_URL}/detect`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ max_records, hours_back, risk_threshold, auto_block }), body: JSON.stringify({ max_records, hours_back, risk_threshold, auto_block }),
signal: controller.signal,
}); });
clearTimeout(timeout);
if (!response.ok) { if (!response.ok) {
throw new Error("Python backend detection failed"); const errorData = await response.json().catch(() => ({}));
return res.status(response.status).json({
error: errorData.detail || "Detection failed",
status: response.status,
});
} }
const data = await response.json(); const data = await response.json();
res.json(data); res.json(data);
} catch (error) { } catch (error: any) {
res.status(500).json({ error: "Failed to trigger detection" }); if (error.name === 'AbortError') {
return res.status(504).json({ error: "Detection timeout - operation took too long" });
}
if (error.code === 'ECONNREFUSED') {
return res.status(503).json({ error: "ML backend not available - is Python server running?" });
}
res.status(500).json({ error: error.message || "Failed to trigger detection" });
} }
}); });
app.get("/api/ml/stats", async (req, res) => { app.get("/api/ml/stats", async (req, res) => {
try { try {
const response = await fetch("http://localhost:8000/stats"); const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout for stats
const response = await fetch(`${ML_BACKEND_URL}/stats`, {
signal: controller.signal,
});
clearTimeout(timeout);
if (!response.ok) { if (!response.ok) {
throw new Error("Python backend stats failed"); const errorData = await response.json().catch(() => ({}));
return res.status(response.status).json({
error: errorData.detail || "Failed to fetch ML stats",
status: response.status,
});
} }
const data = await response.json(); const data = await response.json();
res.json(data); res.json(data);
} catch (error) { } catch (error: any) {
res.status(500).json({ error: "Failed to fetch ML stats" }); if (error.name === 'AbortError') {
return res.status(504).json({ error: "Stats timeout" });
}
if (error.code === 'ECONNREFUSED') {
return res.status(503).json({ error: "ML backend not available" });
}
res.status(500).json({ error: error.message || "Failed to fetch ML stats" });
} }
}); });