diff --git a/client/.env.development b/client/.env.development index 033b42e..86edd50 100644 --- a/client/.env.development +++ b/client/.env.development @@ -1,2 +1,5 @@ # Development environment - runs on root path VITE_BASE_PATH= + +# Debug configuration +VITE_DEBUG_ENABLED=true diff --git a/client/.env.production b/client/.env.production index 2cee3fe..b1c5d78 100644 --- a/client/.env.production +++ b/client/.env.production @@ -1,2 +1,5 @@ # Production environment - deployed under /uggla/ path VITE_BASE_PATH=/uggla/ + +# Debug configuration +VITE_DEBUG_ENABLED=true diff --git a/client/src/components/DebugOverlay.jsx b/client/src/components/DebugOverlay.jsx new file mode 100644 index 0000000..c6574c1 --- /dev/null +++ b/client/src/components/DebugOverlay.jsx @@ -0,0 +1,282 @@ +import React, { useState, useEffect } from 'react'; +import api from '../services/api'; +import { format } from 'date-fns'; +import { + XMarkIcon, + BugAntIcon, + EyeIcon, + ChevronDownIcon, + ChevronUpIcon, + ArrowPathIcon, + ExclamationTriangleIcon +} from '@heroicons/react/24/outline'; + +const DebugOverlay = ({ isOpen, onClose }) => { + const [activeTab, setActiveTab] = useState('detections'); + const [detectionPayloads, setDetectionPayloads] = useState([]); + const [heartbeatPayloads, setHeartbeatPayloads] = useState([]); + const [debugConfig, setDebugConfig] = useState({}); + const [loading, setLoading] = useState(false); + const [expandedItems, setExpandedItems] = useState(new Set()); + const [filters, setFilters] = useState({ + limit: 20, + device_id: '' + }); + + useEffect(() => { + if (isOpen) { + fetchDebugConfig(); + fetchPayloads(); + } + }, [isOpen, activeTab, filters]); + + const fetchDebugConfig = async () => { + try { + const response = await api.get('/debug/config'); + if (response.data.success) { + setDebugConfig(response.data.config); + } + } catch (error) { + console.error('Error fetching debug config:', error); + } + }; + + const fetchPayloads = async () => { + setLoading(true); + try { + const endpoint = activeTab === 'detections' ? '/debug/detection-payloads' : '/debug/heartbeat-payloads'; + const params = new URLSearchParams(); + + if (filters.limit) params.append('limit', filters.limit); + if (filters.device_id) params.append('device_id', filters.device_id); + + const response = await api.get(`${endpoint}?${params}`); + + if (response.data.success) { + if (activeTab === 'detections') { + setDetectionPayloads(response.data.data); + } else { + setHeartbeatPayloads(response.data.data); + } + } + } catch (error) { + console.error('Error fetching payloads:', error); + } finally { + setLoading(false); + } + }; + + const toggleExpanded = (id) => { + const newExpanded = new Set(expandedItems); + if (newExpanded.has(id)) { + newExpanded.delete(id); + } else { + newExpanded.add(id); + } + setExpandedItems(newExpanded); + }; + + const formatPayload = (payload) => { + return JSON.stringify(payload, null, 2); + }; + + const currentData = activeTab === 'detections' ? detectionPayloads : heartbeatPayloads; + + if (!isOpen) return null; + + return ( +
+ {/* Backdrop */} +
+ + {/* Overlay Panel */} +
+
+ {/* Header */} +
+
+
+ +

Debug Payload Viewer

+ {debugConfig.STORE_RAW_PAYLOAD && ( + + Active + + )} +
+ +
+ + {/* Tabs */} +
+ + +
+
+ + {/* Controls */} +
+
+
+ + +
+ +
+ + setFilters({...filters, device_id: e.target.value})} + placeholder="Filter by device ID" + className="rounded border-gray-300 text-sm w-40" + /> +
+ + +
+
+ + {/* Content */} +
+ {!debugConfig.STORE_RAW_PAYLOAD && ( +
+
+
+
+ +
+
+

+ Raw Payload Storage Disabled +

+
+

Set STORE_RAW_PAYLOAD=true in environment variables to enable payload storage.

+
+
+
+
+
+ )} + + {loading && ( +
+ +
+ )} + + {!loading && currentData.length === 0 && ( +
+ No {activeTab} payloads found +
+ )} + +
+ {currentData.map((item) => ( +
+
toggleExpanded(item.id)} + > +
+ + Device {item.device_id} + + {activeTab === 'detections' && ( + <> + + Drone {item.drone_id} | Type {item.drone_type} + + + RSSI: {item.rssi}dBm + + + )} + + {format(new Date(item.server_timestamp || item.received_at), 'MMM dd, HH:mm:ss')} + +
+ +
+ {item.raw_payload && ( + + Raw Data + + )} + {expandedItems.has(item.id) ? ( + + ) : ( + + )} +
+
+ + {expandedItems.has(item.id) && ( +
+ {item.raw_payload ? ( +
+

Raw Payload:

+
+                            {formatPayload(item.raw_payload)}
+                          
+
+ ) : ( +
+ No raw payload stored for this item +
+ )} +
+ )} +
+ ))} +
+
+
+
+
+ ); +}; + +export default DebugOverlay; diff --git a/client/src/components/DebugToggle.jsx b/client/src/components/DebugToggle.jsx new file mode 100644 index 0000000..72171f2 --- /dev/null +++ b/client/src/components/DebugToggle.jsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react'; +import { BugAntIcon } from '@heroicons/react/24/outline'; +import DebugOverlay from './DebugOverlay'; + +const DebugToggle = () => { + const [isDebugOpen, setIsDebugOpen] = useState(false); + + // Only show in development or when debug is enabled + const shouldShow = import.meta.env.DEV || + import.meta.env.VITE_DEBUG_ENABLED === 'true'; + + if (!shouldShow) return null; + + return ( + <> + {/* Floating Debug Button */} + + + {/* Debug Overlay */} + setIsDebugOpen(false)} + /> + + ); +}; + +export default DebugToggle; diff --git a/client/src/components/Layout.jsx b/client/src/components/Layout.jsx index c160caf..210de40 100644 --- a/client/src/components/Layout.jsx +++ b/client/src/components/Layout.jsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { Outlet, Link, useLocation } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; import { useSocket } from '../contexts/SocketContext'; +import DebugToggle from './DebugToggle'; import { HomeIcon, MapIcon, @@ -148,6 +149,9 @@ const Layout = () => {
+ + {/* Debug Toggle (floating button) */} + );