dev experience
This commit is contained in:
365
apps/website/components/dev/DebugModeToggle.tsx
Normal file
365
apps/website/components/dev/DebugModeToggle.tsx
Normal file
@@ -0,0 +1,365 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Bug, X, Settings, Shield, Activity } from 'lucide-react';
|
||||
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
||||
import { getGlobalApiLogger } from '@/lib/infrastructure/ApiRequestLogger';
|
||||
|
||||
interface DebugModeToggleProps {
|
||||
/**
|
||||
* Whether to show the toggle (auto-detected from environment)
|
||||
*/
|
||||
show?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug Mode Toggle Component
|
||||
* Provides a floating interface to control debug features and view real-time metrics
|
||||
*/
|
||||
export function DebugModeToggle({ show }: DebugModeToggleProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [debugEnabled, setDebugEnabled] = useState(false);
|
||||
const [metrics, setMetrics] = useState({
|
||||
errors: 0,
|
||||
apiRequests: 0,
|
||||
apiFailures: 0,
|
||||
});
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const shouldShow = show ?? isDev;
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldShow) return;
|
||||
|
||||
// Load debug state from localStorage
|
||||
const saved = localStorage.getItem('gridpilot_debug_enabled');
|
||||
if (saved === 'true') {
|
||||
setDebugEnabled(true);
|
||||
initializeDebugFeatures();
|
||||
}
|
||||
|
||||
// Update metrics every 2 seconds
|
||||
const interval = setInterval(updateMetrics, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, [shouldShow]);
|
||||
|
||||
useEffect(() => {
|
||||
// Save debug state
|
||||
if (shouldShow) {
|
||||
localStorage.setItem('gridpilot_debug_enabled', debugEnabled.toString());
|
||||
}
|
||||
}, [debugEnabled, shouldShow]);
|
||||
|
||||
const updateMetrics = () => {
|
||||
if (!debugEnabled) return;
|
||||
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
const apiLogger = getGlobalApiLogger();
|
||||
|
||||
const errorStats = globalHandler.getStats();
|
||||
const apiStats = apiLogger.getStats();
|
||||
|
||||
setMetrics({
|
||||
errors: errorStats.total,
|
||||
apiRequests: apiStats.total,
|
||||
apiFailures: apiStats.failed,
|
||||
});
|
||||
};
|
||||
|
||||
const initializeDebugFeatures = () => {
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
const apiLogger = getGlobalApiLogger();
|
||||
|
||||
// Initialize global error handler
|
||||
globalHandler.initialize();
|
||||
|
||||
// Override fetch with logging
|
||||
if (!(window as any).__GRIDPILOT_FETCH_LOGGED__) {
|
||||
const loggedFetch = apiLogger.createLoggedFetch();
|
||||
window.fetch = loggedFetch as any;
|
||||
(window as any).__GRIDPILOT_FETCH_LOGGED__ = true;
|
||||
}
|
||||
|
||||
// Expose to window for easy access
|
||||
(window as any).__GRIDPILOT_GLOBAL_HANDLER__ = globalHandler;
|
||||
(window as any).__GRIDPILOT_API_LOGGER__ = apiLogger;
|
||||
|
||||
console.log('%c[DEBUG MODE] Enabled', 'color: #00ff88; font-weight: bold; font-size: 14px;');
|
||||
console.log('Available globals:', {
|
||||
__GRIDPILOT_GLOBAL_HANDLER__: globalHandler,
|
||||
__GRIDPILOT_API_LOGGER__: apiLogger,
|
||||
__GRIDPILOT_REACT_ERRORS__: (window as any).__GRIDPILOT_REACT_ERRORS__ || [],
|
||||
});
|
||||
};
|
||||
|
||||
const toggleDebug = () => {
|
||||
const newEnabled = !debugEnabled;
|
||||
setDebugEnabled(newEnabled);
|
||||
|
||||
if (newEnabled) {
|
||||
initializeDebugFeatures();
|
||||
} else {
|
||||
// Disable debug features
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
globalHandler.destroy();
|
||||
|
||||
console.log('%c[DEBUG MODE] Disabled', 'color: #ff4444; font-weight: bold; font-size: 14px;');
|
||||
}
|
||||
};
|
||||
|
||||
const triggerTestError = () => {
|
||||
if (!debugEnabled) return;
|
||||
|
||||
// Trigger a test API error
|
||||
const testError = new Error('This is a test error for debugging');
|
||||
(testError as any).type = 'TEST_ERROR';
|
||||
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
globalHandler.report(testError, { test: true, timestamp: Date.now() });
|
||||
|
||||
console.log('%c[TEST] Error triggered', 'color: #ffaa00; font-weight: bold;', testError);
|
||||
};
|
||||
|
||||
const triggerTestApiCall = async () => {
|
||||
if (!debugEnabled) return;
|
||||
|
||||
try {
|
||||
// This will fail and be logged
|
||||
await fetch('https://httpstat.us/500');
|
||||
} catch (error) {
|
||||
// Already logged by interceptor
|
||||
console.log('%c[TEST] API call completed', 'color: #00aaff; font-weight: bold;');
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllLogs = () => {
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
const apiLogger = getGlobalApiLogger();
|
||||
|
||||
globalHandler.clearHistory();
|
||||
apiLogger.clearHistory();
|
||||
|
||||
setMetrics({ errors: 0, apiRequests: 0, apiFailures: 0 });
|
||||
|
||||
console.log('%c[DEBUG] All logs cleared', 'color: #00ff88; font-weight: bold;');
|
||||
};
|
||||
|
||||
const copyDebugInfo = async () => {
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
const apiLogger = getGlobalApiLogger();
|
||||
|
||||
const debugInfo = {
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: {
|
||||
mode: process.env.NODE_ENV,
|
||||
appMode: process.env.NEXT_PUBLIC_GRIDPILOT_MODE,
|
||||
version: process.env.NEXT_PUBLIC_APP_VERSION,
|
||||
},
|
||||
browser: {
|
||||
userAgent: navigator.userAgent,
|
||||
language: navigator.language,
|
||||
platform: navigator.platform,
|
||||
},
|
||||
errors: globalHandler.getStats(),
|
||||
api: apiLogger.getStats(),
|
||||
reactErrors: (window as any).__GRIDPILOT_REACT_ERRORS__ || [],
|
||||
};
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2));
|
||||
console.log('%c[DEBUG] Debug info copied to clipboard', 'color: #00ff88; font-weight: bold;');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 left-4 z-50">
|
||||
{/* Main Toggle Button */}
|
||||
{!isOpen && (
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className={`p-3 rounded-full shadow-lg transition-all ${
|
||||
debugEnabled
|
||||
? 'bg-green-600 hover:bg-green-700 text-white'
|
||||
: 'bg-iron-gray hover:bg-charcoal-outline text-gray-300'
|
||||
}`}
|
||||
title={debugEnabled ? 'Debug Mode Active' : 'Enable Debug Mode'}
|
||||
>
|
||||
<Bug className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Debug Panel */}
|
||||
{isOpen && (
|
||||
<div className="w-80 bg-deep-graphite border border-charcoal-outline rounded-xl shadow-2xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-iron-gray/50 border-b border-charcoal-outline">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bug className="w-4 h-4 text-green-400" />
|
||||
<span className="text-sm font-semibold text-white">Debug Control</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="p-1 hover:bg-charcoal-outline rounded"
|
||||
>
|
||||
<X className="w-4 h-4 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-3 space-y-3">
|
||||
{/* Debug Toggle */}
|
||||
<div className="flex items-center justify-between bg-iron-gray/30 p-2 rounded border border-charcoal-outline">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className={`w-4 h-4 ${debugEnabled ? 'text-green-400' : 'text-gray-500'}`} />
|
||||
<span className="text-sm font-medium">Debug Mode</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleDebug}
|
||||
className={`px-3 py-1 rounded text-xs font-bold transition-colors ${
|
||||
debugEnabled
|
||||
? 'bg-green-600 text-white hover:bg-green-700'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
{debugEnabled ? 'ON' : 'OFF'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Metrics */}
|
||||
{debugEnabled && (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-2 text-center">
|
||||
<div className="text-[10px] text-gray-500">Errors</div>
|
||||
<div className="text-lg font-bold text-red-400">{metrics.errors}</div>
|
||||
</div>
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-2 text-center">
|
||||
<div className="text-[10px] text-gray-500">API</div>
|
||||
<div className="text-lg font-bold text-blue-400">{metrics.apiRequests}</div>
|
||||
</div>
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-2 text-center">
|
||||
<div className="text-[10px] text-gray-500">Failures</div>
|
||||
<div className="text-lg font-bold text-yellow-400">{metrics.apiFailures}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
{debugEnabled && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-semibold text-gray-400">Test Actions</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onClick={triggerTestError}
|
||||
className="px-2 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-xs font-medium"
|
||||
>
|
||||
Test Error
|
||||
</button>
|
||||
<button
|
||||
onClick={triggerTestApiCall}
|
||||
className="px-2 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-medium"
|
||||
>
|
||||
Test API
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs font-semibold text-gray-400 mt-2">Utilities</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onClick={copyDebugInfo}
|
||||
className="px-2 py-1.5 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded text-xs font-medium border border-charcoal-outline"
|
||||
>
|
||||
Copy Info
|
||||
</button>
|
||||
<button
|
||||
onClick={clearAllLogs}
|
||||
className="px-2 py-1.5 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded text-xs font-medium border border-charcoal-outline"
|
||||
>
|
||||
Clear Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Links */}
|
||||
{debugEnabled && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-semibold text-gray-400">Quick Access</div>
|
||||
<div className="text-[10px] text-gray-500 font-mono space-y-0.5">
|
||||
<div>• window.__GRIDPILOT_GLOBAL_HANDLER__</div>
|
||||
<div>• window.__GRIDPILOT_API_LOGGER__</div>
|
||||
<div>• window.__GRIDPILOT_REACT_ERRORS__</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status */}
|
||||
<div className="text-[10px] text-gray-500 text-center pt-2 border-t border-charcoal-outline">
|
||||
{debugEnabled ? 'Debug features active' : 'Debug mode disabled'}
|
||||
{isDev && ' • Development Environment'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for programmatic debug control
|
||||
*/
|
||||
export function useDebugMode() {
|
||||
const [debugEnabled, setDebugEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem('gridpilot_debug_enabled');
|
||||
setDebugEnabled(saved === 'true');
|
||||
}, []);
|
||||
|
||||
const enable = () => {
|
||||
setDebugEnabled(true);
|
||||
localStorage.setItem('gridpilot_debug_enabled', 'true');
|
||||
|
||||
// Initialize debug features
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
globalHandler.initialize();
|
||||
|
||||
const apiLogger = getGlobalApiLogger();
|
||||
if (!(window as any).__GRIDPILOT_FETCH_LOGGED__) {
|
||||
const loggedFetch = apiLogger.createLoggedFetch();
|
||||
window.fetch = loggedFetch as any;
|
||||
(window as any).__GRIDPILOT_FETCH_LOGGED__ = true;
|
||||
}
|
||||
|
||||
(window as any).__GRIDPILOT_GLOBAL_HANDLER__ = globalHandler;
|
||||
(window as any).__GRIDPILOT_API_LOGGER__ = apiLogger;
|
||||
};
|
||||
|
||||
const disable = () => {
|
||||
setDebugEnabled(false);
|
||||
localStorage.setItem('gridpilot_debug_enabled', 'false');
|
||||
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
globalHandler.destroy();
|
||||
};
|
||||
|
||||
const toggle = () => {
|
||||
if (debugEnabled) {
|
||||
disable();
|
||||
} else {
|
||||
enable();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
enabled: debugEnabled,
|
||||
enable,
|
||||
disable,
|
||||
toggle,
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import { useNotifications } from '@/components/notifications/NotificationProvider';
|
||||
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
|
||||
import { Wrench, ChevronDown, ChevronUp, X, MessageSquare, Activity, LogIn } from 'lucide-react';
|
||||
import { Wrench, ChevronDown, ChevronUp, X, MessageSquare, Activity, LogIn, Play } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
|
||||
@@ -16,6 +16,7 @@ import { UrgencySection } from './sections/UrgencySection';
|
||||
import { NotificationSendSection } from './sections/NotificationSendSection';
|
||||
import { APIStatusSection } from './sections/APIStatusSection';
|
||||
import { LoginSection } from './sections/LoginSection';
|
||||
import { ReplaySection } from './sections/ReplaySection';
|
||||
|
||||
// Import types
|
||||
import type { DemoNotificationType, DemoUrgency, LoginMode } from './types';
|
||||
@@ -410,6 +411,16 @@ export default function DevToolbar() {
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
</Accordion>
|
||||
|
||||
{/* Replay Section - Accordion */}
|
||||
<Accordion
|
||||
title="Error Replay"
|
||||
icon={<Play className="w-4 h-4 text-gray-400" />}
|
||||
isOpen={openAccordion === 'replay'}
|
||||
onToggle={() => setOpenAccordion(openAccordion === 'replay' ? null : 'replay')}
|
||||
>
|
||||
<ReplaySection />
|
||||
</Accordion>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
160
apps/website/components/dev/sections/ReplaySection.tsx
Normal file
160
apps/website/components/dev/sections/ReplaySection.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Play, Copy, Trash2, Download, Clock } from 'lucide-react';
|
||||
import { getGlobalReplaySystem } from '@/lib/infrastructure/ErrorReplay';
|
||||
|
||||
interface ReplayEntry {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
error: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export function ReplaySection() {
|
||||
const [replays, setReplays] = useState<ReplayEntry[]>([]);
|
||||
const [selectedReplay, setSelectedReplay] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadReplays();
|
||||
}, []);
|
||||
|
||||
const loadReplays = () => {
|
||||
const system = getGlobalReplaySystem();
|
||||
const index = system.getReplayIndex();
|
||||
setReplays(index);
|
||||
};
|
||||
|
||||
const handleReplay = async (replayId: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const system = getGlobalReplaySystem();
|
||||
await system.replay(replayId);
|
||||
} catch (error) {
|
||||
console.error('Replay failed:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = (replayId: string) => {
|
||||
const system = getGlobalReplaySystem();
|
||||
const data = system.exportReplay(replayId, 'json');
|
||||
|
||||
const blob = new Blob([data], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `replay_${replayId}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleCopy = async (replayId: string) => {
|
||||
const system = getGlobalReplaySystem();
|
||||
const data = system.exportReplay(replayId, 'json');
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(data);
|
||||
console.log('Replay data copied to clipboard');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (replayId: string) => {
|
||||
if (confirm('Delete this replay?')) {
|
||||
const system = getGlobalReplaySystem();
|
||||
system.deleteReplay(replayId);
|
||||
loadReplays();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearAll = () => {
|
||||
if (confirm('Clear all replays?')) {
|
||||
const system = getGlobalReplaySystem();
|
||||
system.clearAll();
|
||||
loadReplays();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-gray-400">Error Replay</span>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={loadReplays}
|
||||
className="p-1 hover:bg-charcoal-outline rounded"
|
||||
title="Refresh"
|
||||
>
|
||||
<Clock className="w-3 h-3 text-gray-400" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClearAll}
|
||||
className="p-1 hover:bg-charcoal-outline rounded"
|
||||
title="Clear All"
|
||||
>
|
||||
<Trash2 className="w-3 h-3 text-red-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{replays.length === 0 ? (
|
||||
<div className="text-xs text-gray-500 text-center py-2">
|
||||
No replays available
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1 max-h-48 overflow-auto">
|
||||
{replays.map((replay) => (
|
||||
<div
|
||||
key={replay.id}
|
||||
className="bg-deep-graphite border border-charcoal-outline rounded p-2 text-xs"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-mono text-red-400 font-bold truncate">
|
||||
{replay.type}
|
||||
</div>
|
||||
<div className="text-gray-300 truncate">{replay.error}</div>
|
||||
<div className="text-gray-500 text-[10px]">
|
||||
{new Date(replay.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1 mt-1">
|
||||
<button
|
||||
onClick={() => handleReplay(replay.id)}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-green-600 hover:bg-green-700 text-white rounded"
|
||||
>
|
||||
<Play className="w-3 h-3" />
|
||||
Replay
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleExport(replay.id)}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded border border-charcoal-outline"
|
||||
>
|
||||
<Download className="w-3 h-3" />
|
||||
Export
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleCopy(replay.id)}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded border border-charcoal-outline"
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(replay.id)}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded border border-charcoal-outline"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user