Update Status.tsx
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import PageMeta from "../../components/common/PageMeta";
|
import PageMeta from "../../components/common/PageMeta";
|
||||||
import ComponentCard from "../../components/common/ComponentCard";
|
import ComponentCard from "../../components/common/ComponentCard";
|
||||||
import { fetchAPI } from "../../services/api";
|
import { fetchAPI, API_BASE_URL } from "../../services/api";
|
||||||
|
|
||||||
interface SystemStatus {
|
interface SystemStatus {
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
@@ -313,6 +313,9 @@ export default function Status() {
|
|||||||
</div>
|
</div>
|
||||||
</ComponentCard>
|
</ComponentCard>
|
||||||
|
|
||||||
|
{/* API Monitoring Status Card */}
|
||||||
|
<APIMonitoringCard />
|
||||||
|
|
||||||
{/* Last Updated */}
|
{/* Last Updated */}
|
||||||
<div className="text-center text-sm text-gray-500 dark:text-gray-400">
|
<div className="text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
Last updated: {new Date(status.timestamp).toLocaleString()}
|
Last updated: {new Date(status.timestamp).toLocaleString()}
|
||||||
@@ -321,3 +324,229 @@ export default function Status() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API Monitoring Component
|
||||||
|
interface APIEndpointStatus {
|
||||||
|
name: string;
|
||||||
|
endpoint: string;
|
||||||
|
status: 'healthy' | 'warning' | 'critical' | 'checking';
|
||||||
|
responseTime: number | null;
|
||||||
|
statusCode: number | null;
|
||||||
|
lastChecked: Date | null;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function APIMonitoringCard() {
|
||||||
|
const initialEndpoints: APIEndpointStatus[] = [
|
||||||
|
{ name: 'Health Check', endpoint: '/api/ping/', status: 'checking', responseTime: null, statusCode: null, lastChecked: null, error: null },
|
||||||
|
{ name: 'System Status', endpoint: '/v1/system/status/', status: 'checking', responseTime: null, statusCode: null, lastChecked: null, error: null },
|
||||||
|
{ name: 'Auth Endpoint', endpoint: '/v1/auth/me/', status: 'checking', responseTime: null, statusCode: null, lastChecked: null, error: null },
|
||||||
|
{ name: 'Planner API', endpoint: '/v1/planner/keywords/?page=1&page_size=1', status: 'checking', responseTime: null, statusCode: null, lastChecked: null, error: null },
|
||||||
|
{ name: 'Writer API', endpoint: '/v1/writer/tasks/?page=1&page_size=1', status: 'checking', responseTime: null, statusCode: null, lastChecked: null, error: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
const [endpoints, setEndpoints] = useState<APIEndpointStatus[]>(initialEndpoints);
|
||||||
|
const endpointsRef = useRef(initialEndpoints);
|
||||||
|
|
||||||
|
// Keep ref in sync with state
|
||||||
|
useEffect(() => {
|
||||||
|
endpointsRef.current = endpoints;
|
||||||
|
}, [endpoints]);
|
||||||
|
|
||||||
|
const checkEndpoint = async (endpoint: APIEndpointStatus) => {
|
||||||
|
const startTime = performance.now();
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}${endpoint.endpoint}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
const endTime = performance.now();
|
||||||
|
const responseTime = Math.round(endTime - startTime);
|
||||||
|
|
||||||
|
// Determine status based on response time and status code
|
||||||
|
let status: 'healthy' | 'warning' | 'critical' = 'healthy';
|
||||||
|
if (response.status >= 500) {
|
||||||
|
status = 'critical';
|
||||||
|
} else if (response.status >= 400 || responseTime > 2000) {
|
||||||
|
status = 'warning';
|
||||||
|
} else if (responseTime > 1000) {
|
||||||
|
status = 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...endpoint,
|
||||||
|
status,
|
||||||
|
responseTime,
|
||||||
|
statusCode: response.status,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
const endTime = performance.now();
|
||||||
|
const responseTime = Math.round(endTime - startTime);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...endpoint,
|
||||||
|
status: 'critical' as const,
|
||||||
|
responseTime,
|
||||||
|
statusCode: null,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
error: err.name === 'AbortError' ? 'Timeout' : err.message || 'Network Error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAllEndpoints = async () => {
|
||||||
|
// Check all endpoints in parallel using ref to get latest state
|
||||||
|
const results = await Promise.all(
|
||||||
|
endpointsRef.current.map(endpoint => checkEndpoint(endpoint))
|
||||||
|
);
|
||||||
|
setEndpoints(results);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
checkAllEndpoints();
|
||||||
|
|
||||||
|
// Check every 5 seconds for real-time monitoring
|
||||||
|
const interval = setInterval(checkAllEndpoints, 5000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'healthy':
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></div>
|
||||||
|
<span className="text-green-600 dark:text-green-400 text-xs font-medium">Online</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'warning':
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-2 h-2 bg-yellow-500 rounded-full mr-2 animate-pulse"></div>
|
||||||
|
<span className="text-yellow-600 dark:text-yellow-400 text-xs font-medium">Slow</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'critical':
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-2 h-2 bg-red-500 rounded-full mr-2"></div>
|
||||||
|
<span className="text-red-600 dark:text-red-400 text-xs font-medium">Down</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-2 h-2 bg-gray-400 rounded-full mr-2 animate-pulse"></div>
|
||||||
|
<span className="text-gray-600 dark:text-gray-400 text-xs font-medium">Checking...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getResponseTimeColor = (responseTime: number | null) => {
|
||||||
|
if (responseTime === null) return 'text-gray-500 dark:text-gray-400';
|
||||||
|
if (responseTime < 500) return 'text-green-600 dark:text-green-400';
|
||||||
|
if (responseTime < 1000) return 'text-yellow-600 dark:text-yellow-400';
|
||||||
|
if (responseTime < 2000) return 'text-orange-600 dark:text-orange-400';
|
||||||
|
return 'text-red-600 dark:text-red-400';
|
||||||
|
};
|
||||||
|
|
||||||
|
const overallStatus = endpoints.every(e => e.status === 'healthy')
|
||||||
|
? 'healthy'
|
||||||
|
: endpoints.some(e => e.status === 'critical')
|
||||||
|
? 'critical'
|
||||||
|
: 'warning';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ComponentCard
|
||||||
|
title="API Monitoring"
|
||||||
|
desc="Real-time API endpoint health and response time monitoring"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Overall Status */}
|
||||||
|
<div className="flex items-center justify-between pb-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-gray-800 dark:text-gray-200">Overall API Status</h4>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
{endpoints.filter(e => e.status === 'healthy').length} of {endpoints.length} endpoints healthy
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={`text-lg font-bold ${getStatusColor(overallStatus)}`}>
|
||||||
|
{overallStatus === 'healthy' ? '✓ All Systems Operational' :
|
||||||
|
overallStatus === 'warning' ? '⚠ Some Issues Detected' :
|
||||||
|
'✗ Critical Issues'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Endpoints List */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{endpoints.map((endpoint, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-sm font-medium text-gray-800 dark:text-gray-200 truncate">
|
||||||
|
{endpoint.name}
|
||||||
|
</span>
|
||||||
|
{getStatusIcon(endpoint.status)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<span className="truncate font-mono">{endpoint.endpoint}</span>
|
||||||
|
{endpoint.responseTime !== null && (
|
||||||
|
<span className={`font-semibold ${getResponseTimeColor(endpoint.responseTime)}`}>
|
||||||
|
{endpoint.responseTime}ms
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{endpoint.statusCode && (
|
||||||
|
<span className={`font-semibold ${
|
||||||
|
endpoint.statusCode >= 200 && endpoint.statusCode < 300
|
||||||
|
? 'text-green-600 dark:text-green-400'
|
||||||
|
: endpoint.statusCode >= 400 && endpoint.statusCode < 500
|
||||||
|
? 'text-yellow-600 dark:text-yellow-400'
|
||||||
|
: 'text-red-600 dark:text-red-400'
|
||||||
|
}`}>
|
||||||
|
{endpoint.statusCode}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{endpoint.error && (
|
||||||
|
<div className="text-xs text-red-600 dark:text-red-400 mt-1">
|
||||||
|
{endpoint.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{endpoint.lastChecked && (
|
||||||
|
<div className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||||
|
Last checked: {endpoint.lastChecked.toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Refresh Indicator */}
|
||||||
|
<div className="text-center text-xs text-gray-400 dark:text-gray-500 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<span className="inline-flex items-center">
|
||||||
|
<span className="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></span>
|
||||||
|
Auto-refreshing every 5 seconds
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ComponentCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user