381 lines
13 KiB
TypeScript
381 lines
13 KiB
TypeScript
import type { ApiRequestLogger } from '@/lib/infrastructure/ApiRequestLogger';
|
|
import { getGlobalApiLogger } from '@/lib/infrastructure/ApiRequestLogger';
|
|
import type { GlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
|
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
|
import { Bug, Shield, X } from 'lucide-react';
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import { Button } from '@/ui/Button';
|
|
import { Icon } from '@/ui/Icon';
|
|
import { Stack } from '@/ui/Stack';
|
|
import { Text } from '@/ui/Text';
|
|
import { Grid } from '@/ui/Grid';
|
|
|
|
// Extend Window interface for debug globals
|
|
declare global {
|
|
interface Window {
|
|
__GRIDPILOT_FETCH_LOGGED__?: boolean;
|
|
__GRIDPILOT_GLOBAL_HANDLER__?: GlobalErrorHandler;
|
|
__GRIDPILOT_API_LOGGER__?: ApiRequestLogger;
|
|
__GRIDPILOT_REACT_ERRORS__?: Array<{ error: unknown; componentStack?: string }>;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
const updateMetrics = useCallback(() => {
|
|
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,
|
|
});
|
|
}, [debugEnabled]);
|
|
|
|
const initializeDebugFeatures = useCallback(() => {
|
|
const globalHandler = getGlobalErrorHandler();
|
|
const apiLogger = getGlobalApiLogger();
|
|
|
|
// Initialize global error handler
|
|
globalHandler.initialize();
|
|
|
|
// Override fetch with logging
|
|
if (!window.__GRIDPILOT_FETCH_LOGGED__) {
|
|
const loggedFetch = apiLogger.createLoggedFetch();
|
|
window.fetch = loggedFetch as typeof fetch;
|
|
window.__GRIDPILOT_FETCH_LOGGED__ = true;
|
|
}
|
|
|
|
// Expose to window for easy access
|
|
window.__GRIDPILOT_GLOBAL_HANDLER__ = globalHandler;
|
|
window.__GRIDPILOT_API_LOGGER__ = apiLogger;
|
|
|
|
console.log('%c[DEBUG MODE] Enabled', 'color: #00ff88; font-weight: bold; font-size: 14px;');
|
|
}, []);
|
|
|
|
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, initializeDebugFeatures, updateMetrics]);
|
|
|
|
useEffect(() => {
|
|
// Save debug state
|
|
if (shouldShow) {
|
|
localStorage.setItem('gridpilot_debug_enabled', debugEnabled.toString());
|
|
}
|
|
}, [debugEnabled, shouldShow]);
|
|
|
|
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 Error & { type?: string }).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,
|
|
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.__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 (
|
|
<Stack position="fixed" bottom="4" left="4" zIndex={50}>
|
|
{/* Main Toggle Button */}
|
|
{!isOpen && (
|
|
<Button
|
|
onClick={() => setIsOpen(true)}
|
|
p={3}
|
|
rounded="full"
|
|
variant={debugEnabled ? 'primary' : 'secondary'}
|
|
className={`transition-all hover:scale-110 ${debugEnabled ? 'bg-green-600' : 'bg-iron-gray'}`}
|
|
title={debugEnabled ? 'Debug Mode Active' : 'Enable Debug Mode'}
|
|
>
|
|
<Icon icon={Bug} size={5} />
|
|
</Button>
|
|
)}
|
|
|
|
{/* Debug Panel */}
|
|
{isOpen && (
|
|
<Stack w="80" bg="bg-deep-graphite" border={true} borderColor="border-charcoal-outline" rounded="xl" shadow="2xl" overflow="hidden">
|
|
{/* Header */}
|
|
<Stack direction="row" align="center" justify="between" px={3} py={2} bg="bg-iron-gray/50" borderBottom={true} borderColor="border-charcoal-outline">
|
|
<Stack direction="row" align="center" gap={2}>
|
|
<Icon icon={Bug} size={4} color="text-green-400" />
|
|
<Text size="sm" weight="semibold" color="text-white">Debug Control</Text>
|
|
</Stack>
|
|
<Button
|
|
onClick={() => setIsOpen(false)}
|
|
p={1}
|
|
variant="ghost"
|
|
className="hover:bg-charcoal-outline rounded"
|
|
>
|
|
<Icon icon={X} size={4} color="text-gray-400" />
|
|
</Button>
|
|
</Stack>
|
|
|
|
{/* Content */}
|
|
<Stack p={3}>
|
|
<Stack gap={3}>
|
|
{/* Debug Toggle */}
|
|
<Stack direction="row" align="center" justify="between" bg="bg-iron-gray/30" p={2} rounded="md" border={true} borderColor="border-charcoal-outline">
|
|
<Stack direction="row" align="center" gap={2}>
|
|
<Icon icon={Shield} size={4} color={debugEnabled ? 'text-green-400' : 'text-gray-500'} />
|
|
<Text size="sm" weight="medium">Debug Mode</Text>
|
|
</Stack>
|
|
<Button
|
|
onClick={toggleDebug}
|
|
size="sm"
|
|
variant={debugEnabled ? 'primary' : 'secondary'}
|
|
className={debugEnabled ? 'bg-green-600 hover:bg-green-700' : ''}
|
|
>
|
|
{debugEnabled ? 'ON' : 'OFF'}
|
|
</Button>
|
|
</Stack>
|
|
|
|
{/* Metrics */}
|
|
{debugEnabled && (
|
|
<Grid cols={3} gap={2}>
|
|
<Stack bg="bg-iron-gray" border={true} borderColor="border-charcoal-outline" rounded="md" p={2} align="center">
|
|
<Text size="xs" color="text-gray-500" block style={{ fontSize: '10px' }}>Errors</Text>
|
|
<Text size="lg" weight="bold" color="text-red-400" block>{metrics.errors}</Text>
|
|
</Stack>
|
|
<Stack bg="bg-iron-gray" border={true} borderColor="border-charcoal-outline" rounded="md" p={2} align="center">
|
|
<Text size="xs" color="text-gray-500" block style={{ fontSize: '10px' }}>API</Text>
|
|
<Text size="lg" weight="bold" color="text-blue-400" block>{metrics.apiRequests}</Text>
|
|
</Stack>
|
|
<Stack bg="bg-iron-gray" border={true} borderColor="border-charcoal-outline" rounded="md" p={2} align="center">
|
|
<Text size="xs" color="text-gray-500" block style={{ fontSize: '10px' }}>Failures</Text>
|
|
<Text size="lg" weight="bold" color="text-yellow-400" block>{metrics.apiFailures}</Text>
|
|
</Stack>
|
|
</Grid>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
{debugEnabled && (
|
|
<Stack gap={2}>
|
|
<Text size="xs" weight="semibold" color="text-gray-400">Test Actions</Text>
|
|
<Grid cols={2} gap={2}>
|
|
<Button
|
|
onClick={triggerTestError}
|
|
variant="danger"
|
|
size="sm"
|
|
>
|
|
Test Error
|
|
</Button>
|
|
<Button
|
|
onClick={triggerTestApiCall}
|
|
size="sm"
|
|
>
|
|
Test API
|
|
</Button>
|
|
</Grid>
|
|
|
|
<Text size="xs" weight="semibold" color="text-gray-400" mt={2}>Utilities</Text>
|
|
<Grid cols={2} gap={2}>
|
|
<Button
|
|
onClick={copyDebugInfo}
|
|
variant="secondary"
|
|
size="sm"
|
|
>
|
|
Copy Info
|
|
</Button>
|
|
<Button
|
|
onClick={clearAllLogs}
|
|
variant="secondary"
|
|
size="sm"
|
|
>
|
|
Clear Logs
|
|
</Button>
|
|
</Grid>
|
|
</Stack>
|
|
)}
|
|
|
|
{/* Quick Links */}
|
|
{debugEnabled && (
|
|
<Stack gap={1}>
|
|
<Text size="xs" weight="semibold" color="text-gray-400">Quick Access</Text>
|
|
<Stack color="text-gray-500" className="font-mono" style={{ fontSize: '10px' }}>
|
|
<Text block>• window.__GRIDPILOT_GLOBAL_HANDLER__</Text>
|
|
<Text block>• window.__GRIDPILOT_API_LOGGER__</Text>
|
|
<Text block>• window.__GRIDPILOT_REACT_ERRORS__</Text>
|
|
</Stack>
|
|
</Stack>
|
|
)}
|
|
|
|
{/* Status */}
|
|
<Stack pt={2} borderTop={true} borderColor="border-charcoal-outline" align="center">
|
|
<Text size="xs" color="text-gray-500" style={{ fontSize: '10px' }} textAlign="center">
|
|
{debugEnabled ? 'Debug features active' : 'Debug mode disabled'}
|
|
{isDev && ' • Development Environment'}
|
|
</Text>
|
|
</Stack>
|
|
</Stack>
|
|
</Stack>
|
|
</Stack>
|
|
)}
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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 = useCallback(() => {
|
|
setDebugEnabled(true);
|
|
localStorage.setItem('gridpilot_debug_enabled', 'true');
|
|
|
|
// Initialize debug features
|
|
const globalHandler = getGlobalErrorHandler();
|
|
globalHandler.initialize();
|
|
|
|
const apiLogger = getGlobalApiLogger();
|
|
if (!window.__GRIDPILOT_FETCH_LOGGED__) {
|
|
const loggedFetch = apiLogger.createLoggedFetch();
|
|
window.fetch = loggedFetch as typeof fetch;
|
|
window.__GRIDPILOT_FETCH_LOGGED__ = true;
|
|
}
|
|
|
|
window.__GRIDPILOT_GLOBAL_HANDLER__ = globalHandler;
|
|
window.__GRIDPILOT_API_LOGGER__ = apiLogger;
|
|
}, []);
|
|
|
|
const disable = useCallback(() => {
|
|
setDebugEnabled(false);
|
|
localStorage.setItem('gridpilot_debug_enabled', 'false');
|
|
|
|
const globalHandler = getGlobalErrorHandler();
|
|
globalHandler.destroy();
|
|
}, []);
|
|
|
|
const toggle = useCallback(() => {
|
|
if (debugEnabled) {
|
|
disable();
|
|
} else {
|
|
enable();
|
|
}
|
|
}, [debugEnabled, enable, disable]);
|
|
|
|
return {
|
|
enabled: debugEnabled,
|
|
enable,
|
|
disable,
|
|
toggle,
|
|
};
|
|
}
|