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) */}
+
);