Files
drone-detector/client/src/pages/Debug.jsx
2025-09-10 07:00:29 +02:00

517 lines
22 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import api from '../services/api';
import { format } from 'date-fns';
import {
BugAntIcon,
ExclamationTriangleIcon,
InformationCircleIcon,
EyeIcon,
TrashIcon,
DocumentTextIcon,
XMarkIcon
} from '@heroicons/react/24/outline';
const Debug = () => {
const [debugData, setDebugData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [pagination, setPagination] = useState({});
const [debugInfo, setDebugInfo] = useState({});
const [showPayloadModal, setShowPayloadModal] = useState(false);
const [selectedPayload, setSelectedPayload] = useState(null);
const [payloadLoading, setPayloadLoading] = useState(false);
const [filters, setFilters] = useState({
drone_type: '',
device_id: '',
page: 1,
limit: 50
});
useEffect(() => {
fetchDebugData();
}, [filters]);
const fetchDebugData = async () => {
try {
setLoading(true);
const params = new URLSearchParams();
Object.entries(filters).forEach(([key, value]) => {
if (value) params.append(key, value);
});
const response = await api.get(`/detections/debug?${params}`);
if (response.data.success) {
setDebugData(response.data.data);
setPagination(response.data.pagination);
setDebugInfo(response.data.debug_info);
} else {
setError(response.data.message || 'Failed to fetch debug data');
}
} catch (err) {
console.error('Error fetching debug data:', err);
setError(err.response?.data?.message || 'Failed to fetch debug data');
} finally {
setLoading(false);
}
};
const handleFilterChange = (key, value) => {
setFilters(prev => ({
...prev,
[key]: value,
page: 1 // Reset to first page when filtering
}));
};
const handlePageChange = (newPage) => {
setFilters(prev => ({ ...prev, page: newPage }));
};
const fetchRawPayload = async (detectionId) => {
try {
setPayloadLoading(true);
const response = await api.get(`/debug/detection-payloads?limit=1&detection_id=${detectionId}`);
if (response.data.success && response.data.data.length > 0) {
const detection = response.data.data[0];
setSelectedPayload({
id: detection.id,
timestamp: detection.server_timestamp,
deviceId: detection.device_id,
rawPayload: detection.raw_payload,
processedData: {
drone_id: detection.drone_id,
drone_type: detection.drone_type,
rssi: detection.rssi,
freq: detection.freq,
geo_lat: detection.geo_lat,
geo_lon: detection.geo_lon,
device_timestamp: detection.device_timestamp,
confidence_level: detection.confidence_level,
signal_duration: detection.signal_duration
}
});
setShowPayloadModal(true);
} else {
console.error('No payload data found for detection:', detectionId);
alert('No raw payload data found for this detection');
}
} catch (err) {
console.error('Error fetching payload:', err);
alert('Failed to fetch payload data');
} finally {
setPayloadLoading(false);
}
};
const closePayloadModal = () => {
setShowPayloadModal(false);
setSelectedPayload(null);
};
const getDroneTypeColor = (droneType) => {
if (droneType === 0) return 'bg-gray-100 text-gray-800';
return 'bg-blue-100 text-blue-800';
};
const getThreatLevelColor = (threatLevel) => {
switch (threatLevel?.toLowerCase()) {
case 'critical':
return 'bg-red-100 text-red-800';
case 'high':
return 'bg-orange-100 text-orange-800';
case 'medium':
return 'bg-yellow-100 text-yellow-800';
case 'low':
return 'bg-green-100 text-green-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-96">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary-500"></div>
</div>
);
}
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<div className="flex">
<ExclamationTriangleIcon className="h-5 w-5 text-red-400" />
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Error</h3>
<div className="mt-2 text-sm text-red-700">{error}</div>
</div>
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="bg-white shadow rounded-lg p-6">
<div className="flex items-center">
<BugAntIcon className="h-8 w-8 text-orange-500 mr-3" />
<div>
<h1 className="text-2xl font-bold text-gray-900">Debug Console</h1>
<p className="text-sm text-gray-500">
Admin-only access to all detection data including drone type 0 (None)
</p>
</div>
</div>
</div>
{/* Debug Info */}
{debugInfo && (
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-4">
<div className="flex">
<InformationCircleIcon className="h-5 w-5 text-yellow-400" />
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">Debug Information</h3>
<div className="mt-2 text-sm text-yellow-700">
<p>{debugInfo.message}</p>
<p className="mt-1">Total None detections: <strong>{debugInfo.total_none_detections}</strong></p>
</div>
</div>
</div>
</div>
)}
{/* Filters */}
<div className="bg-white shadow rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Filters</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Drone Type
</label>
<select
value={filters.drone_type}
onChange={(e) => handleFilterChange('drone_type', e.target.value)}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="">All Types</option>
<option value="0">0 - None (Debug)</option>
<option value="1">1 - Unknown</option>
<option value="2">2 - Orlan</option>
<option value="3">3 - Zala</option>
<option value="4">4 - Eleron</option>
<option value="5">5 - Zala Lancet</option>
<option value="6">6 - Lancet</option>
<option value="7">7 - FPV CrossFire</option>
<option value="8">8 - FPV ELRS</option>
<option value="9">9 - Maybe Orlan</option>
<option value="10">10 - Maybe Zala</option>
<option value="11">11 - Maybe Lancet</option>
<option value="12">12 - Maybe Eleron</option>
<option value="13">13 - DJI</option>
<option value="14">14 - Supercam</option>
<option value="15">15 - Maybe Supercam</option>
<option value="16">16 - REB</option>
<option value="17">17 - Crypto Orlan</option>
<option value="18">18 - DJI Enterprise</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Device ID
</label>
<input
type="number"
value={filters.device_id}
onChange={(e) => handleFilterChange('device_id', e.target.value)}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Filter by device ID"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Results per page
</label>
<select
value={filters.limit}
onChange={(e) => handleFilterChange('limit', e.target.value)}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
</div>
{/* Debug Data Table */}
<div className="bg-white shadow rounded-lg overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">
Debug Detections ({pagination.total || 0})
</h3>
</div>
{debugData.length === 0 ? (
<div className="text-center py-12">
<BugAntIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No debug data</h3>
<p className="mt-1 text-sm text-gray-500">
No detections found matching the current filters.
</p>
</div>
) : (
<>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
ID / Time
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Device
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Drone Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
RSSI / Freq
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Threat Level
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Debug
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{debugData.map((detection) => (
<tr key={detection.id} className={detection.is_debug_data ? 'bg-gray-50' : ''}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">#{detection.id}</div>
<div className="text-sm text-gray-500">
{format(new Date(detection.server_timestamp), 'MMM dd, HH:mm:ss')}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{detection.device?.name || `Device ${detection.device_id}`}
</div>
<div className="text-sm text-gray-500">ID: {detection.device_id}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getDroneTypeColor(detection.drone_type)}`}>
{detection.drone_type_info?.name || `Type ${detection.drone_type}`}
</span>
<div className="text-xs text-gray-500 mt-1">
ID: {detection.drone_type}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{detection.rssi} dBm</div>
<div className="text-sm text-gray-500">{detection.freq} MHz</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{detection.threat_level ? (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getThreatLevelColor(detection.threat_level)}`}>
{detection.threat_level}
</span>
) : (
<span className="text-gray-400 text-sm">N/A</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{detection.is_debug_data && (
<span className="px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
Debug Data
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<button
onClick={() => fetchRawPayload(detection.id)}
disabled={payloadLoading}
className="inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50"
>
<DocumentTextIcon className="h-4 w-4 mr-1" />
{payloadLoading ? 'Loading...' : 'View Payload'}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
{pagination.pages > 1 && (
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => handlePageChange(Math.max(1, pagination.page - 1))}
disabled={pagination.page === 1}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => handlePageChange(Math.min(pagination.pages, pagination.page + 1))}
disabled={pagination.page === pagination.pages}
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing <span className="font-medium">{((pagination.page - 1) * pagination.limit) + 1}</span> to{' '}
<span className="font-medium">
{Math.min(pagination.page * pagination.limit, pagination.total)}
</span>{' '}
of <span className="font-medium">{pagination.total}</span> results
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
<button
onClick={() => handlePageChange(Math.max(1, pagination.page - 1))}
disabled={pagination.page === 1}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => handlePageChange(Math.min(pagination.pages, pagination.page + 1))}
disabled={pagination.page === pagination.pages}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</nav>
</div>
</div>
</div>
)}
</>
)}
</div>
{/* Raw Payload Modal */}
{showPayloadModal && selectedPayload && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
<div className="mt-3">
{/* Modal Header */}
<div className="flex items-center justify-between pb-4 border-b">
<div className="flex items-center">
<DocumentTextIcon className="h-6 w-6 text-blue-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">
Raw Payload Data
</h3>
</div>
<button
onClick={closePayloadModal}
className="text-gray-400 hover:text-gray-600"
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>
{/* Detection Info */}
<div className="mt-4 bg-gray-50 rounded-lg p-4">
<h4 className="font-medium text-gray-900 mb-2">Detection Information</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-600">Detection ID:</span>
<span className="ml-2 font-mono">{selectedPayload.id}</span>
</div>
<div>
<span className="text-gray-600">Device ID:</span>
<span className="ml-2 font-mono">{selectedPayload.deviceId}</span>
</div>
<div>
<span className="text-gray-600">Server Timestamp:</span>
<span className="ml-2 font-mono">
{format(new Date(selectedPayload.timestamp), 'yyyy-MM-dd HH:mm:ss')}
</span>
</div>
<div>
<span className="text-gray-600">Drone Type:</span>
<span className="ml-2 font-mono">{selectedPayload.processedData.drone_type}</span>
</div>
</div>
</div>
{/* Processed Data */}
<div className="mt-4">
<h4 className="font-medium text-gray-900 mb-2">Processed Data</h4>
<div className="bg-gray-100 rounded-lg p-4 font-mono text-sm overflow-x-auto">
<pre className="whitespace-pre-wrap">
{JSON.stringify(selectedPayload.processedData, null, 2)}
</pre>
</div>
</div>
{/* Raw Payload */}
<div className="mt-4">
<h4 className="font-medium text-gray-900 mb-2">Raw Payload from Detector</h4>
{selectedPayload.rawPayload ? (
<div className="bg-black text-green-400 rounded-lg p-4 font-mono text-sm overflow-x-auto max-h-96 overflow-y-auto">
<pre className="whitespace-pre-wrap">
{JSON.stringify(selectedPayload.rawPayload, null, 2)}
</pre>
</div>
) : (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<p className="text-yellow-800 text-sm">
No raw payload data available. This might occur if:
</p>
<ul className="mt-2 text-yellow-700 text-sm list-disc list-inside">
<li>STORE_RAW_PAYLOAD environment variable is disabled</li>
<li>This detection was created before raw payload storage was enabled</li>
<li>The detection was processed without storing the original payload</li>
</ul>
</div>
)}
</div>
{/* Modal Footer */}
<div className="mt-6 flex justify-end space-x-3">
<button
onClick={closePayloadModal}
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-500"
>
Close
</button>
<button
onClick={() => {
navigator.clipboard.writeText(JSON.stringify(selectedPayload, null, 2));
alert('Payload data copied to clipboard!');
}}
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Copy to Clipboard
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default Debug;