Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,390 @@
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 short delay to ensure backend has stored them
setTimeout(() => fetchRequestMetrics(requestId), 200);
}
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) => {
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}`;
}
const response = await nativeFetch.call(window, `${API_BASE_URL}/v1/system/request-metrics/${requestId}/`, {
method: 'GET',
headers,
credentials: 'include', // Include session cookies for authentication
});
if (response.ok) {
const data = await response.json();
console.log('Fetched metrics for request:', requestId, data); // Debug log
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 data = await retryResponse.json();
metricsRef.current = [...metricsRef.current, data];
setMetrics([...metricsRef.current]);
return;
}
}
} catch (refreshError) {
// Refresh failed - user needs to re-login
console.warn('Token refresh failed, user may need to re-authenticate');
}
// Silently ignore 401 errors - user might not be authenticated
} else if (response.status === 404) {
// Metrics not found or expired - this is expected, silently ignore
// Metrics expire after 5 minutes, so 404 is normal for older requests
return;
} else {
console.warn('Failed to fetch metrics:', response.status, response.statusText);
}
} catch (error) {
// Only log non-network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
// Network error - silently ignore
return;
}
console.error('Failed to fetch request metrics:', error);
}
};
// Calculate page load time
const pageLoadTime = pageLoadStart ? performance.now() - pageLoadStart : null;
// Calculate totals
const totals = metrics.reduce((acc, m) => ({
elapsed_time_ms: acc.elapsed_time_ms + m.elapsed_time_ms,
cpu_total_ms: acc.cpu_total_ms + m.cpu.total_time_ms,
memory_delta_mb: acc.memory_delta_mb + m.memory.delta_mb,
io_read_mb: acc.io_read_mb + m.io.read_mb,
io_write_mb: acc.io_write_mb + m.io.write_mb,
}), {
elapsed_time_ms: 0,
cpu_total_ms: 0,
memory_delta_mb: 0,
io_read_mb: 0,
io_write_mb: 0,
});
// Find the slowest request
const slowestRequest = metrics.length > 0
? metrics.reduce((prev, current) =>
(current.elapsed_time_ms > prev.elapsed_time_ms) ? current : prev
)
: null;
// Find the request with highest CPU usage
const highestCpuRequest = metrics.length > 0
? metrics.reduce((prev, current) =>
(current.cpu.total_time_ms > prev.cpu.total_time_ms) ? current : prev
)
: null;
// Find the request with highest memory usage
const highestMemoryRequest = metrics.length > 0
? metrics.reduce((prev, current) =>
(current.memory.delta_mb > prev.memory.delta_mb) ? 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} {slowestRequest.path}
<br />
<span className="ml-4">Time: {slowestRequest.elapsed_time_ms.toFixed(2)} ms</span>
</div>
)}
{highestCpuRequest && highestCpuRequest.cpu.total_time_ms > 100 && (
<div>
<span className="font-semibold">Highest CPU:</span> {highestCpuRequest.method} {highestCpuRequest.path}
<br />
<span className="ml-4">CPU: {highestCpuRequest.cpu.total_time_ms.toFixed(2)} ms (System: {highestCpuRequest.cpu.system_percent.toFixed(1)}%)</span>
</div>
)}
{highestMemoryRequest && highestMemoryRequest.memory.delta_mb > 1 && (
<div>
<span className="font-semibold">Highest Memory:</span> {highestMemoryRequest.method} {highestMemoryRequest.path}
<br />
<span className="ml-4">Memory: {highestMemoryRequest.memory.delta_mb > 0 ? '+' : ''}{highestMemoryRequest.memory.delta_mb.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) => {
const isSlow = m.elapsed_time_ms > 1000;
const isHighCpu = m.cpu.total_time_ms > 100;
const isHighMemory = m.memory.delta_mb > 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: {m.elapsed_time_ms.toFixed(2)} ms
</div>
<div className={isHighCpu ? 'font-semibold text-red-700 dark:text-red-300' : ''}>
🔥 CPU: {m.cpu.total_time_ms.toFixed(2)} ms
<span className="text-gray-500"> (User: {m.cpu.user_time_ms.toFixed(2)}ms, System: {m.cpu.system_time_ms.toFixed(2)}ms)</span>
<br />
<span className="ml-4 text-gray-500">System CPU: {m.cpu.system_percent.toFixed(1)}%</span>
</div>
<div className={isHighMemory ? 'font-semibold text-red-700 dark:text-red-300' : ''}>
💾 Memory: {m.memory.delta_mb > 0 ? '+' : ''}{m.memory.delta_mb.toFixed(2)} MB
<span className="text-gray-500"> (Final RSS: {m.memory.final_rss_mb.toFixed(2)} MB)</span>
<br />
<span className="ml-4 text-gray-500">System Memory: {m.memory.system_used_percent.toFixed(1)}%</span>
</div>
{m.io.read_mb > 0 && (
<div>
📖 I/O Read: {m.io.read_mb.toFixed(2)} MB ({m.io.read_bytes.toLocaleString()} bytes)
</div>
)}
{m.io.write_mb > 0 && (
<div>
📝 I/O Write: {m.io.write_mb.toFixed(2)} MB ({m.io.write_bytes.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>
)}
</>
);
}

View File

@@ -0,0 +1,42 @@
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>
);
}