api monitors

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-15 11:31:49 +00:00
parent 069e0a24d8
commit 133d63813a
14 changed files with 613 additions and 0 deletions

View File

@@ -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 (
<div className={`mb-4 px-2 ${!showLabels ? 'flex justify-center' : ''}`}>
{showLabels && (
<div className="text-xs uppercase text-gray-400 mb-2 px-2">API Status</div>
)}
<div className={`flex items-center gap-2 px-2 ${!showLabels ? 'justify-center' : ''}`}>
<div
className={`w-3 h-3 rounded-full ${getStatusColor(authStatus)} transition-colors`}
title={getStatusTitle(authStatus, 'Auth')}
/>
<div
className={`w-3 h-3 rounded-full ${getStatusColor(systemStatus)} transition-colors`}
title={getStatusTitle(systemStatus, 'System')}
/>
<div
className={`w-3 h-3 rounded-full ${getStatusColor(billingStatus)} transition-colors`}
title={getStatusTitle(billingStatus, 'Billing')}
/>
</div>
{showLabels && (
<div className="flex items-center gap-2 mt-1 px-2 text-xs text-gray-500 dark:text-gray-400">
<span className="text-[10px]">A</span>
<span className="text-[10px]">S</span>
<span className="text-[10px]">B</span>
</div>
)}
</div>
);
}

View File

@@ -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<Record<string, ApiStatus>>({});
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<string, ApiStatus> = {};
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 (
<Card className="mt-6 p-4 border-2 border-dashed border-gray-300 dark:border-gray-700">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">
{title} API Status Monitor
</h3>
<Badge variant="light" color={isChecking ? 'warning' : 'success'}>
{isChecking ? 'Checking...' : 'Active'}
</Badge>
</div>
<div className="space-y-2">
{endpoints.map((endpoint) => {
const key = `${endpoint.method}:${endpoint.endpoint}`;
const status = statuses[key] || { status: 'pending' as const };
return (
<div
key={key}
className="flex items-center justify-between p-2 rounded-lg bg-gray-50 dark:bg-gray-800/50"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<Badge variant="light" color="primary" className="text-xs">
{endpoint.method}
</Badge>
<span className="text-xs font-mono text-gray-700 dark:text-gray-300 truncate">
{endpoint.endpoint}
</span>
</div>
{status.responseTime && (
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{status.responseTime}ms
</div>
)}
{status.error && (
<div className="text-xs text-red-600 dark:text-red-400 mt-1">
{status.error}
</div>
)}
</div>
<div className="ml-4">
<Badge variant="light" color={getStatusColor(status.status)}>
{status.status === 'pending' ? '⏳' : status.status === 'success' ? '✓' : '✗'}
</Badge>
</div>
</div>
);
})}
</div>
{Object.keys(statuses).length > 0 && (
<div className="mt-3 text-xs text-gray-500 dark:text-gray-400">
Last checked: {statuses[Object.keys(statuses)[0]]?.lastChecked?.toLocaleTimeString()}
</div>
)}
</Card>
);
}