feat(migrations): Rename indexes and update global integration settings fields for improved clarity and functionality
feat(admin): Add API monitoring, debug console, and system health templates for enhanced admin interface docs: Add AI system cleanup summary and audit report detailing architecture, token management, and recommendations docs: Introduce credits and tokens system guide outlining configuration, data flow, and monitoring strategies
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
interface AwsAdminGuardProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route guard that only allows access to users of the aws-admin account
|
||||
* Used for the single remaining admin dashboard page
|
||||
*/
|
||||
export const AwsAdminGuard: React.FC<AwsAdminGuardProps> = ({ children }) => {
|
||||
const { user, loading } = useAuthStore();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user belongs to aws-admin account
|
||||
const isAwsAdmin = user?.account?.slug === 'aws-admin';
|
||||
|
||||
if (!isAwsAdmin) {
|
||||
return <Navigate to="/dashboard" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -32,14 +32,6 @@ interface ImageQueueModalProps {
|
||||
model?: string;
|
||||
provider?: string;
|
||||
onUpdateQueue?: (queue: ImageQueueItem[]) => void;
|
||||
onLog?: (log: {
|
||||
timestamp: string;
|
||||
type: 'request' | 'success' | 'error' | 'step';
|
||||
action: string;
|
||||
data: any;
|
||||
stepName?: string;
|
||||
percentage?: number;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export default function ImageQueueModal({
|
||||
@@ -51,7 +43,6 @@ export default function ImageQueueModal({
|
||||
model,
|
||||
provider,
|
||||
onUpdateQueue,
|
||||
onLog,
|
||||
}: ImageQueueModalProps) {
|
||||
const [localQueue, setLocalQueue] = useState<ImageQueueItem[]>(queue);
|
||||
// Track smooth progress animation for each item
|
||||
@@ -250,43 +241,6 @@ export default function ImageQueueModal({
|
||||
console.log(`[ImageQueueModal] Task completed with state:`, taskState);
|
||||
clearInterval(pollInterval);
|
||||
|
||||
// Log completion status
|
||||
if (onLog) {
|
||||
if (taskState === 'SUCCESS') {
|
||||
const result = data.result || (data.meta && data.meta.result);
|
||||
const completed = result?.completed || 0;
|
||||
const failed = result?.failed || 0;
|
||||
const total = result?.total_images || totalImages;
|
||||
|
||||
onLog({
|
||||
timestamp: new Date().toISOString(),
|
||||
type: failed > 0 ? 'error' : 'success',
|
||||
action: 'generate_images',
|
||||
stepName: 'Task Completed',
|
||||
data: {
|
||||
state: 'SUCCESS',
|
||||
completed,
|
||||
failed,
|
||||
total,
|
||||
results: result?.results || []
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// FAILURE
|
||||
onLog({
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'error',
|
||||
action: 'generate_images',
|
||||
stepName: 'Task Failed',
|
||||
data: {
|
||||
state: 'FAILURE',
|
||||
error: data.error || data.meta?.error || 'Task failed',
|
||||
meta: data.meta
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update final state
|
||||
if (taskState === 'SUCCESS' && data.result) {
|
||||
console.log(`[ImageQueueModal] Updating queue from result:`, data.result);
|
||||
|
||||
@@ -1,435 +0,0 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { API_BASE_URL } from '../../services/api';
|
||||
|
||||
interface RequestMetrics {
|
||||
request_id: string;
|
||||
path: string;
|
||||
method: string;
|
||||
elapsed_time_ms: number;
|
||||
cpu: {
|
||||
user_time_ms: number;
|
||||
system_time_ms: number;
|
||||
total_time_ms: number;
|
||||
system_percent: number;
|
||||
};
|
||||
memory: {
|
||||
delta_bytes: number;
|
||||
delta_mb: number;
|
||||
final_rss_mb: number;
|
||||
system_used_percent: number;
|
||||
};
|
||||
io: {
|
||||
read_bytes: number;
|
||||
read_mb: number;
|
||||
write_bytes: number;
|
||||
write_mb: number;
|
||||
};
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface ResourceDebugOverlayProps {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export default function ResourceDebugOverlay({ enabled }: ResourceDebugOverlayProps) {
|
||||
const { user } = useAuthStore();
|
||||
const [metrics, setMetrics] = useState<RequestMetrics[]>([]);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [pageLoadStart, setPageLoadStart] = useState<number | null>(null);
|
||||
const requestIdRef = useRef<string | null>(null);
|
||||
const metricsRef = useRef<RequestMetrics[]>([]);
|
||||
const originalFetchRef = useRef<typeof fetch | null>(null);
|
||||
const nativeFetchRef = useRef<typeof fetch | null>(null); // Store native fetch separately
|
||||
|
||||
// Check if user is admin/developer
|
||||
const isAdminOrDeveloper = user?.role === 'admin' || user?.role === 'developer';
|
||||
|
||||
// Track page load start and intercept fetch requests
|
||||
useEffect(() => {
|
||||
if (!enabled || !isAdminOrDeveloper) {
|
||||
// Restore native fetch if disabled
|
||||
if (nativeFetchRef.current) {
|
||||
window.fetch = nativeFetchRef.current;
|
||||
nativeFetchRef.current = null;
|
||||
originalFetchRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setPageLoadStart(performance.now());
|
||||
|
||||
// Store native fetch and create bound version
|
||||
if (!nativeFetchRef.current) {
|
||||
nativeFetchRef.current = window.fetch; // Store actual native fetch
|
||||
originalFetchRef.current = window.fetch.bind(window); // Create bound version for calling
|
||||
}
|
||||
|
||||
// Intercept fetch requests to track API calls
|
||||
window.fetch = async function(...args) {
|
||||
const startTime = performance.now();
|
||||
const [url, options = {}] = args;
|
||||
|
||||
// Don't intercept our own metrics fetch calls to avoid infinite loops
|
||||
const urlString = typeof url === 'string' ? url : url.toString();
|
||||
if (urlString.includes('/request-metrics/')) {
|
||||
// Use native fetch directly for metrics calls
|
||||
return nativeFetchRef.current!.apply(window, args as [RequestInfo | URL, RequestInit?]);
|
||||
}
|
||||
|
||||
// Add debug header to enable tracking
|
||||
const headers = new Headers(options.headers || {});
|
||||
headers.set('X-Debug-Resource-Tracking', 'true');
|
||||
|
||||
// Use bound fetch to preserve context
|
||||
const response = await originalFetchRef.current!(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
const endTime = performance.now();
|
||||
|
||||
// Get request ID from response header
|
||||
const requestId = response.headers.get('X-Resource-Tracking-ID');
|
||||
if (requestId) {
|
||||
requestIdRef.current = requestId;
|
||||
// Fetch metrics after a delay to ensure backend has stored them
|
||||
// Use a slightly longer delay to avoid race conditions
|
||||
setTimeout(() => fetchRequestMetrics(requestId), 300);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
return () => {
|
||||
// Restore native fetch on cleanup
|
||||
if (nativeFetchRef.current) {
|
||||
window.fetch = nativeFetchRef.current;
|
||||
nativeFetchRef.current = null;
|
||||
originalFetchRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [enabled, isAdminOrDeveloper]);
|
||||
|
||||
// Fetch metrics for a request - use fetchAPI to get proper authentication handling
|
||||
const fetchRequestMetrics = async (requestId: string, retryCount = 0) => {
|
||||
try {
|
||||
// Use fetchAPI which handles token refresh and authentication properly
|
||||
// But we need to use native fetch to avoid interception loop
|
||||
const nativeFetch = nativeFetchRef.current || window.fetch;
|
||||
const { token } = useAuthStore.getState();
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
// Add JWT token if available
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// Silently handle 404s and other errors - metrics might not exist for all requests
|
||||
try {
|
||||
const response = await nativeFetch.call(window, `${API_BASE_URL}/v1/system/request-metrics/${requestId}/`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
credentials: 'include', // Include session cookies for authentication
|
||||
});
|
||||
|
||||
// Silently ignore 404s - metrics endpoint might not exist for all requests
|
||||
if (response.status === 404) {
|
||||
return; // Don't log or retry 404s
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const responseData = await response.json();
|
||||
// Extract data from unified API response format: {success: true, data: {...}}
|
||||
const data = responseData?.data || responseData;
|
||||
// Only log in debug mode to reduce console noise
|
||||
if (import.meta.env.DEV) {
|
||||
console.debug('Fetched metrics for request:', requestId, data);
|
||||
}
|
||||
// Validate data structure before adding
|
||||
if (data && typeof data === 'object' && data.request_id) {
|
||||
metricsRef.current = [...metricsRef.current, data];
|
||||
setMetrics([...metricsRef.current]);
|
||||
}
|
||||
} else if (response.status === 401) {
|
||||
// Token might be expired - try to refresh and retry once
|
||||
try {
|
||||
await useAuthStore.getState().refreshToken();
|
||||
const newToken = useAuthStore.getState().token;
|
||||
if (newToken) {
|
||||
const retryHeaders: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${newToken}`,
|
||||
};
|
||||
const retryResponse = await nativeFetch.call(window, `${API_BASE_URL}/v1/system/request-metrics/${requestId}/`, {
|
||||
method: 'GET',
|
||||
headers: retryHeaders,
|
||||
credentials: 'include',
|
||||
});
|
||||
if (retryResponse.ok) {
|
||||
const responseData = await retryResponse.json();
|
||||
// Extract data from unified API response format: {success: true, data: {...}}
|
||||
const data = responseData?.data || responseData;
|
||||
// Validate data structure before adding
|
||||
if (data && typeof data === 'object' && data.request_id) {
|
||||
metricsRef.current = [...metricsRef.current, data];
|
||||
setMetrics([...metricsRef.current]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (refreshError) {
|
||||
// Refresh failed - silently ignore
|
||||
}
|
||||
// Silently ignore 401 errors - user might not be authenticated
|
||||
} else if (response.status === 404) {
|
||||
// Metrics not found - silently ignore (metrics might not exist for all requests)
|
||||
return;
|
||||
} else {
|
||||
// Other errors - silently ignore
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently ignore all fetch errors (network errors, etc.)
|
||||
// Metrics are optional and not critical for functionality
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently ignore all errors
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate page load time
|
||||
const pageLoadTime = pageLoadStart ? performance.now() - pageLoadStart : null;
|
||||
|
||||
// Calculate totals - with null safety checks
|
||||
const totals = metrics.reduce((acc, m) => {
|
||||
// Safely access nested properties with defaults
|
||||
const elapsed = m?.elapsed_time_ms || 0;
|
||||
const cpuTotal = m?.cpu?.total_time_ms || 0;
|
||||
const memoryDelta = m?.memory?.delta_mb || 0;
|
||||
const ioRead = m?.io?.read_mb || 0;
|
||||
const ioWrite = m?.io?.write_mb || 0;
|
||||
|
||||
return {
|
||||
elapsed_time_ms: acc.elapsed_time_ms + elapsed,
|
||||
cpu_total_ms: acc.cpu_total_ms + cpuTotal,
|
||||
memory_delta_mb: acc.memory_delta_mb + memoryDelta,
|
||||
io_read_mb: acc.io_read_mb + ioRead,
|
||||
io_write_mb: acc.io_write_mb + ioWrite,
|
||||
};
|
||||
}, {
|
||||
elapsed_time_ms: 0,
|
||||
cpu_total_ms: 0,
|
||||
memory_delta_mb: 0,
|
||||
io_read_mb: 0,
|
||||
io_write_mb: 0,
|
||||
});
|
||||
|
||||
// Find the slowest request - with null safety
|
||||
const slowestRequest = metrics.length > 0
|
||||
? metrics.reduce((prev, current) => {
|
||||
const prevTime = prev?.elapsed_time_ms || 0;
|
||||
const currentTime = current?.elapsed_time_ms || 0;
|
||||
return (currentTime > prevTime) ? current : prev;
|
||||
})
|
||||
: null;
|
||||
|
||||
// Find the request with highest CPU usage - with null safety
|
||||
const highestCpuRequest = metrics.length > 0
|
||||
? metrics.reduce((prev, current) => {
|
||||
const prevCpu = prev?.cpu?.total_time_ms || 0;
|
||||
const currentCpu = current?.cpu?.total_time_ms || 0;
|
||||
return (currentCpu > prevCpu) ? current : prev;
|
||||
})
|
||||
: null;
|
||||
|
||||
// Find the request with highest memory usage - with null safety
|
||||
const highestMemoryRequest = metrics.length > 0
|
||||
? metrics.reduce((prev, current) => {
|
||||
const prevMemory = prev?.memory?.delta_mb || 0;
|
||||
const currentMemory = current?.memory?.delta_mb || 0;
|
||||
return (currentMemory > prevMemory) ? current : prev;
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!enabled || !isAdminOrDeveloper) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Toggle Button - Fixed position */}
|
||||
<button
|
||||
onClick={() => setIsVisible(!isVisible)}
|
||||
className="fixed bottom-4 right-4 z-[99999] bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg shadow-lg text-sm font-medium flex items-center gap-2"
|
||||
title="Toggle Resource Debug Overlay"
|
||||
>
|
||||
<span>🔍</span>
|
||||
<span>Debug ({metrics.length})</span>
|
||||
</button>
|
||||
|
||||
{/* Overlay */}
|
||||
{isVisible && (
|
||||
<div className="fixed bottom-20 right-4 z-[99998] bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg shadow-2xl w-[500px] max-h-[85vh] overflow-auto">
|
||||
<div className="sticky top-0 bg-gray-100 dark:bg-gray-800 px-4 py-3 border-b border-gray-300 dark:border-gray-700 flex justify-between items-center">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white">Resource Debug</h3>
|
||||
<button
|
||||
onClick={() => setIsVisible(false)}
|
||||
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Page Load Summary */}
|
||||
{pageLoadTime && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-3 rounded border border-blue-200 dark:border-blue-800">
|
||||
<h4 className="font-semibold text-blue-900 dark:text-blue-200 mb-2">Page Load Time</h4>
|
||||
<div className="text-sm text-blue-800 dark:text-blue-300">
|
||||
{pageLoadTime.toFixed(2)} ms
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Performance Summary - Highlight Culprits */}
|
||||
{metrics.length > 0 && (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-3 rounded border border-yellow-200 dark:border-yellow-800">
|
||||
<h4 className="font-semibold text-yellow-900 dark:text-yellow-200 mb-2">⚠️ Performance Culprits</h4>
|
||||
<div className="text-xs space-y-2 text-yellow-800 dark:text-yellow-300">
|
||||
{slowestRequest && (
|
||||
<div>
|
||||
<span className="font-semibold">Slowest Request:</span> {slowestRequest.method || 'N/A'} {slowestRequest.path || 'N/A'}
|
||||
<br />
|
||||
<span className="ml-4">Time: {(slowestRequest.elapsed_time_ms || 0).toFixed(2)} ms</span>
|
||||
</div>
|
||||
)}
|
||||
{highestCpuRequest && highestCpuRequest.cpu && (highestCpuRequest.cpu.total_time_ms || 0) > 100 && (
|
||||
<div>
|
||||
<span className="font-semibold">Highest CPU:</span> {highestCpuRequest.method || 'N/A'} {highestCpuRequest.path || 'N/A'}
|
||||
<br />
|
||||
<span className="ml-4">CPU: {(highestCpuRequest.cpu.total_time_ms || 0).toFixed(2)} ms (System: {(highestCpuRequest.cpu.system_percent || 0).toFixed(1)}%)</span>
|
||||
</div>
|
||||
)}
|
||||
{highestMemoryRequest && highestMemoryRequest.memory && (highestMemoryRequest.memory.delta_mb || 0) > 1 && (
|
||||
<div>
|
||||
<span className="font-semibold">Highest Memory:</span> {highestMemoryRequest.method || 'N/A'} {highestMemoryRequest.path || 'N/A'}
|
||||
<br />
|
||||
<span className="ml-4">Memory: {(highestMemoryRequest.memory.delta_mb || 0) > 0 ? '+' : ''}{(highestMemoryRequest.memory.delta_mb || 0).toFixed(2)} MB</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Totals */}
|
||||
{metrics.length > 0 && (
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded border border-gray-200 dark:border-gray-700">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">Request Totals</h4>
|
||||
<div className="text-xs space-y-1 text-gray-700 dark:text-gray-300">
|
||||
<div>Total Requests: {metrics.length}</div>
|
||||
<div>Total Time: {totals.elapsed_time_ms.toFixed(2)} ms</div>
|
||||
<div>Total CPU Time: {totals.cpu_total_ms.toFixed(2)} ms</div>
|
||||
<div>Total Memory Delta: {totals.memory_delta_mb > 0 ? '+' : ''}{totals.memory_delta_mb.toFixed(2)} MB</div>
|
||||
<div>Total I/O Read: {totals.io_read_mb.toFixed(2)} MB</div>
|
||||
<div>Total I/O Write: {totals.io_write_mb.toFixed(2)} MB</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Individual Requests - Detailed View */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">All Requests (Detailed)</h4>
|
||||
{metrics.length === 0 ? (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
No requests tracked yet. Navigate to trigger API calls.
|
||||
<br />
|
||||
<span className="text-xs">Make sure debug toggle is enabled in header.</span>
|
||||
</div>
|
||||
) : (
|
||||
metrics.map((m, idx) => {
|
||||
// Safely access properties with defaults
|
||||
const elapsedTime = m?.elapsed_time_ms || 0;
|
||||
const cpuTotal = m?.cpu?.total_time_ms || 0;
|
||||
const memoryDelta = m?.memory?.delta_mb || 0;
|
||||
|
||||
const isSlow = elapsedTime > 1000;
|
||||
const isHighCpu = cpuTotal > 100;
|
||||
const isHighMemory = memoryDelta > 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`p-3 rounded border text-xs ${
|
||||
isSlow || isHighCpu || isHighMemory
|
||||
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'
|
||||
: 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div className="font-semibold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<span>{m.method}</span>
|
||||
<span className="text-gray-600 dark:text-gray-400 truncate">{m.path}</span>
|
||||
{(isSlow || isHighCpu || isHighMemory) && (
|
||||
<span className="text-red-600 dark:text-red-400 text-xs">⚠️</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1 text-gray-700 dark:text-gray-300">
|
||||
<div className={isSlow ? 'font-semibold text-red-700 dark:text-red-300' : ''}>
|
||||
⏱️ Time: {elapsedTime.toFixed(2)} ms
|
||||
</div>
|
||||
{m?.cpu && (
|
||||
<div className={isHighCpu ? 'font-semibold text-red-700 dark:text-red-300' : ''}>
|
||||
🔥 CPU: {cpuTotal.toFixed(2)} ms
|
||||
<span className="text-gray-500"> (User: {(m.cpu.user_time_ms || 0).toFixed(2)}ms, System: {(m.cpu.system_time_ms || 0).toFixed(2)}ms)</span>
|
||||
<br />
|
||||
<span className="ml-4 text-gray-500">System CPU: {(m.cpu.system_percent || 0).toFixed(1)}%</span>
|
||||
</div>
|
||||
)}
|
||||
{m?.memory && (
|
||||
<div className={isHighMemory ? 'font-semibold text-red-700 dark:text-red-300' : ''}>
|
||||
💾 Memory: {memoryDelta > 0 ? '+' : ''}{memoryDelta.toFixed(2)} MB
|
||||
<span className="text-gray-500"> (Final RSS: {(m.memory.final_rss_mb || 0).toFixed(2)} MB)</span>
|
||||
<br />
|
||||
<span className="ml-4 text-gray-500">System Memory: {(m.memory.system_used_percent || 0).toFixed(1)}%</span>
|
||||
</div>
|
||||
)}
|
||||
{m?.io?.read_mb > 0 && (
|
||||
<div>
|
||||
📖 I/O Read: {m.io.read_mb.toFixed(2)} MB ({(m.io.read_bytes || 0).toLocaleString()} bytes)
|
||||
</div>
|
||||
)}
|
||||
{m?.io?.write_mb > 0 && (
|
||||
<div>
|
||||
📝 I/O Write: {m.io.write_mb.toFixed(2)} MB ({(m.io.write_bytes || 0).toLocaleString()} bytes)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Clear Button */}
|
||||
{metrics.length > 0 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setMetrics([]);
|
||||
metricsRef.current = [];
|
||||
setPageLoadStart(performance.now());
|
||||
}}
|
||||
className="w-full bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white px-3 py-2 rounded text-sm"
|
||||
>
|
||||
Clear Metrics
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
export default function ResourceDebugToggle() {
|
||||
const { user } = useAuthStore();
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
const isAdminOrDeveloper = user?.role === 'admin' || user?.role === 'developer';
|
||||
|
||||
// Load saved state from localStorage
|
||||
useEffect(() => {
|
||||
if (isAdminOrDeveloper) {
|
||||
const saved = localStorage.getItem('debug_resource_tracking_enabled');
|
||||
setEnabled(saved === 'true');
|
||||
}
|
||||
}, [isAdminOrDeveloper]);
|
||||
|
||||
const toggle = () => {
|
||||
const newValue = !enabled;
|
||||
setEnabled(newValue);
|
||||
localStorage.setItem('debug_resource_tracking_enabled', String(newValue));
|
||||
// Dispatch event for overlay component
|
||||
window.dispatchEvent(new CustomEvent('debug-resource-tracking-toggle', { detail: newValue }));
|
||||
};
|
||||
|
||||
if (!isAdminOrDeveloper) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggle}
|
||||
className={`flex items-center justify-center w-10 h-10 rounded-lg transition-colors ${
|
||||
enabled
|
||||
? 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400'
|
||||
: 'text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
title={enabled ? 'Disable Resource Debug' : 'Enable Resource Debug'}
|
||||
>
|
||||
🔍
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user