feat(api): add unified response format utilities (Section 1, Step 1.1-1.3) #1
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import PageMeta from "../../components/common/PageMeta";
|
||||
import ComponentCard from "../../components/common/ComponentCard";
|
||||
import { fetchAPI } from "../../services/api";
|
||||
import { fetchAPI, API_BASE_URL } from "../../services/api";
|
||||
|
||||
interface SystemStatus {
|
||||
timestamp: string;
|
||||
@@ -313,6 +313,9 @@ export default function Status() {
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
{/* API Monitoring Status Card */}
|
||||
<APIMonitoringCard />
|
||||
|
||||
{/* Last Updated */}
|
||||
<div className="text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
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