977 lines
38 KiB
TypeScript
977 lines
38 KiB
TypeScript
import React, { useState, useEffect, useCallback, useRef } from "react";
|
|
import { AlertTriangle } from "lucide-react";
|
|
import PageMeta from "../../components/common/PageMeta";
|
|
import ComponentCard from "../../components/common/ComponentCard";
|
|
import SiteAndSectorSelector from "../../components/common/SiteAndSectorSelector";
|
|
import WordPressIntegrationDebug from "./WordPressIntegrationDebug";
|
|
import { API_BASE_URL, fetchAPI } from "../../services/api";
|
|
import { useSiteStore } from "../../store/siteStore";
|
|
import { useToast } from "../../components/ui/toast/ToastContainer";
|
|
|
|
interface HealthCheck {
|
|
name: string;
|
|
description: string;
|
|
status: 'healthy' | 'warning' | 'error' | 'checking';
|
|
message?: string;
|
|
details?: string;
|
|
lastChecked?: string;
|
|
step?: string;
|
|
payloadKeys?: string[];
|
|
responseCode?: number;
|
|
}
|
|
|
|
interface ModuleHealth {
|
|
module: string;
|
|
description: string;
|
|
checks: HealthCheck[];
|
|
}
|
|
|
|
interface SyncEvent {
|
|
id: string;
|
|
timestamp: string;
|
|
direction: '📤 IGNY8 → WP' | '📥 WP → IGNY8';
|
|
trigger: string;
|
|
contentId?: number;
|
|
taskId?: number;
|
|
status: 'success' | 'partial' | 'failed';
|
|
steps: SyncEventStep[];
|
|
payload?: any;
|
|
response?: any;
|
|
}
|
|
|
|
interface SyncEventStep {
|
|
step: string;
|
|
file: string;
|
|
status: 'success' | 'warning' | 'failed' | 'skipped';
|
|
details: string;
|
|
error?: string;
|
|
duration?: number;
|
|
}
|
|
|
|
interface DataSyncValidation {
|
|
field: string;
|
|
sentByIgny8: boolean;
|
|
receivedByWP: boolean;
|
|
storedInWP: boolean;
|
|
verifiedInWPPost: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
interface IntegrationHealth {
|
|
overall: 'healthy' | 'warning' | 'error';
|
|
lastSyncIgny8ToWP?: string;
|
|
lastSyncWPToIgny8?: string;
|
|
lastSiteMetadataCheck?: string;
|
|
wpApiReachable: boolean;
|
|
wpStatusEndpoint: boolean;
|
|
wpMetadataEndpoint: boolean;
|
|
apiKeyValid: boolean;
|
|
jwtTokenValid: boolean;
|
|
}
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'healthy':
|
|
case 'success': return 'text-green-600 dark:text-green-400';
|
|
case 'warning':
|
|
case 'partial': return 'text-yellow-600 dark:text-yellow-400';
|
|
case 'error':
|
|
case 'failed': return 'text-red-600 dark:text-red-400';
|
|
case 'checking': return 'text-blue-600 dark:text-blue-400';
|
|
case 'skipped': return 'text-gray-600 dark:text-gray-400';
|
|
default: return 'text-gray-600 dark:text-gray-400';
|
|
}
|
|
};
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
switch (status) {
|
|
case 'healthy':
|
|
case 'success': return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400';
|
|
case 'warning':
|
|
case 'partial': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400';
|
|
case 'error':
|
|
case 'failed': 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';
|
|
case 'skipped': return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-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':
|
|
case 'success': return '✓';
|
|
case 'warning':
|
|
case 'partial': return '⚠';
|
|
case 'error':
|
|
case 'failed': return '✗';
|
|
case 'checking': return '⟳';
|
|
case 'skipped': return '—';
|
|
default: return '?';
|
|
}
|
|
};
|
|
|
|
export default function DebugStatus() {
|
|
const { activeSite } = useSiteStore();
|
|
const toast = useToast();
|
|
|
|
// Tab navigation state
|
|
const [activeTab, setActiveTab] = useState<'system-health' | 'wp-integration'>('system-health');
|
|
|
|
// Data state
|
|
const [loading, setLoading] = useState(false);
|
|
const [moduleHealths, setModuleHealths] = useState<ModuleHealth[]>([]);
|
|
const [debugEnabled, setDebugEnabled] = useState(false);
|
|
|
|
// Helper to call API endpoints
|
|
const apiCall = useCallback(async (endpoint: string, options: RequestInit = {}) => {
|
|
try {
|
|
console.log(`[DEBUG] Calling API: ${endpoint}`);
|
|
// fetchAPI returns parsed JSON data directly (not Response object)
|
|
const data = await fetchAPI(endpoint, options);
|
|
console.log(`[DEBUG] Success from ${endpoint}:`, data);
|
|
// Return mock response object with data
|
|
return {
|
|
response: { ok: true, status: 200, statusText: 'OK' } as Response,
|
|
data
|
|
};
|
|
} catch (error: any) {
|
|
console.error(`[DEBUG] API call failed for ${endpoint}:`, error);
|
|
// fetchAPI throws errors for non-OK responses - extract error data
|
|
const errorData = error.response || {
|
|
detail: error.message?.replace(/^API Error.*?:\s*/, '') || 'Request failed'
|
|
};
|
|
const status = error.status || 500;
|
|
console.log(`[DEBUG] Error from ${endpoint}:`, { status, errorData });
|
|
return {
|
|
response: { ok: false, status, statusText: error.message } as Response,
|
|
data: errorData
|
|
};
|
|
}
|
|
}, []);
|
|
|
|
// Check database schema field mappings (the issues we just fixed)
|
|
const checkDatabaseSchemaMapping = useCallback(async (): Promise<HealthCheck> => {
|
|
if (!activeSite) {
|
|
return {
|
|
name: 'Database Schema Mapping',
|
|
description: 'Checks if model field names map correctly to database columns',
|
|
status: 'warning',
|
|
message: 'No site selected',
|
|
details: 'Please select a site to run health checks',
|
|
lastChecked: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
try {
|
|
// Test Writer Content endpoint (was failing with entity_type error)
|
|
const { response: contentResp, data: contentData } = await apiCall(`/v1/writer/content/?site=${activeSite.id}`);
|
|
|
|
if (!contentResp.ok) {
|
|
const errorMsg = contentData?.detail || contentData?.error || contentData?.message || `HTTP ${contentResp.status}`;
|
|
return {
|
|
name: 'Database Schema Mapping',
|
|
description: 'Checks if model field names map correctly to database columns',
|
|
status: 'error',
|
|
message: errorMsg,
|
|
details: 'Cannot verify schema - API endpoint failed',
|
|
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 for ${activeSite.name}. 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(),
|
|
};
|
|
}
|
|
}, [apiCall, activeSite]);
|
|
|
|
// Check Writer module health
|
|
const checkWriterModule = useCallback(async (): Promise<HealthCheck[]> => {
|
|
const checks: HealthCheck[] = [];
|
|
|
|
if (!activeSite) {
|
|
checks.push({
|
|
name: 'Content List',
|
|
description: 'Writer content listing endpoint',
|
|
status: 'warning',
|
|
message: 'No site selected',
|
|
lastChecked: new Date().toISOString(),
|
|
});
|
|
return checks;
|
|
}
|
|
|
|
// Check Content endpoint
|
|
try {
|
|
const { response: contentResp, data: contentData } = await apiCall(`/v1/writer/content/?site=${activeSite.id}`);
|
|
|
|
if (contentResp && contentResp.ok) {
|
|
checks.push({
|
|
name: 'Content List',
|
|
description: 'Writer content listing endpoint',
|
|
status: 'healthy',
|
|
message: `Found ${contentData?.count || contentData?.results?.length || 0} content items for ${activeSite.name}`,
|
|
lastChecked: new Date().toISOString(),
|
|
});
|
|
} else {
|
|
const errorMsg = contentData?.detail || contentData?.error || contentData?.message || (contentResp ? `HTTP ${contentResp.status}` : 'Request failed');
|
|
checks.push({
|
|
name: 'Content List',
|
|
description: 'Writer content listing endpoint',
|
|
status: 'error',
|
|
message: errorMsg,
|
|
details: 'Check authentication and endpoint availability',
|
|
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/?site=${activeSite.id}`);
|
|
|
|
if (tasksResp && tasksResp.ok) {
|
|
checks.push({
|
|
name: 'Tasks List',
|
|
description: 'Writer tasks listing endpoint',
|
|
status: 'healthy',
|
|
message: `Found ${tasksData?.count || tasksData?.results?.length || 0} tasks for ${activeSite.name}`,
|
|
lastChecked: new Date().toISOString(),
|
|
});
|
|
} else {
|
|
const errorMsg = tasksData?.detail || tasksData?.error || tasksData?.message || (tasksResp ? `HTTP ${tasksResp.status}` : 'Request failed');
|
|
checks.push({
|
|
name: 'Tasks List',
|
|
description: 'Writer tasks listing endpoint',
|
|
status: 'error',
|
|
message: errorMsg,
|
|
details: 'Check authentication and endpoint availability',
|
|
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/?site=${activeSite.id}`);
|
|
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;
|
|
}, [apiCall, activeSite]);
|
|
|
|
// Check Planner module health
|
|
const checkPlannerModule = useCallback(async (): Promise<HealthCheck[]> => {
|
|
const checks: HealthCheck[] = [];
|
|
|
|
if (!activeSite) {
|
|
checks.push({
|
|
name: 'Keyword Clusters',
|
|
description: 'Planner keyword clustering endpoint',
|
|
status: 'warning',
|
|
message: 'No site selected',
|
|
lastChecked: new Date().toISOString(),
|
|
});
|
|
return checks;
|
|
}
|
|
|
|
// Check Keyword Clusters endpoint
|
|
try {
|
|
const { response: clustersResp, data: clustersData } = await apiCall(`/v1/planner/clusters/?site=${activeSite.id}`);
|
|
|
|
if (clustersResp && clustersResp.ok) {
|
|
checks.push({
|
|
name: 'Clusters List',
|
|
description: 'Planner clusters listing endpoint',
|
|
status: 'healthy',
|
|
message: `Found ${clustersData?.count || clustersData?.results?.length || 0} clusters for ${activeSite.name}`,
|
|
lastChecked: new Date().toISOString(),
|
|
});
|
|
} else {
|
|
const errorMsg = clustersData?.detail || clustersData?.error || clustersData?.message || (clustersResp ? `HTTP ${clustersResp.status}` : 'Request failed');
|
|
checks.push({
|
|
name: 'Clusters List',
|
|
description: 'Planner clusters listing endpoint',
|
|
status: 'error',
|
|
message: errorMsg,
|
|
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/?site=${activeSite.id}`);
|
|
|
|
if (keywordsResp.ok && keywordsData?.success !== false) {
|
|
checks.push({
|
|
name: 'Keywords List',
|
|
description: 'Planner keywords listing endpoint',
|
|
status: 'healthy',
|
|
message: `Found ${keywordsData?.count || 0} keywords for ${activeSite.name}`,
|
|
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/?site=${activeSite.id}`);
|
|
|
|
if (ideasResp.ok && ideasData?.success !== false) {
|
|
checks.push({
|
|
name: 'Ideas List',
|
|
description: 'Planner ideas listing endpoint',
|
|
status: 'healthy',
|
|
message: `Found ${ideasData?.count || 0} ideas for ${activeSite.name}`,
|
|
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;
|
|
}, [apiCall, activeSite]);
|
|
|
|
// 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 && sitesResp.ok) {
|
|
checks.push({
|
|
name: 'Sites List',
|
|
description: 'Sites listing endpoint',
|
|
status: 'healthy',
|
|
message: `Found ${sitesData?.count || sitesData?.results?.length || sitesData?.length || 0} sites`,
|
|
lastChecked: new Date().toISOString(),
|
|
});
|
|
} else {
|
|
const errorMsg = sitesData?.detail || sitesData?.error || sitesData?.message || (sitesResp ? `HTTP ${sitesResp.status}` : 'Request failed');
|
|
checks.push({
|
|
name: 'Sites List',
|
|
description: 'Sites listing endpoint',
|
|
status: 'error',
|
|
message: errorMsg,
|
|
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;
|
|
}, [apiCall]);
|
|
|
|
// Check Integration module health
|
|
const checkIntegrationModule = useCallback(async (): Promise<HealthCheck[]> => {
|
|
const checks: HealthCheck[] = [];
|
|
|
|
if (!activeSite) {
|
|
checks.push({
|
|
name: 'Content Types Sync',
|
|
description: 'Integration content types endpoint',
|
|
status: 'warning',
|
|
message: 'No site selected',
|
|
lastChecked: new Date().toISOString(),
|
|
});
|
|
return checks;
|
|
}
|
|
|
|
// Check Integration content types endpoint
|
|
try {
|
|
// First get the integration for this site
|
|
const { response: intResp, data: intData } = await apiCall(
|
|
`/v1/integration/integrations/?site_id=${activeSite.id}`
|
|
);
|
|
|
|
if (!intResp.ok || !intData || (Array.isArray(intData) && intData.length === 0) || (intData.results && intData.results.length === 0)) {
|
|
checks.push({
|
|
name: 'Content Types Sync',
|
|
description: 'Integration content types endpoint',
|
|
status: 'warning',
|
|
message: 'No WordPress integration configured',
|
|
details: 'Add a WordPress integration in Settings > Integrations',
|
|
lastChecked: new Date().toISOString(),
|
|
});
|
|
} else {
|
|
// Extract integration ID from response
|
|
const integration = Array.isArray(intData) ? intData[0] : (intData.results ? intData.results[0] : intData);
|
|
const integrationId = integration?.id;
|
|
|
|
if (!integrationId) {
|
|
checks.push({
|
|
name: 'Content Types Sync',
|
|
description: 'Integration content types endpoint',
|
|
status: 'error',
|
|
message: 'Invalid integration data',
|
|
lastChecked: new Date().toISOString(),
|
|
});
|
|
} else {
|
|
// Now check content types for this integration
|
|
const { response: contentTypesResp, data: contentTypesData } = await apiCall(
|
|
`/v1/integration/integrations/${integrationId}/content-types/`
|
|
);
|
|
|
|
if (contentTypesResp.ok && contentTypesData?.success !== false) {
|
|
checks.push({
|
|
name: 'Content Types Sync',
|
|
description: 'Integration content types endpoint',
|
|
status: 'healthy',
|
|
message: `Content types synced for ${activeSite.name}`,
|
|
lastChecked: new Date().toISOString(),
|
|
});
|
|
} else {
|
|
checks.push({
|
|
name: 'Content Types Sync',
|
|
description: 'Integration content types endpoint',
|
|
status: 'error',
|
|
message: contentTypesData?.detail || contentTypesData?.error || `Failed with ${contentTypesResp.status}`,
|
|
details: 'Check integration views field mappings (content_type_map vs entity_type_map)',
|
|
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;
|
|
}, [apiCall, activeSite]);
|
|
|
|
// 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] = await Promise.all([
|
|
checkWriterModule(),
|
|
checkPlannerModule(),
|
|
checkSitesModule(),
|
|
checkIntegrationModule(),
|
|
]);
|
|
|
|
// 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,
|
|
},
|
|
];
|
|
|
|
setModuleHealths(moduleHealthResults);
|
|
} catch (error) {
|
|
console.error('Failed to run health checks:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [
|
|
apiCall,
|
|
checkDatabaseSchemaMapping,
|
|
checkWriterModule,
|
|
checkPlannerModule,
|
|
checkSitesModule,
|
|
checkIntegrationModule,
|
|
]);
|
|
|
|
// Run checks on mount and when site changes
|
|
useEffect(() => {
|
|
if (!debugEnabled || !activeSite) {
|
|
setModuleHealths([]);
|
|
return;
|
|
}
|
|
runAllChecks();
|
|
}, [runAllChecks, debugEnabled, activeSite]);
|
|
|
|
// 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>
|
|
|
|
{/* Site Selector */}
|
|
<div className="bg-white dark:bg-gray-800 shadow rounded-lg p-4">
|
|
<SiteAndSectorSelector hideSectorSelector={true} />
|
|
</div>
|
|
|
|
{/* No Site Selected Warning */}
|
|
{!activeSite && (
|
|
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-900/50 rounded-lg p-4">
|
|
<div className="flex items-start space-x-3">
|
|
<AlertTriangle className="h-5 w-5 text-yellow-500 mt-0.5" />
|
|
<div>
|
|
<p className="text-sm text-yellow-800 dark:text-yellow-200 font-medium">No Site Selected</p>
|
|
<p className="text-xs text-yellow-600 dark:text-yellow-300 mt-1">
|
|
Please select a site above to run health checks and view debug information.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Debug Toggle */}
|
|
{activeSite && (
|
|
<div className="bg-white dark:bg-gray-800 shadow rounded-lg p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="font-medium text-gray-900 dark:text-white">System Health Debug</h3>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
Enable debug mode to run health checks for {activeSite.name}
|
|
</p>
|
|
</div>
|
|
<label className="flex items-center space-x-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={debugEnabled}
|
|
onChange={(e) => setDebugEnabled(e.target.checked)}
|
|
className="rounded"
|
|
/>
|
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{debugEnabled ? 'Enabled' : 'Disabled'}
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Tab Content */}
|
|
{debugEnabled && activeSite ? (
|
|
<>
|
|
{/* Tab Navigation */}
|
|
<div className="bg-white dark:bg-gray-800 shadow rounded-lg">
|
|
<div className="border-b border-gray-200 dark:border-gray-700">
|
|
<nav className="flex space-x-8 px-6">
|
|
<button
|
|
onClick={() => setActiveTab('system-health')}
|
|
className={`py-4 px-2 border-b-2 font-medium text-sm transition-colors ${
|
|
activeTab === 'system-health'
|
|
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
|
}`}
|
|
>
|
|
System Health
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('wp-integration')}
|
|
className={`py-4 px-2 border-b-2 font-medium text-sm transition-colors ${
|
|
activeTab === 'wp-integration'
|
|
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
|
}`}
|
|
>
|
|
IGNY8 ↔ WordPress
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
{activeTab === 'system-health' ? (
|
|
<div className="space-y-6">
|
|
{/* 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">content_type</code>,
|
|
<code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs ml-1">content_structure</code>, or
|
|
<code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs ml-1">content_html</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 match database column names</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>All field names now match database (no db_column mappings)</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>All field names now standardized: <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>Old names removed: entity_type, site_entity_type, cluster_role, html_content</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>
|
|
) : (
|
|
// WordPress Integration Debug Tab
|
|
<WordPressIntegrationDebug />
|
|
)}
|
|
</>
|
|
) : (
|
|
<div className="bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700 rounded-lg p-6 text-center">
|
|
<AlertTriangle className="h-8 w-8 text-gray-400 mx-auto mb-2" />
|
|
<p className="text-gray-600 dark:text-gray-400">
|
|
{activeSite
|
|
? 'Enable debug mode above to view system health checks'
|
|
: 'Select a site and enable debug mode to view system health checks'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|