Add API Monitor route and sidebar entry

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-15 13:44:39 +00:00
parent ffd865e755
commit 6109369df4
3 changed files with 409 additions and 0 deletions

View File

@@ -63,6 +63,7 @@ const AISettings = lazy(() => import("./pages/Settings/AI"));
const Plans = lazy(() => import("./pages/Settings/Plans")); const Plans = lazy(() => import("./pages/Settings/Plans"));
const Industries = lazy(() => import("./pages/Settings/Industries")); const Industries = lazy(() => import("./pages/Settings/Industries"));
const Status = lazy(() => import("./pages/Settings/Status")); const Status = lazy(() => import("./pages/Settings/Status"));
const ApiMonitor = lazy(() => import("./pages/Settings/ApiMonitor"));
const Integration = lazy(() => import("./pages/Settings/Integration")); const Integration = lazy(() => import("./pages/Settings/Integration"));
const Sites = lazy(() => import("./pages/Settings/Sites")); const Sites = lazy(() => import("./pages/Settings/Sites"));
const ImportExport = lazy(() => import("./pages/Settings/ImportExport")); const ImportExport = lazy(() => import("./pages/Settings/ImportExport"));
@@ -315,6 +316,11 @@ export default function App() {
<Status /> <Status />
</Suspense> </Suspense>
} /> } />
<Route path="/settings/api-monitor" element={
<Suspense fallback={null}>
<ApiMonitor />
</Suspense>
} />
<Route path="/settings/integration" element={ <Route path="/settings/integration" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<Integration /> <Integration />

View File

@@ -199,6 +199,7 @@ const AppSidebar: React.FC = () => {
name: "System Health", name: "System Health",
subItems: [ subItems: [
{ name: "Status", path: "/settings/status" }, { name: "Status", path: "/settings/status" },
{ name: "API Monitor", path: "/settings/api-monitor" },
], ],
}, },
{ {

View File

@@ -0,0 +1,402 @@
import { useState, useEffect, useCallback } from "react";
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
import { API_BASE_URL } from "../../services/api";
interface EndpointStatus {
endpoint: string;
method: string;
status: 'healthy' | 'warning' | 'error' | 'checking';
responseTime?: number;
lastChecked?: string;
error?: string;
}
interface EndpointGroup {
name: string;
endpoints: {
path: string;
method: string;
description: string;
}[];
}
const endpointGroups: EndpointGroup[] = [
{
name: "Core Health & Auth",
endpoints: [
{ path: "/api/ping/", method: "GET", description: "Health check" },
{ path: "/v1/system/status/", method: "GET", description: "System status" },
{ path: "/v1/auth/login/", method: "POST", description: "Login" },
{ path: "/v1/auth/me/", method: "GET", description: "Current user" },
{ path: "/v1/auth/register/", method: "POST", description: "Registration" },
],
},
{
name: "Auth & User Management",
endpoints: [
{ path: "/v1/auth/users/", method: "GET", description: "List users" },
{ path: "/v1/auth/accounts/", method: "GET", description: "List accounts" },
{ path: "/v1/auth/sites/", method: "GET", description: "List sites" },
{ path: "/v1/auth/sectors/", method: "GET", description: "List sectors" },
{ path: "/v1/auth/plans/", method: "GET", description: "List plans" },
{ path: "/v1/auth/industries/", method: "GET", description: "List industries" },
],
},
{
name: "Planner Module",
endpoints: [
{ path: "/v1/planner/keywords/", method: "GET", description: "List keywords" },
{ path: "/v1/planner/keywords/auto_cluster/", method: "POST", description: "AI clustering" },
{ path: "/v1/planner/clusters/", method: "GET", description: "List clusters" },
{ path: "/v1/planner/clusters/auto_generate_ideas/", method: "POST", description: "AI ideas" },
{ path: "/v1/planner/ideas/", method: "GET", description: "List ideas" },
],
},
{
name: "Writer Module",
endpoints: [
{ path: "/v1/writer/tasks/", method: "GET", description: "List tasks" },
{ path: "/v1/writer/tasks/auto_generate_content/", method: "POST", description: "AI content" },
{ path: "/v1/writer/content/", method: "GET", description: "List content" },
{ path: "/v1/writer/content/generate_image_prompts/", method: "POST", description: "Image prompts" },
{ path: "/v1/writer/images/", method: "GET", description: "List images" },
{ path: "/v1/writer/images/generate_images/", method: "POST", description: "AI images" },
],
},
{
name: "System & Billing",
endpoints: [
{ path: "/v1/system/prompts/", method: "GET", description: "List prompts" },
{ path: "/v1/system/settings/integrations/1/test/", method: "POST", description: "Test integration" },
{ path: "/v1/billing/credits/balance/balance/", method: "GET", description: "Credit balance" },
{ path: "/v1/billing/credits/usage/", method: "GET", description: "Usage logs" },
],
},
];
const getStatusColor = (status: string) => {
switch (status) {
case 'healthy': return 'text-green-600 dark:text-green-400';
case 'warning': return 'text-yellow-600 dark:text-yellow-400';
case 'error': return 'text-red-600 dark:text-red-400';
case 'checking': return 'text-blue-600 dark:text-blue-400';
default: return 'text-gray-600 dark:text-gray-400';
}
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'healthy': return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400';
case 'warning': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400';
case 'error': return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400';
case 'checking': return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400';
default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'healthy': return '✓';
case 'warning': return '⚠';
case 'error': return '✗';
case 'checking': return '⟳';
default: return '?';
}
};
export default function ApiMonitor() {
const [endpointStatuses, setEndpointStatuses] = useState<Record<string, EndpointStatus>>({});
const [loading, setLoading] = useState(false);
const [autoRefresh, setAutoRefresh] = useState(true);
const [refreshInterval, setRefreshInterval] = useState(30); // seconds
const checkEndpoint = useCallback(async (path: string, method: string) => {
const key = `${method}:${path}`;
// Set checking status
setEndpointStatuses(prev => ({
...prev,
[key]: {
endpoint: path,
method,
status: 'checking',
},
}));
const startTime = Date.now();
try {
// Use fetch directly for monitoring to get response status
// Get token from auth store or localStorage
const token = localStorage.getItem('auth_token') ||
(() => {
try {
const authStorage = localStorage.getItem('auth-storage');
if (authStorage) {
const parsed = JSON.parse(authStorage);
return parsed?.state?.token || '';
}
} catch (e) {
// Ignore parsing errors
}
return '';
})();
const headers: HeadersInit = {
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const fetchOptions: RequestInit = {
method,
headers,
credentials: 'include',
};
// For POST endpoints, send empty body for monitoring
if (method === 'POST') {
fetchOptions.body = JSON.stringify({});
}
const response = await fetch(`${API_BASE_URL}${path}`, fetchOptions);
const responseTime = Date.now() - startTime;
// Check response status
// 2xx = healthy, 4xx = warning (endpoint exists but auth/validation issue), 5xx = error
let status: 'healthy' | 'warning' | 'error' = 'healthy';
if (response.status >= 500) {
status = 'error';
} else if (response.status >= 400 && response.status < 500) {
status = 'warning'; // 4xx is warning (auth/permission/validation - endpoint exists)
}
setEndpointStatuses(prev => ({
...prev,
[key]: {
endpoint: path,
method,
status,
responseTime,
lastChecked: new Date().toISOString(),
},
}));
} catch (err: any) {
const responseTime = Date.now() - startTime;
// Network errors or timeouts are real errors
setEndpointStatuses(prev => ({
...prev,
[key]: {
endpoint: path,
method,
status: 'error',
responseTime,
lastChecked: new Date().toISOString(),
error: err instanceof Error ? err.message : 'Network error',
},
}));
}
}, []);
const checkAllEndpoints = useCallback(async () => {
setLoading(true);
// Check all endpoints in parallel (but limit concurrency)
const allChecks = endpointGroups.flatMap(group =>
group.endpoints.map(ep => checkEndpoint(ep.path, ep.method))
);
// Check in batches of 5 to avoid overwhelming the server
const batchSize = 5;
for (let i = 0; i < allChecks.length; i += batchSize) {
const batch = allChecks.slice(i, i + batchSize);
await Promise.all(batch);
// Small delay between batches
if (i + batchSize < allChecks.length) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
setLoading(false);
}, [checkEndpoint]);
useEffect(() => {
// Only check when component is mounted (page is visible)
checkAllEndpoints();
// Set up auto-refresh if enabled
if (autoRefresh) {
const interval = setInterval(() => {
checkAllEndpoints();
}, refreshInterval * 1000);
return () => clearInterval(interval);
}
}, [autoRefresh, refreshInterval, checkAllEndpoints]);
const getEndpointStatus = (path: string, method: string): EndpointStatus => {
const key = `${method}:${path}`;
return endpointStatuses[key] || {
endpoint: path,
method,
status: 'checking',
};
};
const getGroupHealth = (group: EndpointGroup) => {
const statuses = group.endpoints.map(ep => getEndpointStatus(ep.path, ep.method).status);
const healthy = statuses.filter(s => s === 'healthy').length;
const total = statuses.length;
return { healthy, total };
};
return (
<>
<PageMeta title="API Monitor - IGNY8" description="API endpoint monitoring" />
<div className="space-y-6">
{/* Header Controls */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-semibold text-gray-800 dark:text-white/90">API Monitor</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Monitor API endpoint health and response times
</p>
</div>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<input
type="checkbox"
checked={autoRefresh}
onChange={(e) => setAutoRefresh(e.target.checked)}
className="rounded border-gray-300"
/>
Auto-refresh
</label>
{autoRefresh && (
<select
value={refreshInterval}
onChange={(e) => setRefreshInterval(Number(e.target.value))}
className="text-sm rounded border-gray-300 dark:bg-gray-800 dark:border-gray-700"
>
<option value={30}>30s</option>
<option value={60}>1min</option>
<option value={300}>5min</option>
</select>
)}
<button
onClick={checkAllEndpoints}
disabled={loading}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
>
{loading ? 'Checking...' : 'Refresh All'}
</button>
</div>
</div>
{/* Monitoring Tables - 3 per row */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{endpointGroups.map((group, groupIndex) => {
const groupHealth = getGroupHealth(group);
return (
<ComponentCard
key={groupIndex}
title={group.name}
desc={`${groupHealth.healthy}/${groupHealth.total} healthy`}
>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
Endpoint
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
Status
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
Time
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{group.endpoints.map((endpoint, epIndex) => {
const status = getEndpointStatus(endpoint.path, endpoint.method);
return (
<tr key={epIndex} className="hover:bg-gray-50 dark:hover:bg-gray-800/50">
<td className="px-3 py-2">
<div className="text-xs">
<span className="font-mono text-gray-600 dark:text-gray-400">
{endpoint.method}
</span>
<div className="text-xs text-gray-500 dark:text-gray-500 mt-0.5 truncate max-w-[200px]">
{endpoint.path}
</div>
<div className="text-xs text-gray-400 dark:text-gray-600 mt-0.5">
{endpoint.description}
</div>
</div>
</td>
<td className="px-3 py-2">
<span
className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded ${getStatusBadge(status.status)}`}
title={status.error || status.status}
>
<span>{getStatusIcon(status.status)}</span>
<span className="capitalize">{status.status}</span>
</span>
</td>
<td className="px-3 py-2">
{status.responseTime ? (
<span className="text-xs text-gray-600 dark:text-gray-400">
{status.responseTime}ms
</span>
) : (
<span className="text-xs text-gray-400 dark:text-gray-600">-</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</ComponentCard>
);
})}
</div>
{/* Summary Stats */}
<ComponentCard title="Summary" desc="Overall API health statistics">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{endpointGroups.map((group, index) => {
const groupHealth = getGroupHealth(group);
const percentage = groupHealth.total > 0
? Math.round((groupHealth.healthy / groupHealth.total) * 100)
: 0;
return (
<div key={index} className="text-center">
<div className="text-2xl font-semibold text-gray-800 dark:text-white/90">
{percentage}%
</div>
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{group.name}
</div>
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">
{groupHealth.healthy}/{groupHealth.total} healthy
</div>
</div>
);
})}
</div>
</ComponentCard>
</div>
</>
);
}