From 6109369df44f6ae8e19700e10da0167ca0bfc92e Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Sat, 15 Nov 2025 13:44:39 +0000 Subject: [PATCH] Add API Monitor route and sidebar entry --- frontend/src/App.tsx | 6 + frontend/src/layout/AppSidebar.tsx | 1 + frontend/src/pages/Settings/ApiMonitor.tsx | 402 +++++++++++++++++++++ 3 files changed, 409 insertions(+) create mode 100644 frontend/src/pages/Settings/ApiMonitor.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 64bf6c10..86d94459 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -63,6 +63,7 @@ const AISettings = lazy(() => import("./pages/Settings/AI")); const Plans = lazy(() => import("./pages/Settings/Plans")); const Industries = lazy(() => import("./pages/Settings/Industries")); const Status = lazy(() => import("./pages/Settings/Status")); +const ApiMonitor = lazy(() => import("./pages/Settings/ApiMonitor")); const Integration = lazy(() => import("./pages/Settings/Integration")); const Sites = lazy(() => import("./pages/Settings/Sites")); const ImportExport = lazy(() => import("./pages/Settings/ImportExport")); @@ -315,6 +316,11 @@ export default function App() { } /> + + + + } /> diff --git a/frontend/src/layout/AppSidebar.tsx b/frontend/src/layout/AppSidebar.tsx index 709d06d4..e3a1b52b 100644 --- a/frontend/src/layout/AppSidebar.tsx +++ b/frontend/src/layout/AppSidebar.tsx @@ -199,6 +199,7 @@ const AppSidebar: React.FC = () => { name: "System Health", subItems: [ { name: "Status", path: "/settings/status" }, + { name: "API Monitor", path: "/settings/api-monitor" }, ], }, { diff --git a/frontend/src/pages/Settings/ApiMonitor.tsx b/frontend/src/pages/Settings/ApiMonitor.tsx new file mode 100644 index 00000000..6a4fa9c6 --- /dev/null +++ b/frontend/src/pages/Settings/ApiMonitor.tsx @@ -0,0 +1,402 @@ +import { useState, useEffect, useCallback } from "react"; +import PageMeta from "../../components/common/PageMeta"; +import ComponentCard from "../../components/common/ComponentCard"; +import { API_BASE_URL } from "../../services/api"; + +interface EndpointStatus { + endpoint: string; + method: string; + status: 'healthy' | 'warning' | 'error' | 'checking'; + responseTime?: number; + lastChecked?: string; + error?: string; +} + +interface EndpointGroup { + name: string; + endpoints: { + path: string; + method: string; + description: string; + }[]; +} + +const endpointGroups: EndpointGroup[] = [ + { + name: "Core Health & Auth", + endpoints: [ + { path: "/api/ping/", method: "GET", description: "Health check" }, + { path: "/v1/system/status/", method: "GET", description: "System status" }, + { path: "/v1/auth/login/", method: "POST", description: "Login" }, + { path: "/v1/auth/me/", method: "GET", description: "Current user" }, + { path: "/v1/auth/register/", method: "POST", description: "Registration" }, + ], + }, + { + name: "Auth & User Management", + endpoints: [ + { path: "/v1/auth/users/", method: "GET", description: "List users" }, + { path: "/v1/auth/accounts/", method: "GET", description: "List accounts" }, + { path: "/v1/auth/sites/", method: "GET", description: "List sites" }, + { path: "/v1/auth/sectors/", method: "GET", description: "List sectors" }, + { path: "/v1/auth/plans/", method: "GET", description: "List plans" }, + { path: "/v1/auth/industries/", method: "GET", description: "List industries" }, + ], + }, + { + name: "Planner Module", + endpoints: [ + { path: "/v1/planner/keywords/", method: "GET", description: "List keywords" }, + { path: "/v1/planner/keywords/auto_cluster/", method: "POST", description: "AI clustering" }, + { path: "/v1/planner/clusters/", method: "GET", description: "List clusters" }, + { path: "/v1/planner/clusters/auto_generate_ideas/", method: "POST", description: "AI ideas" }, + { path: "/v1/planner/ideas/", method: "GET", description: "List ideas" }, + ], + }, + { + name: "Writer Module", + endpoints: [ + { path: "/v1/writer/tasks/", method: "GET", description: "List tasks" }, + { path: "/v1/writer/tasks/auto_generate_content/", method: "POST", description: "AI content" }, + { path: "/v1/writer/content/", method: "GET", description: "List content" }, + { path: "/v1/writer/content/generate_image_prompts/", method: "POST", description: "Image prompts" }, + { path: "/v1/writer/images/", method: "GET", description: "List images" }, + { path: "/v1/writer/images/generate_images/", method: "POST", description: "AI images" }, + ], + }, + { + name: "System & Billing", + endpoints: [ + { path: "/v1/system/prompts/", method: "GET", description: "List prompts" }, + { path: "/v1/system/settings/integrations/1/test/", method: "POST", description: "Test integration" }, + { path: "/v1/billing/credits/balance/balance/", method: "GET", description: "Credit balance" }, + { path: "/v1/billing/credits/usage/", method: "GET", description: "Usage logs" }, + ], + }, +]; + +const getStatusColor = (status: string) => { + switch (status) { + case 'healthy': return 'text-green-600 dark:text-green-400'; + case 'warning': return 'text-yellow-600 dark:text-yellow-400'; + case 'error': return 'text-red-600 dark:text-red-400'; + case 'checking': return 'text-blue-600 dark:text-blue-400'; + default: return 'text-gray-600 dark:text-gray-400'; + } +}; + +const getStatusBadge = (status: string) => { + switch (status) { + case 'healthy': return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'; + case 'warning': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400'; + case 'error': return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'; + case 'checking': return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400'; + default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400'; + } +}; + +const getStatusIcon = (status: string) => { + switch (status) { + case 'healthy': return '✓'; + case 'warning': return '⚠'; + case 'error': return '✗'; + case 'checking': return '⟳'; + default: return '?'; + } +}; + +export default function ApiMonitor() { + const [endpointStatuses, setEndpointStatuses] = useState>({}); + const [loading, setLoading] = useState(false); + const [autoRefresh, setAutoRefresh] = useState(true); + const [refreshInterval, setRefreshInterval] = useState(30); // seconds + + const checkEndpoint = useCallback(async (path: string, method: string) => { + const key = `${method}:${path}`; + + // Set checking status + setEndpointStatuses(prev => ({ + ...prev, + [key]: { + endpoint: path, + method, + status: 'checking', + }, + })); + + const startTime = Date.now(); + + try { + // Use fetch directly for monitoring to get response status + // Get token from auth store or localStorage + 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 fetchOptions: RequestInit = { + method, + headers, + credentials: 'include', + }; + + // For POST endpoints, send empty body for monitoring + if (method === 'POST') { + fetchOptions.body = JSON.stringify({}); + } + + const response = await fetch(`${API_BASE_URL}${path}`, fetchOptions); + const responseTime = Date.now() - startTime; + + // Check response status + // 2xx = healthy, 4xx = warning (endpoint exists but auth/validation issue), 5xx = error + let status: 'healthy' | 'warning' | 'error' = 'healthy'; + if (response.status >= 500) { + status = 'error'; + } else if (response.status >= 400 && response.status < 500) { + status = 'warning'; // 4xx is warning (auth/permission/validation - endpoint exists) + } + + setEndpointStatuses(prev => ({ + ...prev, + [key]: { + endpoint: path, + method, + status, + responseTime, + lastChecked: new Date().toISOString(), + }, + })); + } catch (err: any) { + const responseTime = Date.now() - startTime; + + // Network errors or timeouts are real errors + setEndpointStatuses(prev => ({ + ...prev, + [key]: { + endpoint: path, + method, + status: 'error', + responseTime, + lastChecked: new Date().toISOString(), + error: err instanceof Error ? err.message : 'Network error', + }, + })); + } + }, []); + + const checkAllEndpoints = useCallback(async () => { + setLoading(true); + + // Check all endpoints in parallel (but limit concurrency) + const allChecks = endpointGroups.flatMap(group => + group.endpoints.map(ep => checkEndpoint(ep.path, ep.method)) + ); + + // Check in batches of 5 to avoid overwhelming the server + const batchSize = 5; + for (let i = 0; i < allChecks.length; i += batchSize) { + const batch = allChecks.slice(i, i + batchSize); + await Promise.all(batch); + // Small delay between batches + if (i + batchSize < allChecks.length) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + + setLoading(false); + }, [checkEndpoint]); + + useEffect(() => { + // Only check when component is mounted (page is visible) + checkAllEndpoints(); + + // Set up auto-refresh if enabled + if (autoRefresh) { + const interval = setInterval(() => { + checkAllEndpoints(); + }, refreshInterval * 1000); + + return () => clearInterval(interval); + } + }, [autoRefresh, refreshInterval, checkAllEndpoints]); + + const getEndpointStatus = (path: string, method: string): EndpointStatus => { + const key = `${method}:${path}`; + return endpointStatuses[key] || { + endpoint: path, + method, + status: 'checking', + }; + }; + + const getGroupHealth = (group: EndpointGroup) => { + const statuses = group.endpoints.map(ep => getEndpointStatus(ep.path, ep.method).status); + const healthy = statuses.filter(s => s === 'healthy').length; + const total = statuses.length; + return { healthy, total }; + }; + + return ( + <> + + +
+ {/* Header Controls */} +
+
+

API Monitor

+

+ Monitor API endpoint health and response times +

+
+
+ + {autoRefresh && ( + + )} + +
+
+ + {/* Monitoring Tables - 3 per row */} +
+ {endpointGroups.map((group, groupIndex) => { + const groupHealth = getGroupHealth(group); + return ( + +
+ + + + + + + + + + {group.endpoints.map((endpoint, epIndex) => { + const status = getEndpointStatus(endpoint.path, endpoint.method); + return ( + + + + + + ); + })} + +
+ Endpoint + + Status + + Time +
+
+ + {endpoint.method} + +
+ {endpoint.path} +
+
+ {endpoint.description} +
+
+
+ + {getStatusIcon(status.status)} + {status.status} + + + {status.responseTime ? ( + + {status.responseTime}ms + + ) : ( + - + )} +
+
+
+ ); + })} +
+ + {/* Summary Stats */} + +
+ {endpointGroups.map((group, index) => { + const groupHealth = getGroupHealth(group); + const percentage = groupHealth.total > 0 + ? Math.round((groupHealth.healthy / groupHealth.total) * 100) + : 0; + + return ( +
+
+ {percentage}% +
+
+ {group.name} +
+
+ {groupHealth.healthy}/{groupHealth.total} healthy +
+
+ ); + })} +
+
+
+ + ); +} +