Files
gridpilot.gg/apps/website/components/dev/DebugModeToggle.tsx
2026-01-18 22:55:55 +01:00

335 lines
11 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 { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { FloatingAction } from '@/ui/FloatingAction';
import { DebugPanel } from '@/ui/DebugPanel';
import { StatGrid } from '@/ui/StatGrid';
import { Bug, Shield } from 'lucide-react';
import React, { useCallback, useEffect, useState } from 'react';
// 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 (
<React.Fragment>
{/* Main Toggle Button */}
{!isOpen && (
<FloatingAction
onClick={() => setIsOpen(true)}
variant={debugEnabled ? 'primary' : 'secondary'}
title={debugEnabled ? 'Debug Mode Active' : 'Enable Debug Mode'}
>
<Icon icon={Bug} size={5} />
</FloatingAction>
)}
{/* Debug Panel */}
{isOpen && (
<DebugPanel
title="Debug Control"
onClose={() => setIsOpen(false)}
icon={<Icon icon={Bug} size={4} intent="success" />}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{/* Debug Toggle */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0.5rem', backgroundColor: 'var(--ui-color-bg-surface-muted)', borderRadius: '0.5rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Icon icon={Shield} size={4} intent={debugEnabled ? 'success' : 'low'} />
<Text size="sm" weight="medium">Debug Mode</Text>
</div>
<Button
onClick={toggleDebug}
size="sm"
variant={debugEnabled ? 'primary' : 'secondary'}
>
{debugEnabled ? 'ON' : 'OFF'}
</Button>
</div>
{/* Metrics */}
{debugEnabled && (
<StatGrid
variant="box"
columns={3}
stats={[
{ label: 'Errors', value: metrics.errors, intent: 'critical', icon: Bug },
{ label: 'API', value: metrics.apiRequests, intent: 'primary', icon: Bug },
{ label: 'Failures', value: metrics.apiFailures, intent: 'warning', icon: Bug },
]}
/>
)}
{/* Actions */}
{debugEnabled && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<Text size="xs" weight="semibold" variant="low" uppercase>Test Actions</Text>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.5rem' }}>
<Button onClick={triggerTestError} variant="danger" size="sm">Test Error</Button>
<Button onClick={triggerTestApiCall} size="sm">Test API</Button>
</div>
<Text size="xs" weight="semibold" variant="low" uppercase style={{ marginTop: '0.5rem' }}>Utilities</Text>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.5rem' }}>
<Button onClick={copyDebugInfo} variant="secondary" size="sm">Copy Info</Button>
<Button onClick={clearAllLogs} variant="secondary" size="sm">Clear Logs</Button>
</div>
</div>
)}
{/* Quick Links */}
{debugEnabled && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
<Text size="xs" weight="semibold" variant="low" uppercase>Quick Access</Text>
<div style={{ fontSize: '10px', color: 'var(--ui-color-text-low)', fontFamily: 'var(--ui-font-mono)' }}>
<div> window.__GRIDPILOT_GLOBAL_HANDLER__</div>
<div> window.__GRIDPILOT_API_LOGGER__</div>
<div> window.__GRIDPILOT_REACT_ERRORS__</div>
</div>
</div>
)}
{/* Status */}
<div style={{ paddingTop: '0.5rem', borderTop: '1px solid var(--ui-color-border-muted)', textAlign: 'center' }}>
<Text size="xs" variant="low" style={{ fontSize: '10px' }}>
{debugEnabled ? 'Debug features active' : 'Debug mode disabled'}
{isDev && ' • Development Environment'}
</Text>
</div>
</div>
</DebugPanel>
)}
</React.Fragment>
);
}
/**
* 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,
};
}