From 133d63813a3f663c80f78ab90aaab7c3f9941ace Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Sat, 15 Nov 2025 11:31:49 +0000 Subject: [PATCH] api monitors --- .../debug/ApiStatusGroupMonitor.tsx | 270 ++++++++++++++++++ .../src/components/debug/ApiStatusMonitor.tsx | 189 ++++++++++++ frontend/src/layout/AppSidebar.tsx | 50 ++++ frontend/src/pages/Billing/Credits.tsx | 9 + frontend/src/pages/Billing/Transactions.tsx | 9 + frontend/src/pages/Billing/Usage.tsx | 10 + frontend/src/pages/Settings/AI.tsx | 9 + frontend/src/pages/Settings/Account.tsx | 9 + frontend/src/pages/Settings/Integration.tsx | 11 + frontend/src/pages/Settings/Sites.tsx | 11 + frontend/src/pages/Settings/Status.tsx | 9 + frontend/src/pages/Settings/System.tsx | 9 + frontend/src/pages/Thinker/AuthorProfiles.tsx | 9 + frontend/src/pages/Thinker/Prompts.tsx | 9 + 14 files changed, 613 insertions(+) create mode 100644 frontend/src/components/debug/ApiStatusGroupMonitor.tsx create mode 100644 frontend/src/components/debug/ApiStatusMonitor.tsx diff --git a/frontend/src/components/debug/ApiStatusGroupMonitor.tsx b/frontend/src/components/debug/ApiStatusGroupMonitor.tsx new file mode 100644 index 00000000..bd59e826 --- /dev/null +++ b/frontend/src/components/debug/ApiStatusGroupMonitor.tsx @@ -0,0 +1,270 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useResourceDebug } from '../../hooks/useResourceDebug'; + +export interface ApiEndpoint { + name: string; + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + endpoint: string; + testData?: any; +} + +interface ApiStatus { + endpoint: string; + status: 'pending' | 'success' | 'error'; + responseTime?: number; + error?: string; + lastChecked?: Date; +} + +interface ApiStatusGroupMonitorProps { + groupName: string; + endpoints: ApiEndpoint[]; + baseUrl?: string; + onStatusChange?: (status: 'pending' | 'success' | 'error') => void; +} + +export function ApiStatusGroupMonitor({ + groupName, + endpoints, + baseUrl, + onStatusChange +}: ApiStatusGroupMonitorProps) { + const debugEnabled = useResourceDebug(); + const [overallStatus, setOverallStatus] = useState<'pending' | 'success' | 'error'>('pending'); + + const checkAllApis = useCallback(async () => { + if (!debugEnabled) { + setOverallStatus('pending'); + if (onStatusChange) { + onStatusChange('pending'); + } + return; + } + + // Set pending while checking + setOverallStatus('pending'); + if (onStatusChange) { + onStatusChange('pending'); + } + + try { + const API_BASE_URL = baseUrl || (import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api'); + const statuses: ApiStatus[] = []; + + // Check all endpoints in parallel with timeout + const checkPromises = endpoints.map(async (endpoint) => { + try { + const startTime = Date.now(); + + // Get auth token + const authStorage = localStorage.getItem('auth-storage'); + const token = authStorage ? JSON.parse(authStorage)?.state?.token : null; + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + // Create abort controller for timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout + + const options: RequestInit = { + method: endpoint.method, + headers, + credentials: 'include', + signal: controller.signal, + }; + + // Only add body for methods that support it + if (['POST', 'PUT', 'PATCH'].includes(endpoint.method) && endpoint.testData) { + options.body = JSON.stringify(endpoint.testData); + } + + try { + const response = await fetch(`${API_BASE_URL}${endpoint.endpoint}`, options); + clearTimeout(timeoutId); + + return { + endpoint: endpoint.endpoint, + status: response.ok ? 'success' : 'error', + responseTime: Date.now() - startTime, + error: response.ok ? undefined : `HTTP ${response.status}`, + lastChecked: new Date(), + } as ApiStatus; + } catch (fetchError: any) { + clearTimeout(timeoutId); + // Handle timeout and other errors gracefully + return { + endpoint: endpoint.endpoint, + status: 'error' as const, + error: fetchError.name === 'AbortError' ? 'Timeout' : (fetchError.message || 'Network error'), + lastChecked: new Date(), + } as ApiStatus; + } + } catch (error: any) { + // Fallback error handling + return { + endpoint: endpoint.endpoint, + status: 'error' as const, + error: error.message || 'Check failed', + lastChecked: new Date(), + } as ApiStatus; + } + }); + + const results = await Promise.allSettled(checkPromises); + results.forEach((result) => { + if (result.status === 'fulfilled') { + statuses.push(result.value); + } else { + statuses.push({ + endpoint: 'unknown', + status: 'error' as const, + error: result.reason?.message || 'Check failed', + lastChecked: new Date(), + }); + } + }); + + // Determine overall status + const hasError = statuses.some(s => s.status === 'error'); + const allSuccess = statuses.length > 0 && statuses.every(s => s.status === 'success'); + + let newStatus: 'pending' | 'success' | 'error'; + if (hasError) { + newStatus = 'error'; + } else if (allSuccess) { + newStatus = 'success'; + } else { + newStatus = 'pending'; + } + + setOverallStatus(newStatus); + if (onStatusChange) { + onStatusChange(newStatus); + } + } catch (error) { + // If something goes wrong, set to error but don't block + console.error('API status check error:', error); + setOverallStatus('error'); + if (onStatusChange) { + onStatusChange('error'); + } + } + }, [debugEnabled, endpoints, baseUrl, onStatusChange]); + + useEffect(() => { + if (!debugEnabled) { + setOverallStatus('pending'); + if (onStatusChange) { + onStatusChange('pending'); + } + return; + } + + // Initial check with delay to avoid blocking page load + const initialTimeout = setTimeout(() => { + checkAllApis(); + }, 1000); + + // Set up interval to check every 1 minute (60 seconds) + const interval = setInterval(() => { + checkAllApis(); + }, 60000); + + return () => { + clearTimeout(initialTimeout); + clearInterval(interval); + }; + }, [debugEnabled, checkAllApis, onStatusChange]); + + // Don't render anything - this component just tracks status + return null; +} + +// Sidebar widget component +interface ApiStatusSidebarWidgetProps { + authStatus: 'pending' | 'success' | 'error'; + systemStatus: 'pending' | 'success' | 'error'; + billingStatus: 'pending' | 'success' | 'error'; + isExpanded?: boolean; + isHovered?: boolean; + isMobileOpen?: boolean; +} + +export function ApiStatusSidebarWidget({ + authStatus, + systemStatus, + billingStatus, + isExpanded = true, + isHovered = false, + isMobileOpen = false +}: ApiStatusSidebarWidgetProps) { + const debugEnabled = useResourceDebug(); + + if (!debugEnabled) { + return null; + } + + const showLabels = isExpanded || isHovered || isMobileOpen; + + const getStatusColor = (status: string) => { + switch (status) { + case 'success': + return 'bg-green-500 dark:bg-green-400'; + case 'error': + return 'bg-red-500 dark:bg-red-400'; + case 'pending': + return 'bg-yellow-500 dark:bg-yellow-400'; + default: + return 'bg-gray-400 dark:bg-gray-500'; + } + }; + + const getStatusTitle = (status: string, group: string) => { + switch (status) { + case 'success': + return `${group} APIs: All healthy`; + case 'error': + return `${group} APIs: Some errors detected`; + case 'pending': + return `${group} APIs: Checking...`; + default: + return `${group} APIs: Unknown status`; + } + }; + + return ( +
+ {showLabels && ( +
API Status
+ )} +
+
+
+
+
+ {showLabels && ( +
+ A + S + B +
+ )} +
+ ); +} + diff --git a/frontend/src/components/debug/ApiStatusMonitor.tsx b/frontend/src/components/debug/ApiStatusMonitor.tsx new file mode 100644 index 00000000..f7fdfe2b --- /dev/null +++ b/frontend/src/components/debug/ApiStatusMonitor.tsx @@ -0,0 +1,189 @@ +import { useState, useEffect } from 'react'; +import { useResourceDebug } from '../../hooks/useResourceDebug'; +import { Card } from '../ui/card'; +import Badge from '../ui/badge/Badge'; + +export interface ApiEndpoint { + name: string; + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + endpoint: string; + testData?: any; +} + +interface ApiStatusMonitorProps { + title: string; + endpoints: ApiEndpoint[]; + baseUrl?: string; +} + +interface ApiStatus { + endpoint: string; + status: 'pending' | 'success' | 'error'; + responseTime?: number; + error?: string; + lastChecked?: Date; +} + +export default function ApiStatusMonitor({ title, endpoints, baseUrl }: ApiStatusMonitorProps) { + const debugEnabled = useResourceDebug(); + const [statuses, setStatuses] = useState>({}); + const [isChecking, setIsChecking] = useState(false); + + // Only make requests if debug is enabled + useEffect(() => { + if (!debugEnabled) { + // Clear statuses when debug is disabled + setStatuses({}); + return; + } + + // Initial check + checkAllApis(); + + // Set up interval to check every 30 seconds + const interval = setInterval(() => { + checkAllApis(); + }, 30000); + + return () => clearInterval(interval); + }, [debugEnabled, endpoints]); + + const checkAllApis = async () => { + if (!debugEnabled) return; + + setIsChecking(true); + const API_BASE_URL = baseUrl || (import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api'); + + const newStatuses: Record = {}; + + for (const endpoint of endpoints) { + const key = `${endpoint.method}:${endpoint.endpoint}`; + + try { + const startTime = Date.now(); + + // Get auth token + const authStorage = localStorage.getItem('auth-storage'); + const token = authStorage ? JSON.parse(authStorage)?.state?.token : null; + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + const options: RequestInit = { + method: endpoint.method, + headers, + credentials: 'include', + }; + + // Only add body for methods that support it + if (['POST', 'PUT', 'PATCH'].includes(endpoint.method) && endpoint.testData) { + options.body = JSON.stringify(endpoint.testData); + } + + const response = await fetch(`${API_BASE_URL}${endpoint.endpoint}`, options); + const responseTime = Date.now() - startTime; + + newStatuses[key] = { + endpoint: endpoint.endpoint, + status: response.ok ? 'success' : 'error', + responseTime, + error: response.ok ? undefined : `HTTP ${response.status}`, + lastChecked: new Date(), + }; + } catch (error: any) { + newStatuses[key] = { + endpoint: endpoint.endpoint, + status: 'error', + error: error.message || 'Network error', + lastChecked: new Date(), + }; + } + } + + setStatuses(newStatuses); + setIsChecking(false); + }; + + // Don't render anything if debug is disabled + if (!debugEnabled) { + return null; + } + + const getStatusColor = (status: string) => { + switch (status) { + case 'success': + return 'success'; + case 'error': + return 'error'; + case 'pending': + return 'warning'; + default: + return 'primary'; + } + }; + + return ( + +
+

+ {title} API Status Monitor +

+ + {isChecking ? 'Checking...' : 'Active'} + +
+ +
+ {endpoints.map((endpoint) => { + const key = `${endpoint.method}:${endpoint.endpoint}`; + const status = statuses[key] || { status: 'pending' as const }; + + return ( +
+
+
+ + {endpoint.method} + + + {endpoint.endpoint} + +
+ {status.responseTime && ( +
+ {status.responseTime}ms +
+ )} + {status.error && ( +
+ {status.error} +
+ )} +
+
+ + {status.status === 'pending' ? '⏳' : status.status === 'success' ? '✓' : '✗'} + +
+
+ ); + })} +
+ + {Object.keys(statuses).length > 0 && ( +
+ Last checked: {statuses[Object.keys(statuses)[0]]?.lastChecked?.toLocaleTimeString()} +
+ )} +
+ ); +} + diff --git a/frontend/src/layout/AppSidebar.tsx b/frontend/src/layout/AppSidebar.tsx index 709d06d4..14cb8ffb 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 { ApiStatusSidebarWidget, ApiStatusGroupMonitor } from "../components/debug/ApiStatusGroupMonitor"; type NavItem = { name: string; @@ -52,6 +53,11 @@ const AppSidebar: React.FC = () => { {} ); const subMenuRefs = useRef>({}); + + // API Status states for groups + const [authStatus, setAuthStatus] = useState<'pending' | 'success' | 'error'>('pending'); + const [systemStatus, setSystemStatus] = useState<'pending' | 'success' | 'error'>('pending'); + const [billingStatus, setBillingStatus] = useState<'pending' | 'success' | 'error'>('pending'); const isActive = useCallback( (path: string) => location.pathname === path, @@ -494,8 +500,52 @@ const AppSidebar: React.FC = () => { )}
+ {/* API Status Monitors - Hidden components that track status */} + + + +
)} + + {/* API Status Monitor - Only shows when debug toggle is enabled */} +
); } diff --git a/frontend/src/pages/Billing/Usage.tsx b/frontend/src/pages/Billing/Usage.tsx index 64213568..2132d934 100644 --- a/frontend/src/pages/Billing/Usage.tsx +++ b/frontend/src/pages/Billing/Usage.tsx @@ -4,6 +4,7 @@ import { useToast } from '../../components/ui/toast/ToastContainer'; import { fetchCreditUsage, CreditUsageLog, fetchUsageLimits, LimitCard } from '../../services/api'; import { Card } from '../../components/ui/card'; import Badge from '../../components/ui/badge/Badge'; +import ApiStatusMonitor from '../../components/debug/ApiStatusMonitor'; export default function Usage() { const toast = useToast(); @@ -211,6 +212,15 @@ export default function Usage() { )}
+ + {/* API Status Monitor - Only shows when debug toggle is enabled */} + ); } diff --git a/frontend/src/pages/Settings/AI.tsx b/frontend/src/pages/Settings/AI.tsx index 1cffd29c..c75ac984 100644 --- a/frontend/src/pages/Settings/AI.tsx +++ b/frontend/src/pages/Settings/AI.tsx @@ -3,6 +3,7 @@ import PageMeta from '../../components/common/PageMeta'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { fetchAPI } from '../../services/api'; import { Card } from '../../components/ui/card'; +import ApiStatusMonitor from '../../components/debug/ApiStatusMonitor'; export default function AISettings() { const toast = useToast(); @@ -42,6 +43,14 @@ export default function AISettings() {

AI settings management interface coming soon.

)} + + {/* API Status Monitor - Only shows when debug toggle is enabled */} + ); } diff --git a/frontend/src/pages/Settings/Account.tsx b/frontend/src/pages/Settings/Account.tsx index 6f3eaedf..e7c7fa51 100644 --- a/frontend/src/pages/Settings/Account.tsx +++ b/frontend/src/pages/Settings/Account.tsx @@ -3,6 +3,7 @@ import PageMeta from '../../components/common/PageMeta'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { fetchAPI } from '../../services/api'; import { Card } from '../../components/ui/card'; +import ApiStatusMonitor from '../../components/debug/ApiStatusMonitor'; export default function AccountSettings() { const toast = useToast(); @@ -42,6 +43,14 @@ export default function AccountSettings() {

Account settings management interface coming soon.

)} + + {/* API Status Monitor - Only shows when debug toggle is enabled */} + ); } diff --git a/frontend/src/pages/Settings/Integration.tsx b/frontend/src/pages/Settings/Integration.tsx index 3a151cef..caba5400 100644 --- a/frontend/src/pages/Settings/Integration.tsx +++ b/frontend/src/pages/Settings/Integration.tsx @@ -14,6 +14,7 @@ import SelectDropdown from '../../components/form/SelectDropdown'; import { useToast } from '../../components/ui/toast/ToastContainer'; import Alert from '../../components/ui/alert/Alert'; import { fetchAPI } from '../../services/api'; +import ApiStatusMonitor from '../../components/debug/ApiStatusMonitor'; // OpenAI Icon SVG const OpenAIIcon = () => ( @@ -1165,6 +1166,16 @@ export default function Integration() { /> )} + + {/* API Status Monitor - Only shows when debug toggle is enabled */} + ); } diff --git a/frontend/src/pages/Settings/Sites.tsx b/frontend/src/pages/Settings/Sites.tsx index 0c8efd7f..42aae5e7 100644 --- a/frontend/src/pages/Settings/Sites.tsx +++ b/frontend/src/pages/Settings/Sites.tsx @@ -19,6 +19,7 @@ import { Sector, } from '../../services/api'; import Badge from '../../components/ui/badge/Badge'; +import ApiStatusMonitor from '../../components/debug/ApiStatusMonitor'; // Site Icon SVG const SiteIcon = () => ( @@ -591,6 +592,16 @@ export default function Sites() { /> )} + + {/* API Status Monitor - Only shows when debug toggle is enabled */} + ); } diff --git a/frontend/src/pages/Settings/Status.tsx b/frontend/src/pages/Settings/Status.tsx index db5e511a..62d06abf 100644 --- a/frontend/src/pages/Settings/Status.tsx +++ b/frontend/src/pages/Settings/Status.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import PageMeta from "../../components/common/PageMeta"; import ComponentCard from "../../components/common/ComponentCard"; import { fetchAPI } from "../../services/api"; +import ApiStatusMonitor from "../../components/debug/ApiStatusMonitor"; interface SystemStatus { timestamp: string; @@ -318,6 +319,14 @@ export default function Status() { Last updated: {new Date(status.timestamp).toLocaleString()} + + {/* API Status Monitor - Only shows when debug toggle is enabled */} + ); } diff --git a/frontend/src/pages/Settings/System.tsx b/frontend/src/pages/Settings/System.tsx index 1254a55f..3a9992fa 100644 --- a/frontend/src/pages/Settings/System.tsx +++ b/frontend/src/pages/Settings/System.tsx @@ -3,6 +3,7 @@ import PageMeta from '../../components/common/PageMeta'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { fetchAPI } from '../../services/api'; import { Card } from '../../components/ui/card'; +import ApiStatusMonitor from '../../components/debug/ApiStatusMonitor'; export default function SystemSettings() { const toast = useToast(); @@ -42,6 +43,14 @@ export default function SystemSettings() {

System settings management interface coming soon.

)} + + {/* API Status Monitor - Only shows when debug toggle is enabled */} + ); } diff --git a/frontend/src/pages/Thinker/AuthorProfiles.tsx b/frontend/src/pages/Thinker/AuthorProfiles.tsx index db943097..0ec4a76f 100644 --- a/frontend/src/pages/Thinker/AuthorProfiles.tsx +++ b/frontend/src/pages/Thinker/AuthorProfiles.tsx @@ -7,6 +7,7 @@ import Button from '../../components/ui/button/Button'; import FormModal, { FormField } from '../../components/common/FormModal'; import Badge from '../../components/ui/badge/Badge'; import { PlusIcon } from '../../icons'; +import ApiStatusMonitor from '../../components/debug/ApiStatusMonitor'; export default function AuthorProfiles() { const toast = useToast(); @@ -158,6 +159,14 @@ export default function AuthorProfiles() { data={formData} onChange={setFormData} /> + + {/* API Status Monitor - Only shows when debug toggle is enabled */} + ); } diff --git a/frontend/src/pages/Thinker/Prompts.tsx b/frontend/src/pages/Thinker/Prompts.tsx index a3f8555a..7a1f8ec1 100644 --- a/frontend/src/pages/Thinker/Prompts.tsx +++ b/frontend/src/pages/Thinker/Prompts.tsx @@ -5,6 +5,7 @@ import TextArea from '../../components/form/input/TextArea'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { BoltIcon } from '../../icons'; import { fetchAPI } from '../../services/api'; +import ApiStatusMonitor from '../../components/debug/ApiStatusMonitor'; interface PromptData { prompt_type: string; @@ -427,6 +428,14 @@ export default function Prompts() { + + {/* API Status Monitor - Only shows when debug toggle is enabled */} + ); }