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
297 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|