diff --git a/frontend/src/components/debug/DebugToolbar.tsx b/frontend/src/components/debug/DebugToolbar.tsx new file mode 100644 index 0000000..74ff01e --- /dev/null +++ b/frontend/src/components/debug/DebugToolbar.tsx @@ -0,0 +1,178 @@ +import React, { useEffect, useState } from 'react'; +import { useStore } from '@nanostores/react'; +import { debugIsOpen, activeTab, logs, type LogEntry } from '../../stores/debugStore'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { getDirectusClient } from '@/lib/directus/client'; + +// Create a client for the devtools if one doesn't exist in context +// (Ideally this component is inside the main QueryClientProvider, but we'll see) +const queryClient = new QueryClient(); + +export default function DebugToolbar() { + const isOpen = useStore(debugIsOpen); + const currentTab = useStore(activeTab); + const logEntries = useStore(logs); + const [backendStatus, setBackendStatus] = useState<'checking' | 'online' | 'error'>('checking'); + const [latency, setLatency] = useState(null); + + useEffect(() => { + if (isOpen && currentTab === 'backend') { + checkBackend(); + } + }, [isOpen, currentTab]); + + const checkBackend = async () => { + setBackendStatus('checking'); + const start = performance.now(); + try { + const client = getDirectusClient(); + await client.request(() => ({ + path: '/server/ping', + method: 'GET' + })); + setLatency(Math.round(performance.now() - start)); + setBackendStatus('online'); + } catch (e) { + setBackendStatus('error'); + } + }; + + if (!isOpen) { + return ( + + ); + } + + return ( +
+ {/* Header */} +
+
+ ⚡ Spark Debug +
+ {(['console', 'backend', 'network'] as const).map(tab => ( + + ))} +
+
+ +
+ + {/* Content */} +
+ + {/* Console Tab */} + {currentTab === 'console' && ( +
+ {logEntries.length === 0 && ( +
No logs captured yet...
+ )} + {logEntries.map((log) => ( +
+ [{log.timestamp}] + + {log.type} + + + {log.messages.join(' ')} + +
+ ))} +
+ +
+
+ )} + + {/* Backend Tab */} + {currentTab === 'backend' && ( +
+
+ {backendStatus === 'online' ? '● Online' : + backendStatus === 'error' ? '✖ Error' : '● Checking...'} +
+ +
+

+ Directus URL: {import.meta.env.PUBLIC_DIRECTUS_URL} +

+ {latency && ( +

+ Latency: {latency}ms +

+ )} +
+ + +
+ )} + + {/* Network / React Query Tab */} + {currentTab === 'network' && ( +
+
+ {/* + React Query Devtools needs a QueryClientProvider context. + In Astro, components are islands. If this island doesn't share context with the main app + (which it likely won't if they are separate roots), we might see empty devtools. + However, putting it here is the best attempt. + */} +
+

React Query Devtools

+

+ (If empty, data fetching might be happening Server-Side or in a different Context) +

+
+
+ {/* We force mount devtools panel here if possible */} + + + +
+ )} + +
+
+ ); +} diff --git a/frontend/src/layouts/BaseLayout.astro b/frontend/src/layouts/BaseLayout.astro index a002e0b..89e2f85 100644 --- a/frontend/src/layouts/BaseLayout.astro +++ b/frontend/src/layouts/BaseLayout.astro @@ -1,6 +1,7 @@ --- import type { Globals, Navigation } from '@/types/schema'; import { GlobalToaster } from '@/components/providers/CoreProviders'; +import DebugToolbar from '@/components/debug/DebugToolbar'; interface Props { title: string; @@ -276,5 +277,6 @@ const ogImage = image || globals?.logo || ''; }); + diff --git a/frontend/src/stores/debugStore.ts b/frontend/src/stores/debugStore.ts new file mode 100644 index 0000000..ecc1c81 --- /dev/null +++ b/frontend/src/stores/debugStore.ts @@ -0,0 +1,62 @@ +import { atom, map } from 'nanostores'; + +export type LogType = 'log' | 'warn' | 'error' | 'info'; + +export interface LogEntry { + id: string; + timestamp: string; + type: LogType; + messages: any[]; +} + +export const debugIsOpen = atom(false); +export const activeTab = atom<'console' | 'network' | 'backend'>('console'); +export const logs = atom([]); + +// Initialize log capturer +if (typeof window !== 'undefined') { + const originalLog = console.log; + const originalWarn = console.warn; + const originalError = console.error; + const originalInfo = console.info; + + const addLog = (type: LogType, args: any[]) => { + const entry: LogEntry = { + id: Math.random().toString(36).substr(2, 9), + timestamp: new Date().toISOString().split('T')[1].slice(0, 8), // HH:MM:SS + type, + messages: args.map(arg => { + try { + return typeof arg === 'object' ? JSON.stringify(arg) : String(arg); + } catch (e) { + return '[Circular/Unserializable]'; + } + }) + }; + + const currentLogs = logs.get(); + // Keep last 100 logs + const newLogs = [...currentLogs, entry].slice(-100); + logs.set(newLogs); + }; + + console.log = (...args) => { + originalLog(...args); + addLog('log', args); + }; + + console.warn = (...args) => { + originalWarn(...args); + addLog('warn', args); + }; + + console.error = (...args) => { + originalError(...args); + addLog('error', args); + }; + + console.info = (...args) => { + originalInfo(...args); + addLog('info', args); + }; +}