be fe fixes
This commit is contained in:
802
frontend/src/pages/Settings/DebugStatus.tsx
Normal file
802
frontend/src/pages/Settings/DebugStatus.tsx
Normal file
@@ -0,0 +1,802 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import PageMeta from "../../components/common/PageMeta";
|
||||
import ComponentCard from "../../components/common/ComponentCard";
|
||||
import { API_BASE_URL, fetchAPI } from "../../services/api";
|
||||
|
||||
interface HealthCheck {
|
||||
name: string;
|
||||
description: string;
|
||||
status: 'healthy' | 'warning' | 'error' | 'checking';
|
||||
message?: string;
|
||||
details?: string;
|
||||
lastChecked?: string;
|
||||
}
|
||||
|
||||
interface ModuleHealth {
|
||||
module: string;
|
||||
description: string;
|
||||
checks: HealthCheck[];
|
||||
}
|
||||
|
||||
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 DebugStatus() {
|
||||
const [moduleHealths, setModuleHealths] = useState<ModuleHealth[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Helper to get auth token
|
||||
const getAuthToken = () => {
|
||||
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 '';
|
||||
})();
|
||||
return token;
|
||||
};
|
||||
|
||||
// Helper to make authenticated API calls
|
||||
const apiCall = async (path: string, method: string = 'GET', body?: any) => {
|
||||
const token = getAuthToken();
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
};
|
||||
|
||||
if (body && method !== 'GET') {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, options);
|
||||
const data = await response.json();
|
||||
return { response, data };
|
||||
};
|
||||
|
||||
// Check database schema field mappings (the issues we just fixed)
|
||||
const checkDatabaseSchemaMapping = useCallback(async (): Promise<HealthCheck> => {
|
||||
try {
|
||||
// Test Writer Content endpoint (was failing with entity_type error)
|
||||
const { response: contentResp, data: contentData } = await apiCall('/v1/writer/content/');
|
||||
|
||||
if (!contentResp.ok) {
|
||||
return {
|
||||
name: 'Database Schema Mapping',
|
||||
description: 'Checks if model field names map correctly to database columns',
|
||||
status: 'error',
|
||||
message: `Writer Content API failed with ${contentResp.status}`,
|
||||
details: 'Model fields may not be mapped to database columns correctly (e.g., content_type vs entity_type)',
|
||||
lastChecked: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// Check if response has the expected structure with new field names
|
||||
if (contentData?.results && Array.isArray(contentData.results)) {
|
||||
// If we can fetch content, schema mapping is working
|
||||
return {
|
||||
name: 'Database Schema Mapping',
|
||||
description: 'Checks if model field names map correctly to database columns',
|
||||
status: 'healthy',
|
||||
message: 'All model fields correctly mapped via db_column attributes',
|
||||
details: `Content API working correctly. Fields like content_type, content_html, content_structure are properly mapped.`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Database Schema Mapping',
|
||||
description: 'Checks if model field names map correctly to database columns',
|
||||
status: 'warning',
|
||||
message: 'Content API returned unexpected structure',
|
||||
details: 'Response format may have changed',
|
||||
lastChecked: new Date().toISOString(),
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
name: 'Database Schema Mapping',
|
||||
description: 'Checks if model field names map correctly to database columns',
|
||||
status: 'error',
|
||||
message: error.message || 'Failed to check schema mapping',
|
||||
details: 'Check if db_column attributes are set correctly in models',
|
||||
lastChecked: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Check Writer module health
|
||||
const checkWriterModule = useCallback(async (): Promise<HealthCheck[]> => {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
// Check Content endpoint
|
||||
try {
|
||||
const { response: contentResp, data: contentData } = await apiCall('/v1/writer/content/');
|
||||
|
||||
if (contentResp.ok && contentData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Content List',
|
||||
description: 'Writer content listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${contentData?.count || 0} content items`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Content List',
|
||||
description: 'Writer content listing endpoint',
|
||||
status: 'error',
|
||||
message: contentData?.error || `Failed with ${contentResp.status}`,
|
||||
details: 'Check model field mappings (content_type, content_html, content_structure)',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Content List',
|
||||
description: 'Writer content listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check Tasks endpoint
|
||||
try {
|
||||
const { response: tasksResp, data: tasksData } = await apiCall('/v1/writer/tasks/');
|
||||
|
||||
if (tasksResp.ok && tasksData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Tasks List',
|
||||
description: 'Writer tasks listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${tasksData?.count || 0} tasks`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Tasks List',
|
||||
description: 'Writer tasks listing endpoint',
|
||||
status: 'error',
|
||||
message: tasksData?.error || `Failed with ${tasksResp.status}`,
|
||||
details: 'Check model field mappings (content_type, content_structure, taxonomy_term)',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Tasks List',
|
||||
description: 'Writer tasks listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check Content Validation
|
||||
try {
|
||||
// Get first content ID if available
|
||||
const { data: contentData } = await apiCall('/v1/writer/content/');
|
||||
const firstContentId = contentData?.results?.[0]?.id;
|
||||
|
||||
if (firstContentId) {
|
||||
const { response: validationResp, data: validationData } = await apiCall(
|
||||
`/v1/writer/content/${firstContentId}/validation/`
|
||||
);
|
||||
|
||||
if (validationResp.ok && validationData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Content Validation',
|
||||
description: 'Content validation before publish',
|
||||
status: 'healthy',
|
||||
message: 'Validation endpoint working',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Content Validation',
|
||||
description: 'Content validation before publish',
|
||||
status: 'error',
|
||||
message: validationData?.error || `Failed with ${validationResp.status}`,
|
||||
details: 'Check validation_service.py field references (content_html, content_type)',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Content Validation',
|
||||
description: 'Content validation before publish',
|
||||
status: 'warning',
|
||||
message: 'No content available to test validation',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Content Validation',
|
||||
description: 'Content validation before publish',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}, []);
|
||||
|
||||
// Check Planner module health
|
||||
const checkPlannerModule = useCallback(async (): Promise<HealthCheck[]> => {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
// Check Clusters endpoint
|
||||
try {
|
||||
const { response: clustersResp, data: clustersData } = await apiCall('/v1/planner/clusters/');
|
||||
|
||||
if (clustersResp.ok && clustersData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Clusters List',
|
||||
description: 'Planner clusters listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${clustersData?.count || 0} clusters`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Clusters List',
|
||||
description: 'Planner clusters listing endpoint',
|
||||
status: 'error',
|
||||
message: clustersData?.error || `Failed with ${clustersResp.status}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Clusters List',
|
||||
description: 'Planner clusters listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check Keywords endpoint
|
||||
try {
|
||||
const { response: keywordsResp, data: keywordsData } = await apiCall('/v1/planner/keywords/');
|
||||
|
||||
if (keywordsResp.ok && keywordsData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Keywords List',
|
||||
description: 'Planner keywords listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${keywordsData?.count || 0} keywords`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Keywords List',
|
||||
description: 'Planner keywords listing endpoint',
|
||||
status: 'error',
|
||||
message: keywordsData?.error || `Failed with ${keywordsResp.status}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Keywords List',
|
||||
description: 'Planner keywords listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check Ideas endpoint
|
||||
try {
|
||||
const { response: ideasResp, data: ideasData } = await apiCall('/v1/planner/ideas/');
|
||||
|
||||
if (ideasResp.ok && ideasData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Ideas List',
|
||||
description: 'Planner ideas listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${ideasData?.count || 0} ideas`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Ideas List',
|
||||
description: 'Planner ideas listing endpoint',
|
||||
status: 'error',
|
||||
message: ideasData?.error || `Failed with ${ideasResp.status}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Ideas List',
|
||||
description: 'Planner ideas listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}, []);
|
||||
|
||||
// Check Sites module health
|
||||
const checkSitesModule = useCallback(async (): Promise<HealthCheck[]> => {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
// Check Sites list
|
||||
try {
|
||||
const { response: sitesResp, data: sitesData } = await apiCall('/v1/auth/sites/');
|
||||
|
||||
if (sitesResp.ok && sitesData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Sites List',
|
||||
description: 'Sites listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${sitesData?.count || sitesData?.results?.length || 0} sites`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Sites List',
|
||||
description: 'Sites listing endpoint',
|
||||
status: 'error',
|
||||
message: sitesData?.error || `Failed with ${sitesResp.status}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Sites List',
|
||||
description: 'Sites listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}, []);
|
||||
|
||||
// Check Integration module health
|
||||
const checkIntegrationModule = useCallback(async (): Promise<HealthCheck[]> => {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
// Check Integration content types endpoint
|
||||
try {
|
||||
// Get first site ID if available
|
||||
const { data: sitesData } = await apiCall('/v1/auth/sites/');
|
||||
const firstSiteId = sitesData?.results?.[0]?.id || sitesData?.[0]?.id;
|
||||
|
||||
if (firstSiteId) {
|
||||
const { response: contentTypesResp, data: contentTypesData } = await apiCall(
|
||||
`/v1/integration/integrations/${firstSiteId}/content-types/`
|
||||
);
|
||||
|
||||
if (contentTypesResp.ok && contentTypesData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Content Types Sync',
|
||||
description: 'Integration content types endpoint',
|
||||
status: 'healthy',
|
||||
message: 'Content types endpoint working',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Content Types Sync',
|
||||
description: 'Integration content types endpoint',
|
||||
status: 'error',
|
||||
message: contentTypesData?.error || `Failed with ${contentTypesResp.status}`,
|
||||
details: 'Check integration views field mappings (content_type_map vs entity_type_map)',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Content Types Sync',
|
||||
description: 'Integration content types endpoint',
|
||||
status: 'warning',
|
||||
message: 'No sites available to test integration',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Content Types Sync',
|
||||
description: 'Integration content types endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}, []);
|
||||
|
||||
// Check Content Manager module health (taxonomy relations)
|
||||
const checkContentManagerModule = useCallback(async (): Promise<HealthCheck[]> => {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
// Check Taxonomy endpoint
|
||||
try {
|
||||
const { response: taxonomyResp, data: taxonomyData } = await apiCall('/v1/writer/taxonomy/');
|
||||
|
||||
if (taxonomyResp.ok && taxonomyData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Taxonomy System',
|
||||
description: 'Content taxonomy endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${taxonomyData?.count || 0} taxonomy items`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Taxonomy System',
|
||||
description: 'Content taxonomy endpoint',
|
||||
status: 'error',
|
||||
message: taxonomyData?.error || `Failed with ${taxonomyResp.status}`,
|
||||
details: 'Check ContentTaxonomyRelation through model and field mappings',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Taxonomy System',
|
||||
description: 'Content taxonomy endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}, []);
|
||||
|
||||
// Run all health checks
|
||||
const runAllChecks = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Run schema check first
|
||||
const schemaCheck = await checkDatabaseSchemaMapping();
|
||||
|
||||
// Run module checks in parallel
|
||||
const [writerChecks, plannerChecks, sitesChecks, integrationChecks, contentMgrChecks] = await Promise.all([
|
||||
checkWriterModule(),
|
||||
checkPlannerModule(),
|
||||
checkSitesModule(),
|
||||
checkIntegrationModule(),
|
||||
checkContentManagerModule(),
|
||||
]);
|
||||
|
||||
// Build module health results
|
||||
const moduleHealthResults: ModuleHealth[] = [
|
||||
{
|
||||
module: 'Database Schema',
|
||||
description: 'Critical database field mapping checks',
|
||||
checks: [schemaCheck],
|
||||
},
|
||||
{
|
||||
module: 'Writer Module',
|
||||
description: 'Content creation and task management',
|
||||
checks: writerChecks,
|
||||
},
|
||||
{
|
||||
module: 'Planner Module',
|
||||
description: 'Keyword clustering and content planning',
|
||||
checks: plannerChecks,
|
||||
},
|
||||
{
|
||||
module: 'Sites Module',
|
||||
description: 'Site management and configuration',
|
||||
checks: sitesChecks,
|
||||
},
|
||||
{
|
||||
module: 'Integration Module',
|
||||
description: 'External platform sync (WordPress, etc.)',
|
||||
checks: integrationChecks,
|
||||
},
|
||||
{
|
||||
module: 'Content Manager',
|
||||
description: 'Taxonomy and content organization',
|
||||
checks: contentMgrChecks,
|
||||
},
|
||||
];
|
||||
|
||||
setModuleHealths(moduleHealthResults);
|
||||
} catch (error) {
|
||||
console.error('Failed to run health checks:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [
|
||||
checkDatabaseSchemaMapping,
|
||||
checkWriterModule,
|
||||
checkPlannerModule,
|
||||
checkSitesModule,
|
||||
checkIntegrationModule,
|
||||
checkContentManagerModule,
|
||||
]);
|
||||
|
||||
// Run checks on mount
|
||||
useEffect(() => {
|
||||
runAllChecks();
|
||||
}, [runAllChecks]);
|
||||
|
||||
// Calculate module status
|
||||
const getModuleStatus = (module: ModuleHealth): 'error' | 'warning' | 'healthy' => {
|
||||
const statuses = module.checks.map(c => c.status);
|
||||
if (statuses.some(s => s === 'error')) return 'error';
|
||||
if (statuses.some(s => s === 'warning')) return 'warning';
|
||||
return 'healthy';
|
||||
};
|
||||
|
||||
// Calculate overall health
|
||||
const getOverallHealth = () => {
|
||||
const allStatuses = moduleHealths.flatMap(m => m.checks.map(c => c.status));
|
||||
const total = allStatuses.length;
|
||||
const healthy = allStatuses.filter(s => s === 'healthy').length;
|
||||
const warning = allStatuses.filter(s => s === 'warning').length;
|
||||
const error = allStatuses.filter(s => s === 'error').length;
|
||||
|
||||
let status: 'error' | 'warning' | 'healthy' = 'healthy';
|
||||
if (error > 0) status = 'error';
|
||||
else if (warning > 0) status = 'warning';
|
||||
|
||||
return { total, healthy, warning, error, status, percentage: total > 0 ? Math.round((healthy / total) * 100) : 0 };
|
||||
};
|
||||
|
||||
const overallHealth = getOverallHealth();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Debug Status - IGNY8" description="Module health checks and diagnostics" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-gray-800 dark:text-white/90">Debug Status</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Comprehensive health checks for all modules and recent bug fixes
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={runAllChecks}
|
||||
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 ? 'Running Checks...' : 'Refresh All'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Overall Health Summary */}
|
||||
<ComponentCard
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Overall System Health</span>
|
||||
<span className={`text-lg ${getStatusColor(overallHealth.status)}`}>
|
||||
{getStatusIcon(overallHealth.status)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
desc={
|
||||
overallHealth.status === 'error'
|
||||
? `${overallHealth.error} critical issue${overallHealth.error !== 1 ? 's' : ''} detected`
|
||||
: overallHealth.status === 'warning'
|
||||
? `${overallHealth.warning} warning${overallHealth.warning !== 1 ? 's' : ''} detected`
|
||||
: 'All systems operational'
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Health Percentage */}
|
||||
<div className="text-center">
|
||||
<div className={`text-5xl font-bold ${getStatusColor(overallHealth.status)}`}>
|
||||
{overallHealth.percentage}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||
{overallHealth.healthy} of {overallHealth.total} checks passed
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Health Breakdown */}
|
||||
<div className="grid grid-cols-3 gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="text-center">
|
||||
<div className={`text-2xl font-semibold ${getStatusColor('healthy')}`}>
|
||||
{overallHealth.healthy}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">Healthy</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={`text-2xl font-semibold ${getStatusColor('warning')}`}>
|
||||
{overallHealth.warning}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">Warnings</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={`text-2xl font-semibold ${getStatusColor('error')}`}>
|
||||
{overallHealth.error}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">Errors</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
{/* Module Health Cards */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{moduleHealths.map((moduleHealth, index) => {
|
||||
const moduleStatus = getModuleStatus(moduleHealth);
|
||||
const healthyCount = moduleHealth.checks.filter(c => c.status === 'healthy').length;
|
||||
const totalCount = moduleHealth.checks.length;
|
||||
|
||||
return (
|
||||
<ComponentCard
|
||||
key={index}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{moduleHealth.module}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${getStatusBadge(moduleStatus)}`}>
|
||||
{getStatusIcon(moduleStatus)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
desc={
|
||||
moduleStatus === 'error'
|
||||
? `Issues detected - ${healthyCount}/${totalCount} checks passed`
|
||||
: moduleStatus === 'warning'
|
||||
? `Warnings detected - ${healthyCount}/${totalCount} checks passed`
|
||||
: `All ${totalCount} checks passed`
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
{moduleHealth.description}
|
||||
</p>
|
||||
|
||||
{/* Health Checks List */}
|
||||
{moduleHealth.checks.map((check, checkIndex) => (
|
||||
<div
|
||||
key={checkIndex}
|
||||
className={`p-3 rounded-lg border ${
|
||||
check.status === 'healthy'
|
||||
? 'border-green-200 dark:border-green-900/50 bg-green-50 dark:bg-green-900/20'
|
||||
: check.status === 'warning'
|
||||
? 'border-yellow-200 dark:border-yellow-900/50 bg-yellow-50 dark:bg-yellow-900/20'
|
||||
: check.status === 'error'
|
||||
? 'border-red-200 dark:border-red-900/50 bg-red-50 dark:bg-red-900/20'
|
||||
: 'border-blue-200 dark:border-blue-900/50 bg-blue-50 dark:bg-blue-900/20'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className={`text-lg ${getStatusColor(check.status)} flex-shrink-0`}>
|
||||
{getStatusIcon(check.status)}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
{check.name}
|
||||
</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${getStatusBadge(check.status)}`}>
|
||||
{check.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
||||
{check.description}
|
||||
</p>
|
||||
{check.message && (
|
||||
<p className={`text-sm ${getStatusColor(check.status)} font-medium`}>
|
||||
{check.message}
|
||||
</p>
|
||||
)}
|
||||
{check.details && (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1 italic">
|
||||
💡 {check.details}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ComponentCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Help Section */}
|
||||
<ComponentCard
|
||||
title="Troubleshooting Guide"
|
||||
desc="Common issues and solutions"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-900/50">
|
||||
<h3 className="font-medium text-gray-800 dark:text-white/90 mb-2">
|
||||
Database Schema Mapping Errors
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
If you see errors about missing fields like <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">entity_type</code>,
|
||||
<code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs ml-1">cluster_role</code>, or
|
||||
<code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs ml-1">html_content</code>:
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-400 list-disc list-inside space-y-1">
|
||||
<li>Check that model fields have correct <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">db_column</code> attributes</li>
|
||||
<li>Verify database columns exist with <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">SELECT column_name FROM information_schema.columns</code></li>
|
||||
<li>Review <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">DATABASE_SCHEMA_FIELD_MAPPING_GUIDE.md</code> in repo root</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-900/50">
|
||||
<h3 className="font-medium text-gray-800 dark:text-white/90 mb-2">
|
||||
Field Reference Errors in Code
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
If API endpoints return 500 errors with AttributeError or similar:
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-400 list-disc list-inside space-y-1">
|
||||
<li>Search codebase for old field names: <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">entity_type</code>, <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">cluster_role</code>, <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">html_content</code></li>
|
||||
<li>Replace with new names: <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">content_type</code>, <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">content_structure</code>, <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">content_html</code></li>
|
||||
<li>Check views, services, and serializers in writer/planner/integration modules</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-900/50">
|
||||
<h3 className="font-medium text-gray-800 dark:text-white/90 mb-2">
|
||||
All Checks Passing?
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Great! Your system is healthy. This page will help you quickly diagnose issues if they appear in the future.
|
||||
Bookmark this page and check it first when troubleshooting module-specific problems.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user