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:
IGNY8 VPS (Salman)
2025-11-15 23:04:31 +00:00
parent 5a3706d997
commit 0ec594363c
11 changed files with 593 additions and 113 deletions

View File

@@ -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>
);