import { useState, useEffect, useCallback, useRef } from "react"; import { useLocation } from "react-router-dom"; import { API_BASE_URL } from "../../services/api"; import { useAuthStore } from "../../store/authStore"; interface GroupStatus { name: string; abbreviation: string; healthy: number; total: number; isHealthy: boolean; } const endpointGroups = [ { name: "Core Health & Auth", abbreviation: "CO", endpoints: [ { path: "/v1/system/status/", method: "GET" }, { path: "/v1/auth/login/", method: "POST" }, { path: "/v1/auth/me/", method: "GET" }, { path: "/v1/auth/register/", method: "POST" }, ], }, { name: "Auth & User Management", abbreviation: "AU", endpoints: [ { path: "/v1/auth/users/", method: "GET" }, { path: "/v1/auth/accounts/", method: "GET" }, { path: "/v1/auth/sites/", method: "GET" }, { path: "/v1/auth/sectors/", method: "GET" }, { path: "/v1/auth/plans/", method: "GET" }, { path: "/v1/auth/industries/", method: "GET" }, { path: "/v1/auth/seed-keywords/", method: "GET" }, { path: "/v1/auth/site-access/", method: "GET" }, ], }, { name: "Planner Module", abbreviation: "PM", endpoints: [ { path: "/v1/planner/keywords/", method: "GET" }, { path: "/v1/planner/keywords/auto_cluster/", method: "POST" }, { path: "/v1/planner/keywords/bulk_delete/", method: "POST" }, { path: "/v1/planner/clusters/", method: "GET" }, { path: "/v1/planner/clusters/auto_generate_ideas/", method: "POST" }, { path: "/v1/planner/ideas/", method: "GET" }, ], }, { name: "Writer Module", abbreviation: "WM", endpoints: [ { path: "/v1/writer/tasks/", method: "GET" }, { path: "/v1/writer/tasks/auto_generate_content/", method: "POST" }, { path: "/v1/writer/tasks/bulk_update/", method: "POST" }, { path: "/v1/writer/content/", method: "GET" }, { path: "/v1/writer/content/generate_image_prompts/", method: "POST" }, { path: "/v1/writer/images/", method: "GET" }, { path: "/v1/writer/images/generate_images/", method: "POST" }, ], }, { name: "CRUD Operations - Planner", abbreviation: "PC", endpoints: [ { path: "/v1/planner/keywords/", method: "GET" }, { path: "/v1/planner/keywords/", method: "POST" }, { path: "/v1/planner/keywords/1/", method: "GET" }, { path: "/v1/planner/keywords/1/", method: "PUT" }, { path: "/v1/planner/keywords/1/", method: "DELETE" }, { path: "/v1/planner/clusters/", method: "GET" }, { path: "/v1/planner/clusters/", method: "POST" }, { path: "/v1/planner/clusters/1/", method: "GET" }, { path: "/v1/planner/clusters/1/", method: "PUT" }, { path: "/v1/planner/clusters/1/", method: "DELETE" }, { path: "/v1/planner/ideas/", method: "GET" }, { path: "/v1/planner/ideas/", method: "POST" }, { path: "/v1/planner/ideas/1/", method: "GET" }, { path: "/v1/planner/ideas/1/", method: "PUT" }, { path: "/v1/planner/ideas/1/", method: "DELETE" }, ], }, { name: "CRUD Operations - Writer", abbreviation: "WC", endpoints: [ { path: "/v1/writer/tasks/", method: "GET" }, { path: "/v1/writer/tasks/", method: "POST" }, { path: "/v1/writer/tasks/1/", method: "GET" }, { path: "/v1/writer/tasks/1/", method: "PUT" }, { path: "/v1/writer/tasks/1/", method: "DELETE" }, { path: "/v1/writer/content/", method: "GET" }, { path: "/v1/writer/content/", method: "POST" }, { path: "/v1/writer/content/1/", method: "GET" }, { path: "/v1/writer/content/1/", method: "PUT" }, { path: "/v1/writer/content/1/", method: "DELETE" }, { path: "/v1/writer/images/", method: "GET" }, { path: "/v1/writer/images/", method: "POST" }, { path: "/v1/writer/images/1/", method: "GET" }, { path: "/v1/writer/images/1/", method: "PUT" }, { path: "/v1/writer/images/1/", method: "DELETE" }, ], }, { name: "System & Billing", abbreviation: "SY", endpoints: [ { path: "/v1/system/prompts/", method: "GET" }, { path: "/v1/system/author-profiles/", method: "GET" }, { path: "/v1/system/strategies/", method: "GET" }, { path: "/v1/system/settings/integrations/openai/test/", method: "POST" }, { path: "/v1/system/settings/account/", method: "GET" }, { path: "/v1/billing/credits/balance/", method: "GET" }, { path: "/v1/billing/credits/usage/", method: "GET" }, { path: "/v1/billing/credits/usage/summary/", method: "GET" }, { path: "/v1/billing/credits/transactions/", method: "GET" }, ], }, ]; export default function ApiStatusIndicator() { const { user } = useAuthStore(); const location = useLocation(); const [groupStatuses, setGroupStatuses] = useState([]); const [isChecking, setIsChecking] = useState(false); const intervalRef = useRef | null>(null); // Only show and run for aws-admin accounts const isAwsAdmin = user?.account?.slug === 'aws-admin'; // Only run API checks on API monitor page to avoid console errors on other pages const isApiMonitorPage = location.pathname === '/settings/api-monitor'; const checkEndpoint = useCallback(async (path: string, method: string): Promise<'healthy' | 'warning' | 'error'> => { try { const token = localStorage.getItem('auth_token') || (() => { try { const authStorage = localStorage.getItem('auth-storage'); if (authStorage) { const parsed = JSON.parse(authStorage); return parsed?.state?.token || ''; } } catch (e) { // Ignore parsing errors } return ''; })(); const headers: HeadersInit = { 'Content-Type': 'application/json', }; if (token) { headers['Authorization'] = `Bearer ${token}`; } const isExpensiveAIEndpoint = path.includes('/auto_generate_content') || path.includes('/auto_cluster') || path.includes('/auto_generate_ideas') || path.includes('/generate_image_prompts') || path.includes('/generate_images'); let actualMethod = method; let fetchOptions: RequestInit = { method: actualMethod, headers, credentials: 'include', }; if (method === 'POST' && isExpensiveAIEndpoint) { actualMethod = 'OPTIONS'; fetchOptions.method = 'OPTIONS'; delete (fetchOptions as any).body; } else if (method === 'POST') { let body: any = {}; if (path.includes('/test/')) { body = {}; } else if (path.includes('/login/')) { body = { username: 'test', password: 'test' }; } else if (path.includes('/register/')) { body = { username: 'test', email: 'test@test.com', password: 'test' }; } else if (path.includes('/bulk_delete/')) { body = { ids: [] }; // Empty array to trigger validation error } else if (path.includes('/bulk_update/')) { body = { ids: [] }; // Empty array to trigger validation error } fetchOptions.body = JSON.stringify(body); } else if (method === 'PUT' || method === 'DELETE') { // For PUT/DELETE, we need to send a body for PUT or handle DELETE if (method === 'PUT') { fetchOptions.body = JSON.stringify({}); // Empty object to trigger validation } } // Suppress console errors for expected 400 responses (validation errors from test data) // These are expected and indicate the endpoint is working const isExpected400 = method === 'POST' && ( path.includes('/login/') || path.includes('/register/') || path.includes('/bulk_') || path.includes('/test/') ); // Use a silent fetch that won't log to console for expected errors let response: Response; try { response = await fetch(`${API_BASE_URL}${path}`, fetchOptions); } catch (fetchError) { // Network errors are real errors return 'error'; } if (actualMethod === 'OPTIONS') { if (response.status === 200) { return 'healthy'; } else if (response.status === 404) { return 'error'; } else if (response.status >= 500) { return 'error'; } return 'warning'; } else if (method === 'GET') { if (response.status >= 200 && response.status < 300) { return 'healthy'; } else if (response.status === 401 || response.status === 403) { return 'warning'; } else if (response.status === 404) { // For GET requests to specific resource IDs (e.g., /v1/planner/keywords/1/), // 404 is expected and healthy (resource doesn't exist, but endpoint works correctly) // For other GET requests (like list endpoints), 404 means endpoint doesn't exist const isResourceByIdRequest = /\/\d+\/?$/.test(path); // Path ends with /number/ or /number if (isResourceByIdRequest) { return 'healthy'; // GET to specific ID returning 404 is healthy (endpoint exists, resource doesn't) } return 'error'; // Endpoint doesn't exist } else if (response.status >= 500) { return 'error'; } return 'warning'; } else if (method === 'POST') { // Suppress console errors for expected 400 responses (validation errors from test data) // CRUD POST endpoints (like /v1/planner/keywords/, /v1/writer/tasks/) return 400 for empty/invalid test data const isExpected400 = path.includes('/login/') || path.includes('/register/') || path.includes('/bulk_') || path.includes('/test/') || // CRUD CREATE endpoints - POST to list endpoints (no ID in path, ends with / or exact match) /\/v1\/(planner|writer)\/(keywords|clusters|ideas|tasks|content|images)\/?$/.test(path); if (response.status === 400) { // 400 is expected for test requests - endpoint is working // Don't log warnings for expected 400s - they're normal validation errors return 'healthy'; } else if (response.status >= 200 && response.status < 300) { return 'healthy'; } else if (response.status === 401 || response.status === 403) { return 'warning'; } else if (response.status === 404) { return 'error'; } else if (response.status >= 500) { return 'error'; } return 'warning'; } else if (method === 'PUT' || method === 'DELETE') { // UPDATE/DELETE operations if (response.status === 400 || response.status === 404) { // 400/404 expected for test requests - endpoint is working return 'healthy'; } else if (response.status === 204 || (response.status >= 200 && response.status < 300)) { return 'healthy'; } else if (response.status === 401 || response.status === 403) { return 'warning'; } else if (response.status >= 500) { return 'error'; } return 'warning'; } return 'warning'; } catch (err) { return 'error'; } }, []); const checkAllGroups = useCallback(async () => { setIsChecking(true); const statusPromises = endpointGroups.map(async (group) => { const endpointChecks = group.endpoints.map(ep => checkEndpoint(ep.path, ep.method)); const results = await Promise.all(endpointChecks); const healthy = results.filter(s => s === 'healthy').length; const total = results.length; const isHealthy = healthy === total; return { name: group.name, abbreviation: group.abbreviation, healthy, total, isHealthy, }; }); const statuses = await Promise.all(statusPromises); setGroupStatuses(statuses); setIsChecking(false); }, [checkEndpoint]); useEffect(() => { // Only run if aws-admin and on API monitor page if (!isAwsAdmin || !isApiMonitorPage) { return; } // Initial check checkAllGroups(); // Get refresh interval from localStorage (same as API Monitor page) const getRefreshInterval = () => { const saved = localStorage.getItem('api-monitor-refresh-interval'); return saved ? parseInt(saved, 10) * 1000 : 30000; // Convert to milliseconds }; // Setup interval function that reads fresh interval value each time const setupInterval = () => { if (intervalRef.current) { clearTimeout(intervalRef.current); } // Use a recursive timeout that reads the interval each time const scheduleNext = () => { const interval = getRefreshInterval(); intervalRef.current = setTimeout(() => { checkAllGroups(); scheduleNext(); // Schedule next check }, interval); }; scheduleNext(); }; // Initial interval setup setupInterval(); // Listen for storage changes (when user changes interval in another tab) const handleStorageChange = (e: StorageEvent) => { if (e.key === 'api-monitor-refresh-interval') { setupInterval(); } }; // Listen for custom event (when user changes interval in same tab) const handleCustomStorageChange = () => { setupInterval(); }; window.addEventListener('storage', handleStorageChange); window.addEventListener('api-monitor-interval-changed', handleCustomStorageChange); return () => { if (intervalRef.current) { clearTimeout(intervalRef.current); } window.removeEventListener('storage', handleStorageChange); window.removeEventListener('api-monitor-interval-changed', handleCustomStorageChange); }; }, [checkAllGroups, isAwsAdmin, isApiMonitorPage]); const getStatusColor = (isHealthy: boolean) => { if (isHealthy) { return 'bg-green-500 dark:bg-green-400'; // Success color for 100% } else { return 'bg-yellow-500 dark:bg-yellow-400'; // Warning color for < 100% } }; // Return null if not aws-admin account or not on API monitor page // This check must come AFTER all hooks are called if (!isAwsAdmin || !isApiMonitorPage) { return null; } if (groupStatuses.length === 0 && !isChecking) { return null; } return (
{groupStatuses.map((group, index) => (
{group.abbreviation}
))}
); }