Implement unified API standard across backend viewsets and serializers, enhancing error handling and response formatting. Update AccountModelViewSet to standardize CRUD operations with success and error responses. Refactor various viewsets to inherit from AccountModelViewSet, ensuring compliance with the new standard. Improve frontend components to handle API responses consistently and update configuration for better user experience.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
interface ComponentCardProps {
|
||||
title: string;
|
||||
title: string | React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
className?: string; // Additional custom classes for styling
|
||||
desc?: string; // Description text
|
||||
desc?: string | React.ReactNode; // Description text
|
||||
}
|
||||
|
||||
const ComponentCard: React.FC<ComponentCardProps> = ({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useLocation } from "react-router";
|
||||
import { API_BASE_URL } from "../../services/api";
|
||||
import { useAuthStore } from "../../store/authStore";
|
||||
|
||||
@@ -37,7 +38,7 @@ const endpointGroups = [
|
||||
},
|
||||
{
|
||||
name: "Planner Module",
|
||||
abbreviation: "PL",
|
||||
abbreviation: "PM",
|
||||
endpoints: [
|
||||
{ path: "/v1/planner/keywords/", method: "GET" },
|
||||
{ path: "/v1/planner/keywords/auto_cluster/", method: "POST" },
|
||||
@@ -49,7 +50,7 @@ const endpointGroups = [
|
||||
},
|
||||
{
|
||||
name: "Writer Module",
|
||||
abbreviation: "WR",
|
||||
abbreviation: "WM",
|
||||
endpoints: [
|
||||
{ path: "/v1/writer/tasks/", method: "GET" },
|
||||
{ path: "/v1/writer/tasks/auto_generate_content/", method: "POST" },
|
||||
@@ -60,6 +61,48 @@ const endpointGroups = [
|
||||
{ path: "/v1/writer/images/generate_images/", method: "POST" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "CRUD Operations - Planner",
|
||||
abbreviation: "PC",
|
||||
endpoints: [
|
||||
{ path: "/v1/planner/keywords/", method: "GET" },
|
||||
{ path: "/v1/planner/keywords/", method: "POST" },
|
||||
{ path: "/v1/planner/keywords/1/", method: "GET" },
|
||||
{ path: "/v1/planner/keywords/1/", method: "PUT" },
|
||||
{ path: "/v1/planner/keywords/1/", method: "DELETE" },
|
||||
{ path: "/v1/planner/clusters/", method: "GET" },
|
||||
{ path: "/v1/planner/clusters/", method: "POST" },
|
||||
{ path: "/v1/planner/clusters/1/", method: "GET" },
|
||||
{ path: "/v1/planner/clusters/1/", method: "PUT" },
|
||||
{ path: "/v1/planner/clusters/1/", method: "DELETE" },
|
||||
{ path: "/v1/planner/ideas/", method: "GET" },
|
||||
{ path: "/v1/planner/ideas/", method: "POST" },
|
||||
{ path: "/v1/planner/ideas/1/", method: "GET" },
|
||||
{ path: "/v1/planner/ideas/1/", method: "PUT" },
|
||||
{ path: "/v1/planner/ideas/1/", method: "DELETE" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "CRUD Operations - Writer",
|
||||
abbreviation: "WC",
|
||||
endpoints: [
|
||||
{ path: "/v1/writer/tasks/", method: "GET" },
|
||||
{ path: "/v1/writer/tasks/", method: "POST" },
|
||||
{ path: "/v1/writer/tasks/1/", method: "GET" },
|
||||
{ path: "/v1/writer/tasks/1/", method: "PUT" },
|
||||
{ path: "/v1/writer/tasks/1/", method: "DELETE" },
|
||||
{ path: "/v1/writer/content/", method: "GET" },
|
||||
{ path: "/v1/writer/content/", method: "POST" },
|
||||
{ path: "/v1/writer/content/1/", method: "GET" },
|
||||
{ path: "/v1/writer/content/1/", method: "PUT" },
|
||||
{ path: "/v1/writer/content/1/", method: "DELETE" },
|
||||
{ path: "/v1/writer/images/", method: "GET" },
|
||||
{ path: "/v1/writer/images/", method: "POST" },
|
||||
{ path: "/v1/writer/images/1/", method: "GET" },
|
||||
{ path: "/v1/writer/images/1/", method: "PUT" },
|
||||
{ path: "/v1/writer/images/1/", method: "DELETE" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "System & Billing",
|
||||
abbreviation: "SY",
|
||||
@@ -67,7 +110,7 @@ const endpointGroups = [
|
||||
{ path: "/v1/system/prompts/", method: "GET" },
|
||||
{ path: "/v1/system/author-profiles/", method: "GET" },
|
||||
{ path: "/v1/system/strategies/", method: "GET" },
|
||||
{ path: "/v1/system/settings/integrations/1/test/", method: "POST" },
|
||||
{ path: "/v1/system/settings/integrations/openai/test/", method: "POST" },
|
||||
{ path: "/v1/system/settings/account/", method: "GET" },
|
||||
{ path: "/v1/billing/credits/balance/balance/", method: "GET" },
|
||||
{ path: "/v1/billing/credits/usage/", method: "GET" },
|
||||
@@ -79,6 +122,7 @@ const endpointGroups = [
|
||||
|
||||
export default function ApiStatusIndicator() {
|
||||
const { user } = useAuthStore();
|
||||
const location = useLocation();
|
||||
const [groupStatuses, setGroupStatuses] = useState<GroupStatus[]>([]);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const intervalRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -86,8 +130,11 @@ export default function ApiStatusIndicator() {
|
||||
// Only show and run for aws-admin accounts
|
||||
const isAwsAdmin = user?.account?.slug === 'aws-admin';
|
||||
|
||||
// Return null if not aws-admin account
|
||||
if (!isAwsAdmin) {
|
||||
// Only run API checks on API monitor page to avoid console errors on other pages
|
||||
const isApiMonitorPage = location.pathname === '/settings/api-monitor';
|
||||
|
||||
// Return null if not aws-admin account or not on API monitor page
|
||||
if (!isAwsAdmin || !isApiMonitorPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -141,8 +188,17 @@ export default function ApiStatusIndicator() {
|
||||
body = { username: 'test', password: 'test' };
|
||||
} else if (path.includes('/register/')) {
|
||||
body = { username: 'test', email: 'test@test.com', password: 'test' };
|
||||
} else if (path.includes('/bulk_delete/')) {
|
||||
body = { ids: [] }; // Empty array to trigger validation error
|
||||
} else if (path.includes('/bulk_update/')) {
|
||||
body = { ids: [] }; // Empty array to trigger validation error
|
||||
}
|
||||
fetchOptions.body = JSON.stringify(body);
|
||||
} else if (method === 'PUT' || method === 'DELETE') {
|
||||
// For PUT/DELETE, we need to send a body for PUT or handle DELETE
|
||||
if (method === 'PUT') {
|
||||
fetchOptions.body = JSON.stringify({}); // Empty object to trigger validation
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress console errors for expected 400 responses (validation errors from test data)
|
||||
@@ -154,11 +210,13 @@ export default function ApiStatusIndicator() {
|
||||
path.includes('/test/')
|
||||
);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, fetchOptions);
|
||||
|
||||
// Suppress console errors for expected 400 responses
|
||||
if (!isExpected400 || response.status !== 400) {
|
||||
// Only log if it's not an expected 400
|
||||
// Use a silent fetch that won't log to console for expected errors
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`${API_BASE_URL}${path}`, fetchOptions);
|
||||
} catch (fetchError) {
|
||||
// Network errors are real errors
|
||||
return 'error';
|
||||
}
|
||||
|
||||
if (actualMethod === 'OPTIONS') {
|
||||
@@ -176,24 +234,31 @@ export default function ApiStatusIndicator() {
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
return 'warning';
|
||||
} else if (response.status === 404) {
|
||||
return 'error';
|
||||
// For GET requests to specific resource IDs (e.g., /v1/planner/keywords/1/),
|
||||
// 404 is expected and healthy (resource doesn't exist, but endpoint works correctly)
|
||||
// For other GET requests (like list endpoints), 404 means endpoint doesn't exist
|
||||
const isResourceByIdRequest = /\/\d+\/?$/.test(path); // Path ends with /number/ or /number
|
||||
if (isResourceByIdRequest) {
|
||||
return 'healthy'; // GET to specific ID returning 404 is healthy (endpoint exists, resource doesn't)
|
||||
}
|
||||
return 'error'; // Endpoint doesn't exist
|
||||
} else if (response.status >= 500) {
|
||||
return 'error';
|
||||
}
|
||||
return 'warning';
|
||||
} else if (method === 'POST') {
|
||||
// Suppress console errors for expected 400 responses (validation errors from test data)
|
||||
// CRUD POST endpoints (like /v1/planner/keywords/, /v1/writer/tasks/) return 400 for empty/invalid test data
|
||||
const isExpected400 = path.includes('/login/') ||
|
||||
path.includes('/register/') ||
|
||||
path.includes('/bulk_') ||
|
||||
path.includes('/test/');
|
||||
path.includes('/test/') ||
|
||||
// CRUD CREATE endpoints - POST to list endpoints (no ID in path, ends with / or exact match)
|
||||
/\/v1\/(planner|writer)\/(keywords|clusters|ideas|tasks|content|images)\/?$/.test(path);
|
||||
|
||||
if (response.status === 400) {
|
||||
// 400 is expected for test requests - endpoint is working
|
||||
if (!isExpected400) {
|
||||
// Only log if it's unexpected
|
||||
console.warn(`[ApiStatusIndicator] ${method} ${path}: 400 (unexpected)`);
|
||||
}
|
||||
// Don't log warnings for expected 400s - they're normal validation errors
|
||||
return 'healthy';
|
||||
} else if (response.status >= 200 && response.status < 300) {
|
||||
return 'healthy';
|
||||
|
||||
@@ -515,7 +515,7 @@ export const createKeywordsPageConfig = (
|
||||
label: 'Seed Keyword',
|
||||
type: 'select',
|
||||
placeholder: 'Select a seed keyword',
|
||||
value: handlers.formData.seed_keyword_id?.toString() || '',
|
||||
value: (handlers.formData.seed_keyword_id && handlers.formData.seed_keyword_id > 0) ? handlers.formData.seed_keyword_id.toString() : '',
|
||||
onChange: (value: any) =>
|
||||
handlers.setFormData({ ...handlers.formData, seed_keyword_id: value ? parseInt(value) : 0 }),
|
||||
required: true,
|
||||
|
||||
@@ -874,6 +874,7 @@ export default function Keywords() {
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
<FormModal
|
||||
key={`keyword-form-${isEditMode ? editingKeyword?.id : 'new'}-${formData.seed_keyword_id}-${formData.status}`}
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false);
|
||||
|
||||
@@ -299,7 +299,27 @@ export default function ApiMonitor() {
|
||||
fetchOptions.body = JSON.stringify({});
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, fetchOptions);
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`${API_BASE_URL}${path}`, fetchOptions);
|
||||
} catch (error: any) {
|
||||
// Network error or fetch failed
|
||||
const responseTime = Date.now() - startTime;
|
||||
setEndpointStatuses(prev => ({
|
||||
...prev,
|
||||
[key]: {
|
||||
endpoint: path,
|
||||
method,
|
||||
status: 'error',
|
||||
responseTime,
|
||||
error: error.message || 'Network error',
|
||||
apiStatus: 'error',
|
||||
dataStatus: 'error',
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
// Determine status based on response
|
||||
@@ -349,38 +369,55 @@ export default function ApiMonitor() {
|
||||
if (responseData.success === false) {
|
||||
status = 'error'; // API returned an error in unified format
|
||||
} else if (responseData.success === true) {
|
||||
// Check if data is empty for endpoints that should return data
|
||||
// These endpoints should have data: {count: X, results: [...]} or data: {...}
|
||||
const shouldHaveData =
|
||||
path.includes('/content_images/') ||
|
||||
path.includes('/prompts/by_type/') ||
|
||||
path.includes('/usage/limits/') ||
|
||||
path.includes('/prompts/') && !path.includes('/save/');
|
||||
// Check for paginated response format (success: true, count: X, results: [...])
|
||||
// or single object response format (success: true, data: {...})
|
||||
const isPaginatedResponse = 'results' in responseData && 'count' in responseData;
|
||||
const isSingleObjectResponse = 'data' in responseData;
|
||||
|
||||
if (shouldHaveData) {
|
||||
// Check if data field exists and has content
|
||||
if (responseData.data === null || responseData.data === undefined) {
|
||||
status = 'warning'; // Missing data field
|
||||
} else if (Array.isArray(responseData.data) && responseData.data.length === 0) {
|
||||
// Empty array might be OK for some endpoints, but check if results should exist
|
||||
if (isPaginatedResponse) {
|
||||
// Paginated response - check results at top level
|
||||
if (!Array.isArray(responseData.results)) {
|
||||
status = 'warning'; // Missing or invalid results array
|
||||
} else if (responseData.results.length === 0 && responseData.count === 0) {
|
||||
// Empty results with count 0 is OK for list endpoints
|
||||
// Only warn for critical endpoints that should have data
|
||||
if (path.includes('/content_images/') || path.includes('/prompts/by_type/')) {
|
||||
// These endpoints should return data, empty might indicate a problem
|
||||
status = 'warning'; // Empty data - might indicate configuration issue
|
||||
status = 'warning'; // No data available - might indicate configuration issue
|
||||
}
|
||||
} else if (typeof responseData.data === 'object' && responseData.data !== null) {
|
||||
// Check if it's a paginated response with empty results
|
||||
if (responseData.data.results && Array.isArray(responseData.data.results) && responseData.data.results.length === 0) {
|
||||
// Empty results might be OK, but for critical endpoints it's a warning
|
||||
}
|
||||
} else if (isSingleObjectResponse) {
|
||||
// Single object response - check data field
|
||||
const shouldHaveData =
|
||||
path.includes('/content_images/') ||
|
||||
path.includes('/prompts/by_type/') ||
|
||||
path.includes('/usage/limits/');
|
||||
|
||||
if (shouldHaveData) {
|
||||
if (responseData.data === null || responseData.data === undefined) {
|
||||
status = 'warning'; // Missing data field
|
||||
} else if (Array.isArray(responseData.data) && responseData.data.length === 0) {
|
||||
if (path.includes('/content_images/') || path.includes('/prompts/by_type/')) {
|
||||
status = 'warning'; // Empty results - might indicate data issue
|
||||
status = 'warning'; // Empty data - might indicate configuration issue
|
||||
}
|
||||
} else if (responseData.data.count !== undefined && responseData.data.count === 0) {
|
||||
// Paginated response with count: 0
|
||||
if (path.includes('/content_images/') || path.includes('/prompts/by_type/')) {
|
||||
status = 'warning'; // No data available - might indicate configuration issue
|
||||
} else if (typeof responseData.data === 'object' && responseData.data !== null) {
|
||||
// Check if it's a nested paginated response
|
||||
if (responseData.data.results && Array.isArray(responseData.data.results) && responseData.data.results.length === 0) {
|
||||
if (path.includes('/content_images/') || path.includes('/prompts/by_type/')) {
|
||||
status = 'warning'; // Empty results - might indicate data issue
|
||||
}
|
||||
} else if (responseData.data.count !== undefined && responseData.data.count === 0) {
|
||||
if (path.includes('/content_images/') || path.includes('/prompts/by_type/')) {
|
||||
status = 'warning'; // No data available - might indicate configuration issue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!isPaginatedResponse && !isSingleObjectResponse) {
|
||||
// Response has success: true but no data or results
|
||||
// For paginated list endpoints, this is a problem
|
||||
if (path.includes('/prompts/') && !path.includes('/save/') && !path.includes('/by_type/')) {
|
||||
status = 'warning'; // Paginated endpoint missing results field
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,7 +431,15 @@ export default function ApiMonitor() {
|
||||
} 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
|
||||
// For GET requests to specific resource IDs (e.g., /v1/planner/keywords/1/),
|
||||
// 404 is expected and healthy (resource doesn't exist, but endpoint works correctly)
|
||||
// For other GET requests (like list endpoints), 404 means endpoint doesn't exist
|
||||
const isResourceByIdRequest = /\/\d+\/?$/.test(path); // Path ends with /number/ or /number
|
||||
if (method === 'GET' && isResourceByIdRequest) {
|
||||
status = 'healthy'; // GET to specific ID returning 404 is healthy (endpoint exists, resource doesn't)
|
||||
} else {
|
||||
status = 'error'; // Endpoint doesn't exist
|
||||
}
|
||||
} else if (response.status >= 500) {
|
||||
status = 'error';
|
||||
} else {
|
||||
@@ -510,18 +555,42 @@ export default function ApiMonitor() {
|
||||
}
|
||||
|
||||
// Log warnings/errors for issues detected in response content
|
||||
if (status === 'warning' || status === 'error') {
|
||||
// Skip logging for expected 400 responses on POST (validation errors are expected)
|
||||
const isExpected400Post = method === 'POST' && response.status === 400;
|
||||
if ((status === 'warning' || status === 'error') && !isExpected400Post) {
|
||||
if (responseData) {
|
||||
if (responseData.success === false) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Unified format error - ${responseData.error || 'Unknown error'}`);
|
||||
} else if (responseData.data === null || responseData.data === undefined) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Missing data field in response`);
|
||||
} else if (Array.isArray(responseData.data) && responseData.data.length === 0) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Empty data array returned`);
|
||||
} else if (responseData.data?.results && Array.isArray(responseData.data.results) && responseData.data.results.length === 0) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Empty results array returned`);
|
||||
} else if (responseData.data?.count === 0) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: No data available (count: 0)`);
|
||||
} else {
|
||||
// Check for paginated response format
|
||||
const isPaginated = 'results' in responseData && 'count' in responseData;
|
||||
const isSingleObject = 'data' in responseData;
|
||||
|
||||
if (isPaginated) {
|
||||
// Paginated response - check results at top level
|
||||
if (!Array.isArray(responseData.results)) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Missing or invalid results array in paginated response`);
|
||||
} else if (responseData.results.length === 0 && responseData.count === 0 &&
|
||||
(path.includes('/prompts/') && !path.includes('/save/') && !path.includes('/by_type/'))) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Empty paginated response (count: 0, results: [])`);
|
||||
}
|
||||
} else if (isSingleObject) {
|
||||
// Single object response - check data field
|
||||
if (responseData.data === null || responseData.data === undefined) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Missing data field in response`);
|
||||
} else if (Array.isArray(responseData.data) && responseData.data.length === 0) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Empty data array returned`);
|
||||
} else if (responseData.data?.results && Array.isArray(responseData.data.results) && responseData.data.results.length === 0) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Empty results array returned`);
|
||||
} else if (responseData.data?.count === 0) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: No data available (count: 0)`);
|
||||
}
|
||||
} else if (responseData.success === true && !isPaginated && !isSingleObject) {
|
||||
// Response has success: true but no data or results
|
||||
if (path.includes('/prompts/') && !path.includes('/save/') && !path.includes('/by_type/')) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Paginated endpoint missing results field`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -529,13 +598,16 @@ export default function ApiMonitor() {
|
||||
// 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)
|
||||
// Don't log expected 404s for GET requests to specific resource IDs (they indicate endpoint works correctly)
|
||||
const isResourceByIdRequest = /\/\d+\/?$/.test(path); // Path ends with /number/ or /number
|
||||
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 && status === 'healthy'); // Expected GET success with valid data
|
||||
(method === 'GET' && response.status >= 200 && response.status < 300 && status === 'healthy') || // Expected GET success with valid data
|
||||
(method === 'GET' && response.status === 404 && isResourceByIdRequest); // Expected 404 for GET to non-existent resource ID
|
||||
|
||||
if (!isExpectedResponse && (response.status >= 500 ||
|
||||
(method === 'GET' && response.status === 404) ||
|
||||
(method === 'GET' && response.status === 404 && !isResourceByIdRequest) ||
|
||||
(actualMethod === 'OPTIONS' && response.status !== 200))) {
|
||||
// These are real errors worth logging
|
||||
console.warn(`[API Monitor] ${method} ${path}: ${response.status}`, responseText.substring(0, 100));
|
||||
@@ -622,10 +694,43 @@ export default function ApiMonitor() {
|
||||
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 warning = statuses.filter(s => s === 'warning').length;
|
||||
const error = statuses.filter(s => s === 'error').length;
|
||||
const total = statuses.length;
|
||||
return { healthy, total };
|
||||
return { healthy, warning, error, total };
|
||||
};
|
||||
|
||||
const getGroupStatus = (group: EndpointGroup): 'error' | 'warning' | 'healthy' => {
|
||||
const health = getGroupHealth(group);
|
||||
if (health.error > 0) return 'error';
|
||||
if (health.warning > 0) return 'warning';
|
||||
return 'healthy';
|
||||
};
|
||||
|
||||
const getStatusPriority = (status: 'error' | 'warning' | 'healthy'): number => {
|
||||
switch (status) {
|
||||
case 'error': return 0;
|
||||
case 'warning': return 1;
|
||||
case 'healthy': return 2;
|
||||
default: return 3;
|
||||
}
|
||||
};
|
||||
|
||||
// Sort endpoint groups by status (error > warning > healthy)
|
||||
const sortedEndpointGroups = [...endpointGroups].sort((a, b) => {
|
||||
const statusA = getGroupStatus(a);
|
||||
const statusB = getGroupStatus(b);
|
||||
const priorityA = getStatusPriority(statusA);
|
||||
const priorityB = getStatusPriority(statusB);
|
||||
|
||||
// If same priority, sort by name
|
||||
if (priorityA === priorityB) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
return priorityA - priorityB;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="API Monitor - IGNY8" description="API endpoint monitoring" />
|
||||
@@ -682,13 +787,27 @@ export default function ApiMonitor() {
|
||||
|
||||
{/* Monitoring Tables - 3 per row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{endpointGroups.map((group, groupIndex) => {
|
||||
{sortedEndpointGroups.map((group, groupIndex) => {
|
||||
const groupHealth = getGroupHealth(group);
|
||||
const groupStatus = getGroupStatus(group);
|
||||
return (
|
||||
<ComponentCard
|
||||
key={groupIndex}
|
||||
title={group.name}
|
||||
desc={`${groupHealth.healthy}/${groupHealth.total} healthy`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{group.name}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${getStatusBadge(groupStatus)}`}>
|
||||
{getStatusIcon(groupStatus)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
desc={
|
||||
groupStatus === 'error'
|
||||
? `${groupHealth.error} error${groupHealth.error !== 1 ? 's' : ''}, ${groupHealth.warning} warning${groupHealth.warning !== 1 ? 's' : ''}, ${groupHealth.healthy} healthy`
|
||||
: groupStatus === 'warning'
|
||||
? `${groupHealth.warning} warning${groupHealth.warning !== 1 ? 's' : ''}, ${groupHealth.healthy} healthy`
|
||||
: `${groupHealth.healthy}/${groupHealth.total} healthy`
|
||||
}
|
||||
>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
@@ -706,8 +825,24 @@ export default function ApiMonitor() {
|
||||
</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);
|
||||
{group.endpoints
|
||||
.map((endpoint, epIndex) => ({
|
||||
endpoint,
|
||||
epIndex,
|
||||
status: getEndpointStatus(endpoint.path, endpoint.method),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
const priorityA = getStatusPriority(a.status.status);
|
||||
const priorityB = getStatusPriority(b.status.status);
|
||||
// If same priority, sort by method then path
|
||||
if (priorityA === priorityB) {
|
||||
const methodCompare = a.endpoint.method.localeCompare(b.endpoint.method);
|
||||
if (methodCompare !== 0) return methodCompare;
|
||||
return a.endpoint.path.localeCompare(b.endpoint.path);
|
||||
}
|
||||
return priorityA - priorityB;
|
||||
})
|
||||
.map(({ endpoint, epIndex, status }) => {
|
||||
return (
|
||||
<tr key={epIndex} className="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td className="px-3 py-2">
|
||||
@@ -767,22 +902,26 @@ export default function ApiMonitor() {
|
||||
{/* 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) => {
|
||||
{sortedEndpointGroups.map((group, index) => {
|
||||
const groupHealth = getGroupHealth(group);
|
||||
const groupStatus = getGroupStatus(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">
|
||||
<div className={`text-2xl font-semibold ${getStatusColor(groupStatus)}`}>
|
||||
{percentage}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{group.name}
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1 flex items-center justify-center gap-1">
|
||||
<span>{group.name}</span>
|
||||
<span className={getStatusColor(groupStatus)}>{getStatusIcon(groupStatus)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
||||
{groupHealth.healthy}/{groupHealth.total} healthy
|
||||
{groupHealth.error > 0 && ` • ${groupHealth.error} error${groupHealth.error !== 1 ? 's' : ''}`}
|
||||
{groupHealth.warning > 0 && ` • ${groupHealth.warning} warning${groupHealth.warning !== 1 ? 's' : ''}`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -640,18 +640,33 @@ export async function autoClusterKeywords(keywordIds: number[], sectorId?: numbe
|
||||
const requestBody = { ids: keywordIds, sector_id: sectorId };
|
||||
|
||||
try {
|
||||
// fetchAPI will automatically extract data from unified format
|
||||
// For action endpoints, response is {success: true, data: {...}}
|
||||
// fetchAPI extracts and returns the data field, so response should already be the data object
|
||||
const response = await fetchAPI(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
// Check if response indicates an error (success: false)
|
||||
if (response && response.success === false) {
|
||||
// Return error response as-is so caller can check result.success
|
||||
return response;
|
||||
// After fetchAPI processing, response should be the data object (not wrapped in success/data)
|
||||
// But check if it's still wrapped (shouldn't happen, but for safety)
|
||||
if (response && typeof response === 'object') {
|
||||
if ('success' in response && response.success === false) {
|
||||
// Error response - return as-is
|
||||
return response as any;
|
||||
}
|
||||
// If response has data field, extract it
|
||||
if ('data' in response && response.data) {
|
||||
return { success: true, ...response.data } as any;
|
||||
}
|
||||
// Response is already the data object (after fetchAPI extraction)
|
||||
// Ensure it has success: true
|
||||
if (!('success' in response)) {
|
||||
return { success: true, ...response } as any;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
return response as any;
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user