From 5a08a558ef79274679f7642c227b9e5621d6bbc3 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Sat, 15 Nov 2025 14:18:08 +0000 Subject: [PATCH] Add API Status Indicator to AppSidebar and enhance ApiMonitor with localStorage support for auto-refresh and refresh interval settings --- .../components/sidebar/ApiStatusIndicator.tsx | 284 ++++++++++++++++++ frontend/src/layout/AppSidebar.tsx | 3 + frontend/src/pages/Settings/ApiMonitor.tsx | 142 +++++++-- 3 files changed, 412 insertions(+), 17 deletions(-) create mode 100644 frontend/src/components/sidebar/ApiStatusIndicator.tsx diff --git a/frontend/src/components/sidebar/ApiStatusIndicator.tsx b/frontend/src/components/sidebar/ApiStatusIndicator.tsx new file mode 100644 index 00000000..32717e20 --- /dev/null +++ b/frontend/src/components/sidebar/ApiStatusIndicator.tsx @@ -0,0 +1,284 @@ +import { useState, useEffect, useCallback, useRef } from "react"; +import { API_BASE_URL } from "../../services/api"; + +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" }, + ], + }, + { + name: "Planner Module", + abbreviation: "PL", + endpoints: [ + { path: "/v1/planner/keywords/", method: "GET" }, + { path: "/v1/planner/keywords/auto_cluster/", 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: "WR", + endpoints: [ + { path: "/v1/writer/tasks/", method: "GET" }, + { path: "/v1/writer/tasks/auto_generate_content/", 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: "System & Billing", + abbreviation: "SY", + endpoints: [ + { path: "/v1/system/prompts/", method: "GET" }, + { path: "/v1/system/settings/integrations/1/test/", method: "POST" }, + { path: "/v1/billing/credits/balance/balance/", method: "GET" }, + { path: "/v1/billing/credits/usage/", method: "GET" }, + ], + }, +]; + +export default function ApiStatusIndicator() { + const [groupStatuses, setGroupStatuses] = useState([]); + const [isChecking, setIsChecking] = useState(false); + const intervalRef = useRef | null>(null); + + 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' }; + } + fetchOptions.body = JSON.stringify(body); + } + + const response = await fetch(`${API_BASE_URL}${path}`, fetchOptions); + + 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) { + return 'error'; + } else if (response.status >= 500) { + return 'error'; + } + return 'warning'; + } else if (method === 'POST') { + if (response.status === 400) { + 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'; + } + + 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(() => { + // 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]); + + 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% + } + }; + + if (groupStatuses.length === 0 && !isChecking) { + return null; + } + + return ( +
+
+ {groupStatuses.map((group, index) => ( +
+
+ + {group.abbreviation} + +
+ ))} +
+
+ ); +} + diff --git a/frontend/src/layout/AppSidebar.tsx b/frontend/src/layout/AppSidebar.tsx index e3a1b52b..10f3bee0 100644 --- a/frontend/src/layout/AppSidebar.tsx +++ b/frontend/src/layout/AppSidebar.tsx @@ -20,6 +20,7 @@ import { useSidebar } from "../context/SidebarContext"; import SidebarWidget from "./SidebarWidget"; import { APP_VERSION } from "../config/version"; import { useAuthStore } from "../store/authStore"; +import ApiStatusIndicator from "../components/sidebar/ApiStatusIndicator"; type NavItem = { name: string; @@ -495,6 +496,8 @@ const AppSidebar: React.FC = () => { )}
+ {/* API Status Indicator - above OVERVIEW section */} +