Initial commit

This commit is contained in:
2025-08-16 19:43:44 +02:00
commit ea9a2627b4
64 changed files with 9232 additions and 0 deletions

View File

@@ -0,0 +1,325 @@
import React, { useState, useEffect } from 'react';
import { useSocket } from '../contexts/SocketContext';
import api from '../services/api';
import {
ServerIcon,
ExclamationTriangleIcon,
BellIcon,
SignalIcon,
EyeIcon
} from '@heroicons/react/24/outline';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
BarChart,
Bar,
PieChart,
Pie,
Cell
} from 'recharts';
import { format } from 'date-fns';
const Dashboard = () => {
const [overview, setOverview] = useState(null);
const [chartData, setChartData] = useState([]);
const [deviceActivity, setDeviceActivity] = useState([]);
const [recentActivity, setRecentActivity] = useState([]);
const [loading, setLoading] = useState(true);
const { recentDetections, deviceStatus, connected } = useSocket();
useEffect(() => {
fetchDashboardData();
const interval = setInterval(fetchDashboardData, 30000); // Refresh every 30 seconds
return () => clearInterval(interval);
}, []);
const fetchDashboardData = async () => {
try {
const [overviewRes, chartRes, deviceRes, activityRes] = await Promise.all([
api.get('/dashboard/overview?hours=24'),
api.get('/dashboard/charts/detections?hours=24&interval=hour'),
api.get('/dashboard/charts/devices?hours=24'),
api.get('/dashboard/activity?hours=24&limit=10')
]);
setOverview(overviewRes.data.data);
setChartData(chartRes.data.data);
setDeviceActivity(deviceRes.data.data);
setRecentActivity(activityRes.data.data);
} catch (error) {
console.error('Error fetching dashboard data:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary-600"></div>
</div>
);
}
const stats = [
{
id: 1,
name: 'Total Devices',
stat: overview?.summary?.total_devices || 0,
icon: ServerIcon,
change: null,
changeType: 'neutral',
color: 'bg-blue-500'
},
{
id: 2,
name: 'Online Devices',
stat: overview?.summary?.online_devices || 0,
icon: SignalIcon,
change: null,
changeType: 'positive',
color: 'bg-green-500'
},
{
id: 3,
name: 'Recent Detections',
stat: overview?.summary?.recent_detections || 0,
icon: ExclamationTriangleIcon,
change: null,
changeType: 'negative',
color: 'bg-red-500'
},
{
id: 4,
name: 'Unique Drones',
stat: overview?.summary?.unique_drones_detected || 0,
icon: EyeIcon,
change: null,
changeType: 'neutral',
color: 'bg-purple-500'
}
];
const deviceStatusData = [
{ name: 'Online', value: overview?.device_status?.online || 0, color: '#22c55e' },
{ name: 'Offline', value: overview?.device_status?.offline || 0, color: '#ef4444' },
{ name: 'Inactive', value: overview?.device_status?.inactive || 0, color: '#6b7280' }
];
return (
<div className="space-y-6">
{/* Stats */}
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">
System Overview
</h3>
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((item) => (
<div
key={item.id}
className="relative bg-white pt-5 px-4 pb-12 sm:pt-6 sm:px-6 shadow rounded-lg overflow-hidden"
>
<dt>
<div className={`absolute ${item.color} rounded-md p-3`}>
<item.icon className="h-6 w-6 text-white" aria-hidden="true" />
</div>
<p className="ml-16 text-sm font-medium text-gray-500 truncate">
{item.name}
</p>
</dt>
<dd className="ml-16 pb-6 flex items-baseline sm:pb-7">
<p className="text-2xl font-semibold text-gray-900">
{item.stat}
</p>
</dd>
</div>
))}
</dl>
</div>
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Detection Timeline */}
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-medium text-gray-900 mb-4">
Detections Timeline (24h)
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="timestamp"
tickFormatter={(value) => format(new Date(value), 'HH:mm')}
/>
<YAxis />
<Tooltip
labelFormatter={(value) => format(new Date(value), 'MMM dd, HH:mm')}
/>
<Area
type="monotone"
dataKey="count"
stroke="#ef4444"
fill="#ef4444"
fillOpacity={0.3}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
{/* Device Status */}
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-medium text-gray-900 mb-4">
Device Status
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={deviceStatusData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={5}
dataKey="value"
>
{deviceStatusData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</div>
<div className="mt-4 flex justify-center space-x-4">
{deviceStatusData.map((item, index) => (
<div key={index} className="flex items-center">
<div
className="w-3 h-3 rounded-full mr-2"
style={{ backgroundColor: item.color }}
/>
<span className="text-sm text-gray-600">
{item.name}: {item.value}
</span>
</div>
))}
</div>
</div>
</div>
{/* Device Activity */}
{deviceActivity.length > 0 && (
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-medium text-gray-900 mb-4">
Device Activity (24h)
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={deviceActivity}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="device_name"
angle={-45}
textAnchor="end"
height={60}
/>
<YAxis />
<Tooltip />
<Bar dataKey="detection_count" fill="#3b82f6" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
)}
{/* Recent Activity & Real-time Detections */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Recent Activity */}
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">Recent Activity</h3>
</div>
<div className="divide-y divide-gray-200 max-h-96 overflow-y-auto">
{recentActivity.map((activity, index) => (
<div key={index} className="px-6 py-4">
<div className="flex items-center space-x-3">
<div className={`flex-shrink-0 w-2 h-2 rounded-full ${
activity.type === 'detection' ? 'bg-red-400' : 'bg-green-400'
}`} />
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-900">
{activity.type === 'detection' ? (
<>Drone {activity.data.drone_id} detected by {activity.data.device_name}</>
) : (
<>Heartbeat from {activity.data.device_name}</>
)}
</p>
<p className="text-xs text-gray-500">
{format(new Date(activity.timestamp), 'MMM dd, HH:mm:ss')}
</p>
</div>
</div>
</div>
))}
{recentActivity.length === 0 && (
<div className="px-6 py-8 text-center text-gray-500">
No recent activity
</div>
)}
</div>
</div>
{/* Real-time Detections */}
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 className="text-lg font-medium text-gray-900">Live Detections</h3>
<div className={`flex items-center space-x-2 px-2 py-1 rounded-full text-xs ${
connected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
<div className={`w-2 h-2 rounded-full ${
connected ? 'bg-green-400' : 'bg-red-400'
}`} />
<span>{connected ? 'Live' : 'Disconnected'}</span>
</div>
</div>
<div className="divide-y divide-gray-200 max-h-96 overflow-y-auto">
{recentDetections.map((detection, index) => (
<div key={index} className="px-6 py-4 animate-fade-in">
<div className="flex items-center space-x-3">
<div className="flex-shrink-0 w-2 h-2 rounded-full bg-red-400 animate-pulse" />
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-900">
Drone {detection.drone_id} detected
</p>
<p className="text-xs text-gray-500">
{detection.device.name || `Device ${detection.device_id}`}
RSSI: {detection.rssi}dBm
Freq: {detection.freq}MHz
</p>
<p className="text-xs text-gray-500">
{format(new Date(detection.server_timestamp), 'HH:mm:ss')}
</p>
</div>
</div>
</div>
))}
{recentDetections.length === 0 && (
<div className="px-6 py-8 text-center text-gray-500">
No recent detections
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default Dashboard;