Add API Status Indicator to AppSidebar and enhance ApiMonitor with localStorage support for auto-refresh and refresh interval settings
This commit is contained in:
@@ -25,7 +25,6 @@ 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" },
|
||||
@@ -108,8 +107,16 @@ const getStatusIcon = (status: string) => {
|
||||
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 [autoRefresh, setAutoRefresh] = useState(() => {
|
||||
// Load from localStorage
|
||||
const saved = localStorage.getItem('api-monitor-auto-refresh');
|
||||
return saved !== null ? saved === 'true' : true;
|
||||
});
|
||||
const [refreshInterval, setRefreshInterval] = useState(() => {
|
||||
// Load from localStorage, default to 30 seconds
|
||||
const saved = localStorage.getItem('api-monitor-refresh-interval');
|
||||
return saved ? parseInt(saved, 10) : 30;
|
||||
});
|
||||
|
||||
const checkEndpoint = useCallback(async (path: string, method: string) => {
|
||||
const key = `${method}:${path}`;
|
||||
@@ -127,7 +134,6 @@ export default function ApiMonitor() {
|
||||
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') ||
|
||||
(() => {
|
||||
@@ -151,27 +157,119 @@ export default function ApiMonitor() {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method,
|
||||
// Determine if this is an expensive AI endpoint that should use OPTIONS
|
||||
const isExpensiveAIEndpoint =
|
||||
path.includes('/auto_generate_content') ||
|
||||
path.includes('/auto_cluster') ||
|
||||
path.includes('/auto_generate_ideas') ||
|
||||
path.includes('/generate_image_prompts') ||
|
||||
path.includes('/generate_images');
|
||||
|
||||
let actualMethod = method;
|
||||
let fetchOptions: RequestInit = {
|
||||
method: actualMethod,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
};
|
||||
|
||||
// For POST endpoints, send empty body for monitoring
|
||||
if (method === 'POST') {
|
||||
fetchOptions.body = JSON.stringify({});
|
||||
// For expensive AI POST endpoints, use OPTIONS to check existence without triggering function
|
||||
// If OPTIONS fails, we'll fall back to POST with empty IDs (which fails validation before triggering AI)
|
||||
if (method === 'POST' && isExpensiveAIEndpoint) {
|
||||
actualMethod = 'OPTIONS';
|
||||
fetchOptions.method = 'OPTIONS';
|
||||
// OPTIONS doesn't need body - remove it if present
|
||||
delete (fetchOptions as any).body;
|
||||
} else if (method === 'POST') {
|
||||
// For non-expensive POST endpoints, send invalid data that fails validation early
|
||||
let body: any = {};
|
||||
|
||||
if (path.includes('/test/')) {
|
||||
body = {}; // Test endpoint might accept empty body
|
||||
} else if (path.includes('/login/')) {
|
||||
body = { username: 'test', password: 'test' }; // Will fail validation but endpoint exists
|
||||
} else if (path.includes('/register/')) {
|
||||
body = { username: 'test', email: 'test@test.com', password: 'test' }; // Will fail validation but endpoint exists
|
||||
}
|
||||
|
||||
fetchOptions.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
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
|
||||
// Determine status based on response
|
||||
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)
|
||||
let responseText = '';
|
||||
|
||||
// Read response body for debugging (but don't log errors for expected 400s)
|
||||
try {
|
||||
responseText = await response.text();
|
||||
} catch (e) {
|
||||
// Ignore body read errors
|
||||
}
|
||||
|
||||
if (actualMethod === 'OPTIONS') {
|
||||
// OPTIONS: 200 with Allow header = healthy, anything else = error
|
||||
if (response.status === 200) {
|
||||
const allowHeader = response.headers.get('Allow') || response.headers.get('allow');
|
||||
if (allowHeader && (allowHeader.includes('POST') || allowHeader.includes('post'))) {
|
||||
status = 'healthy';
|
||||
} else {
|
||||
// If we can't read Allow header (CORS issue), but got 200, assume it's healthy
|
||||
// OPTIONS 200 generally means endpoint exists
|
||||
status = 'healthy';
|
||||
}
|
||||
} else if (response.status === 404) {
|
||||
status = 'error'; // Endpoint doesn't exist
|
||||
} else if (response.status >= 500) {
|
||||
status = 'error';
|
||||
} else {
|
||||
status = 'warning';
|
||||
}
|
||||
} else if (method === 'GET') {
|
||||
// GET: 2xx = healthy, 401/403 = warning (needs auth), 404 = error, 5xx = error
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
status = 'healthy';
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
status = 'warning'; // Endpoint exists, needs authentication
|
||||
} else if (response.status === 404) {
|
||||
status = 'error'; // Endpoint doesn't exist
|
||||
} else if (response.status >= 500) {
|
||||
status = 'error';
|
||||
} else {
|
||||
status = 'warning';
|
||||
}
|
||||
} else if (method === 'POST') {
|
||||
// POST: 400 = healthy (endpoint exists and validates), 401/403 = warning, 404 = error, 5xx = error
|
||||
if (response.status === 400) {
|
||||
// 400 means endpoint exists and validation works - this is healthy for monitoring
|
||||
status = 'healthy';
|
||||
} else if (response.status >= 200 && response.status < 300) {
|
||||
status = 'healthy';
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
status = 'warning'; // Endpoint exists, needs authentication
|
||||
} else if (response.status === 404) {
|
||||
status = 'error'; // Endpoint doesn't exist
|
||||
} else if (response.status >= 500) {
|
||||
status = 'error';
|
||||
} else {
|
||||
status = 'warning';
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress console errors for expected monitoring responses
|
||||
// Only log real errors (5xx, network errors, or unexpected 4xx for GET endpoints)
|
||||
// Don't log expected 400s for POST endpoints (they indicate validation is working)
|
||||
const isExpectedResponse =
|
||||
(method === 'POST' && response.status === 400) || // Expected validation error
|
||||
(actualMethod === 'OPTIONS' && response.status === 200) || // Expected OPTIONS success
|
||||
(method === 'GET' && response.status >= 200 && response.status < 300); // Expected GET success
|
||||
|
||||
if (!isExpectedResponse && (response.status >= 500 ||
|
||||
(method === 'GET' && response.status === 404) ||
|
||||
(actualMethod === 'OPTIONS' && response.status !== 200))) {
|
||||
// These are real errors worth logging
|
||||
console.warn(`[API Monitor] ${method} ${path}: ${response.status}`, responseText.substring(0, 100));
|
||||
}
|
||||
|
||||
setEndpointStatuses(prev => ({
|
||||
@@ -272,7 +370,11 @@ export default function ApiMonitor() {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoRefresh}
|
||||
onChange={(e) => setAutoRefresh(e.target.checked)}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.checked;
|
||||
setAutoRefresh(newValue);
|
||||
localStorage.setItem('api-monitor-auto-refresh', String(newValue));
|
||||
}}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
Auto-refresh
|
||||
@@ -280,7 +382,13 @@ export default function ApiMonitor() {
|
||||
{autoRefresh && (
|
||||
<select
|
||||
value={refreshInterval}
|
||||
onChange={(e) => setRefreshInterval(Number(e.target.value))}
|
||||
onChange={(e) => {
|
||||
const newValue = Number(e.target.value);
|
||||
setRefreshInterval(newValue);
|
||||
localStorage.setItem('api-monitor-refresh-interval', String(newValue));
|
||||
// Dispatch custom event to notify sidebar component
|
||||
window.dispatchEvent(new Event('api-monitor-interval-changed'));
|
||||
}}
|
||||
className="text-sm rounded border-gray-300 dark:bg-gray-800 dark:border-gray-700"
|
||||
>
|
||||
<option value={30}>30s</option>
|
||||
|
||||
Reference in New Issue
Block a user