ids.alfacom.it/client/src/pages/DashboardLive.tsx
marco370 3da26587f9 Display accurate real-time attack statistics on the dashboard
Introduce a new API endpoint to fetch aggregated dashboard statistics, replacing raw data retrieval with pre-calculated metrics for improved performance and accuracy in the live dashboard view.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: d36a1b00-13ef-4857-b703-6096a3bd4601
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/x5P9dcJ
2025-11-24 10:12:02 +00:00

297 lines
11 KiB
TypeScript

import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Activity, Globe, Shield, TrendingUp, AlertTriangle } from "lucide-react";
import { AreaChart, Area, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
import type { Detection, NetworkLog } from "@shared/schema";
import { getFlag } from "@/lib/country-flags";
import { format } from "date-fns";
interface DashboardStats {
totalPackets: number;
attackPackets: number;
normalPackets: number;
uniqueIps: number;
attackUniqueIps: number;
attacksByCountry: Record<string, number>;
attacksByType: Record<string, number>;
recentDetections: Detection[];
}
export default function DashboardLive() {
// Fetch aggregated stats from analytics (ultimi 72h = 3 giorni)
const { data: stats, isLoading } = useQuery<DashboardStats>({
queryKey: ["/api/dashboard/live?hours=72"],
refetchInterval: 10000, // Aggiorna ogni 10s
});
// Usa dati aggregati precisi
const totalTraffic = stats?.totalPackets || 0;
const totalAttacks = stats?.attackPackets || 0;
const normalTraffic = stats?.normalPackets || 0;
const attackPercentage = totalTraffic > 0 ? ((totalAttacks / totalTraffic) * 100).toFixed(2) : "0";
const detections = stats?.recentDetections || [];
const blockedAttacks = detections.filter(d => d.blocked).length;
// Usa dati aggregati già calcolati dal backend
const attacksByCountry = stats?.attacksByCountry || {};
const attacksByType = stats?.attacksByType || {};
const countryChartData = Object.entries(attacksByCountry)
.map(([name, attacks]) => ({
name: `${getFlag(name, name.substring(0, 2))} ${name}`,
attacks,
normal: 0,
}))
.sort((a, b) => b.attacks - a.attacks)
.slice(0, 10);
const typeChartData = Object.entries(attacksByType).map(([name, value]) => ({
name: name.replace('_', ' ').toUpperCase(),
value,
}));
// Traffico normale vs attacchi (gauge data)
const trafficDistribution = [
{ name: 'Normal', value: normalTraffic, color: '#22c55e' },
{ name: 'Attacks', value: totalAttacks, color: '#ef4444' },
];
// Ultimi eventi (stream)
const recentEvents = [...detections]
.sort((a, b) => new Date(b.detectedAt).getTime() - new Date(a.detectedAt).getTime())
.slice(0, 20);
const COLORS = ['#ef4444', '#f97316', '#f59e0b', '#eab308', '#84cc16'];
return (
<div className="flex flex-col gap-6 p-6" data-testid="page-dashboard-live">
{/* Header */}
<div>
<h1 className="text-3xl font-semibold flex items-center gap-2" data-testid="text-page-title">
<Activity className="h-8 w-8" />
Dashboard Live
</h1>
<p className="text-muted-foreground" data-testid="text-page-subtitle">
Monitoraggio real-time (ultimi 3 giorni)
</p>
</div>
{isLoading && (
<div className="text-center py-8" data-testid="text-loading">
Caricamento dati...
</div>
)}
{!isLoading && (
<>
{/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card data-testid="card-total-traffic">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Traffico Totale
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold" data-testid="text-total-traffic">
{totalTraffic.toLocaleString()}
</div>
<p className="text-xs text-muted-foreground mt-1">pacchetti</p>
</CardContent>
</Card>
<Card data-testid="card-normal-traffic">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Traffico Normale
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-green-600" data-testid="text-normal-traffic">
{normalTraffic.toLocaleString()}
</div>
<p className="text-xs text-muted-foreground mt-1">
{(100 - parseFloat(attackPercentage)).toFixed(1)}% del totale
</p>
</CardContent>
</Card>
<Card data-testid="card-attacks">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Attacchi Rilevati
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-red-600" data-testid="text-attacks">
{totalAttacks}
</div>
<p className="text-xs text-muted-foreground mt-1">
{attackPercentage}% del traffico
</p>
</CardContent>
</Card>
<Card data-testid="card-blocked">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
IP Bloccati
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-orange-600" data-testid="text-blocked">
{blockedAttacks}
</div>
<p className="text-xs text-muted-foreground mt-1">
{totalAttacks > 0 ? ((blockedAttacks / totalAttacks) * 100).toFixed(1) : 0}% degli attacchi
</p>
</CardContent>
</Card>
</div>
{/* Charts Row 1 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Traffic Distribution (Pie) */}
<Card data-testid="card-distribution">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
Distribuzione Traffico
</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={trafficDistribution}
cx="50%"
cy="50%"
labelLine={false}
label={(entry) => `${entry.name}: ${entry.value}`}
outerRadius={100}
fill="#8884d8"
dataKey="value"
>
{trafficDistribution.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Attacks by Type (Pie) */}
<Card data-testid="card-attack-types">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5" />
Tipi di Attacco
</CardTitle>
</CardHeader>
<CardContent>
{typeChartData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={typeChartData}
cx="50%"
cy="50%"
labelLine={false}
label={(entry) => `${entry.name}: ${entry.value}`}
outerRadius={100}
fill="#8884d8"
dataKey="value"
>
{typeChartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
) : (
<div className="text-center py-20 text-muted-foreground">
Nessun attacco rilevato
</div>
)}
</CardContent>
</Card>
</div>
{/* Top Countries (Bar Chart) */}
<Card data-testid="card-countries">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Globe className="h-5 w-5" />
Top 10 Paesi Attaccanti
</CardTitle>
</CardHeader>
<CardContent>
{countryChartData.length > 0 ? (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={countryChartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="attacks" fill="#ef4444" name="Attacchi" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="text-center py-20 text-muted-foreground">
Nessun dato disponibile
</div>
)}
</CardContent>
</Card>
{/* Real-time Event Stream */}
<Card data-testid="card-event-stream">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
Stream Eventi Recenti
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2 max-h-96 overflow-y-auto">
{recentEvents.map(event => (
<div
key={event.id}
className="flex items-center justify-between p-3 rounded-lg border hover-elevate"
data-testid={`event-${event.id}`}
>
<div className="flex items-center gap-3">
{event.countryCode && (
<span className="text-xl">
{getFlag(event.country, event.countryCode)}
</span>
)}
<div>
<code className="font-mono font-semibold">{event.sourceIp}</code>
<p className="text-xs text-muted-foreground">
{event.anomalyType.replace('_', ' ')} {format(new Date(event.detectedAt), "HH:mm:ss")}
</p>
</div>
</div>
<Badge variant={event.blocked ? "destructive" : "secondary"}>
{event.blocked ? "Bloccato" : "Attivo"}
</Badge>
</div>
))}
</div>
</CardContent>
</Card>
</>
)}
</div>
);
}