feat(api): add unified response format utilities (Section 1, Step 1.1-1.3) #1

Closed
salman wants to merge 11 commits from feature/api-unified-response-format into main
Showing only changes of commit a533d05e51 - Show all commits

View File

@@ -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,46 +313,8 @@ export default function Status() {
</div>
</ComponentCard>
{/* API Status Card */}
<ComponentCard title="API Status" desc="API endpoint availability and health">
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">API Server</span>
<span className={`text-xs px-2 py-1 rounded ${getStatusBadge(status.database?.status || 'unknown')}`}>
{status.database?.connected ? 'Operational' : 'Offline'}
</span>
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
Base URL: {typeof window !== 'undefined' ? window.location.origin.replace('app.', 'api.') : 'api.igny8.com'}
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">Response Format</span>
<span className="text-xs px-2 py-1 rounded bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
Unified
</span>
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
All endpoints use standardized response format
</div>
</div>
</div>
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
<div> Health Check: <span className="text-green-600 dark:text-green-400">/api/ping/</span></div>
<div> System Status: <span className="text-green-600 dark:text-green-400">/v1/system/status/</span></div>
<div> Authentication: <span className="text-green-600 dark:text-green-400">/v1/auth/</span></div>
<div> Planner API: <span className="text-green-600 dark:text-green-400">/v1/planner/</span></div>
<div> Writer API: <span className="text-green-600 dark:text-green-400">/v1/writer/</span></div>
</div>
</div>
</div>
</ComponentCard>
{/* API Monitoring Status Card */}
<APIMonitoringCard />
{/* Last Updated */}
<div className="text-center text-sm text-gray-500 dark:text-gray-400">
@@ -362,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>
);
}