diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0c722110..f72f89de 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -79,7 +79,7 @@ const ModuleSettings = lazy(() => import("./pages/Settings/Modules")); const AISettings = lazy(() => import("./pages/Settings/AI")); const Plans = lazy(() => import("./pages/Settings/Plans")); const Industries = lazy(() => import("./pages/Settings/Industries")); -const Status = lazy(() => import("./pages/Settings/Status")); +const MasterStatus = lazy(() => import("./pages/Settings/MasterStatus")); const ApiMonitor = lazy(() => import("./pages/Settings/ApiMonitor")); const DebugStatus = lazy(() => import("./pages/Settings/DebugStatus")); const Integration = lazy(() => import("./pages/Settings/Integration")); @@ -427,7 +427,7 @@ export default function App() { } /> - + } /> ([]); + const [sitesLoading, setSitesLoading] = useState(true); + const siteButtonRef = useRef(null); + const noSitesAvailable = !sitesLoading && sites.length === 0; + + // Load sites + useEffect(() => { + if (isAuthenticated && user) { + refreshUser().catch((error) => { + console.debug('DebugSiteSelector: Failed to refresh user (non-critical):', error); + }); + } + }, [isAuthenticated]); + + useEffect(() => { + loadSites(); + if (!activeSite) { + loadActiveSite(); + } + }, [user?.account?.id]); + + const loadSites = async () => { + try { + setSitesLoading(true); + const response = await fetchSites(); + const activeSites = (response.results || []).filter(site => site.is_active); + setSites(activeSites); + } catch (error: any) { + console.error('Failed to load sites:', error); + toast.error(`Failed to load sites: ${error.message}`); + } finally { + setSitesLoading(false); + } + }; + + const handleSiteSelect = async (siteId: number) => { + try { + await apiSetActiveSite(siteId); + const selectedSite = sites.find(s => s.id === siteId); + if (selectedSite) { + setActiveSite(selectedSite); + toast.success(`Switched to "${selectedSite.name}"`); + } + setSitesOpen(false); + } catch (error: any) { + toast.error(`Failed to switch site: ${error.message}`); + } + }; + + const handleCreateSite = () => navigate('/sites'); + + if (sitesLoading && sites.length === 0) { + return ( +
+ Loading sites... +
+ ); + } + + if (noSitesAvailable) { + return ( +
+ No active sites yet. Create a site to unlock planner and writer modules. + +
+ ); + } + + return ( +
+ + + setSitesOpen(false)} + anchorRef={siteButtonRef} + placement="bottom-left" + className="w-64 p-2" + > + {sites.map((site) => ( + handleSiteSelect(site.id)} + className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${ + activeSite?.id === site.id + ? "bg-brand-50 text-brand-700 dark:bg-brand-500/20 dark:text-brand-300" + : "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300" + }`} + > + {site.name} + {activeSite?.id === site.id && ( + + + + )} + + ))} + +
+ ); +} diff --git a/frontend/src/components/common/SiteSelector.tsx b/frontend/src/components/common/SiteSelector.tsx new file mode 100644 index 00000000..0e50c79e --- /dev/null +++ b/frontend/src/components/common/SiteSelector.tsx @@ -0,0 +1,320 @@ +/** + * Combined Site and Sector Selector Component + * Displays both site switcher and sector selector side by side with accent colors + */ +import { useState, useEffect, useRef } from 'react'; +import { useNavigate } from 'react-router'; +import { Dropdown } from '../ui/dropdown/Dropdown'; +import { DropdownItem } from '../ui/dropdown/DropdownItem'; +import { fetchSites, Site, setActiveSite as apiSetActiveSite } from '../../services/api'; +import { useToast } from '../ui/toast/ToastContainer'; +import { useSiteStore } from '../../store/siteStore'; +import { useSectorStore } from '../../store/sectorStore'; +import { useAuthStore } from '../../store/authStore'; +import Button from '../ui/button/Button'; + +interface SiteAndSectorSelectorProps { + hideSectorSelector?: boolean; +} + +export default function SiteAndSectorSelector({ + hideSectorSelector = false, +}: SiteAndSectorSelectorProps) { + const toast = useToast(); + const navigate = useNavigate(); + const { activeSite, setActiveSite, loadActiveSite } = useSiteStore(); + const { activeSector, sectors, setActiveSector, loading: sectorsLoading } = useSectorStore(); + const { user, refreshUser, isAuthenticated } = useAuthStore(); + + // Site switcher state + const [sitesOpen, setSitesOpen] = useState(false); + const [sites, setSites] = useState([]); + const [sitesLoading, setSitesLoading] = useState(true); + const siteButtonRef = useRef(null); + + // Sector selector state + const [sectorsOpen, setSectorsOpen] = useState(false); + const sectorButtonRef = useRef(null); + const noSitesAvailable = !sitesLoading && sites.length === 0; + + // Load sites + useEffect(() => { + if (isAuthenticated && user) { + refreshUser().catch((error) => { + console.debug('SiteAndSectorSelector: Failed to refresh user (non-critical):', error); + }); + } + }, [isAuthenticated]); + + useEffect(() => { + loadSites(); + if (!activeSite) { + loadActiveSite(); + } + }, [user?.account?.id]); + + const loadSites = async () => { + try { + setSitesLoading(true); + const response = await fetchSites(); + const activeSites = (response.results || []).filter(site => site.is_active); + setSites(activeSites); + } catch (error: any) { + console.error('Failed to load sites:', error); + toast.error(`Failed to load sites: ${error.message}`); + } finally { + setSitesLoading(false); + } + }; + + const handleSiteSelect = async (siteId: number) => { + try { + await apiSetActiveSite(siteId); + const selectedSite = sites.find(s => s.id === siteId); + if (selectedSite) { + setActiveSite(selectedSite); + toast.success(`Switched to "${selectedSite.name}"`); + } + setSitesOpen(false); + } catch (error: any) { + toast.error(`Failed to switch site: ${error.message}`); + } + }; + + const handleSectorSelect = (sectorId: number | null) => { + if (sectorId === null) { + setActiveSector(null); + setSectorsOpen(false); + } else { + const sector = sectors.find(s => s.id === sectorId); + if (sector) { + setActiveSector(sector); + setSectorsOpen(false); + } + } + }; + + const handleCreateSite = () => navigate('/sites'); + + if (sitesLoading && sites.length === 0) { + return ( +
+ Loading sites... +
+ ); + } + + if (noSitesAvailable) { + return ( +
+ No active sites yet. Create a site to unlock planner and writer modules. + +
+ ); + } + + return ( +
+ {/* Site Switcher */} +
+ + + setSitesOpen(false)} + anchorRef={siteButtonRef} + placement="bottom-left" + className="w-64 p-2" + > + {sites.map((site) => ( + handleSiteSelect(site.id)} + className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${ + activeSite?.id === site.id + ? "bg-brand-50 text-brand-700 dark:bg-brand-500/20 dark:text-brand-300" + : "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300" + }`} + > + {site.name} + {activeSite?.id === site.id && ( + + + + )} + + ))} + +
+ + {/* Sector Selector */} + {!hideSectorSelector && ( +
+ {!activeSite ? ( +
+ Select a site to choose sectors +
+ ) : sectorsLoading ? ( +
+ Loading sectors... +
+ ) : sectors.length === 0 ? ( +
+ This site has no sectors yet. +
+ ) : ( + <> + + + setSectorsOpen(false)} + anchorRef={sectorButtonRef} + placement="bottom-right" + className="w-64 p-2 overflow-y-auto max-h-[300px]" + > + {/* "All Sectors" option */} + handleSectorSelect(null)} + className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${ + !activeSector + ? "bg-brand-50 text-brand-700 dark:bg-brand-500/20 dark:text-brand-300" + : "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300" + }`} + > + All Sectors + {!activeSector && ( + + + + )} + + {sectors.map((sector) => ( + handleSectorSelect(sector.id)} + className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${ + activeSector?.id === sector.id + ? "bg-brand-50 text-brand-700 dark:bg-brand-500/20 dark:text-brand-300" + : "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300" + }`} + > + {sector.name} + {activeSector?.id === sector.id && ( + + + + )} + + ))} + + + )} +
+ )} +
+ ); +} + diff --git a/frontend/src/pages/Settings/DebugStatus.tsx b/frontend/src/pages/Settings/DebugStatus.tsx index 53dbcdab..9c4203db 100644 --- a/frontend/src/pages/Settings/DebugStatus.tsx +++ b/frontend/src/pages/Settings/DebugStatus.tsx @@ -2,7 +2,7 @@ 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 DebugSiteSelector from "../../components/common/DebugSiteSelector"; import WordPressIntegrationDebug from "./WordPressIntegrationDebug"; import { API_BASE_URL, fetchAPI } from "../../services/api"; import { useSiteStore } from "../../store/siteStore"; @@ -694,9 +694,24 @@ export default function DebugStatus() { - {/* Site Selector */} + {/* Site Selector & Debug Toggle Combined */}
- +
+ + {activeSite && ( + + )} +
{/* No Site Selected Warning */} @@ -714,31 +729,6 @@ export default function DebugStatus() { )} - {/* Debug Toggle */} - {activeSite && ( -
-
-
-

System Health Debug

-

- Enable debug mode to run health checks for {activeSite.name} -

-
- -
-
- )} - {/* Tab Content */} {debugEnabled && activeSite ? ( <> diff --git a/frontend/src/pages/Settings/MasterStatus.tsx b/frontend/src/pages/Settings/MasterStatus.tsx new file mode 100644 index 00000000..1349daa5 --- /dev/null +++ b/frontend/src/pages/Settings/MasterStatus.tsx @@ -0,0 +1,548 @@ +import { useState, useEffect, useCallback } from 'react'; +import { + Activity, + Zap, + Database, + Server, + Workflow, + Globe, + CheckCircle, + AlertTriangle, + XCircle, + RefreshCw, + TrendingUp, + Clock, + Cpu, + HardDrive, + MemoryStick +} from 'lucide-react'; +import PageMeta from '../../components/common/PageMeta'; +import DebugSiteSelector from '../../components/common/DebugSiteSelector'; +import { fetchAPI } from '../../services/api'; +import { useSiteStore } from '../../store/siteStore'; + +// Types +interface SystemMetrics { + cpu: { usage_percent: number; cores: number; status: string }; + memory: { total_gb: number; used_gb: number; usage_percent: number; status: string }; + disk: { total_gb: number; used_gb: number; usage_percent: number; status: string }; +} + +interface ServiceHealth { + database: { status: string; connected: boolean }; + redis: { status: string; connected: boolean }; + celery: { status: string; worker_count: number }; +} + +interface ApiGroupHealth { + name: string; + total: number; + healthy: number; + warning: number; + error: number; + percentage: number; + status: 'healthy' | 'warning' | 'error'; +} + +interface WorkflowHealth { + name: string; + steps: { name: string; status: 'healthy' | 'warning' | 'error'; message?: string }[]; + overall: 'healthy' | 'warning' | 'error'; +} + +interface IntegrationHealth { + platform: string; + connected: boolean; + last_sync: string | null; + sync_enabled: boolean; + plugin_active: boolean; + status: 'healthy' | 'warning' | 'error'; +} + +export default function MasterStatus() { + const { activeSite } = useSiteStore(); + const [loading, setLoading] = useState(true); + const [lastUpdate, setLastUpdate] = useState(new Date()); + + // System metrics + const [systemMetrics, setSystemMetrics] = useState(null); + const [serviceHealth, setServiceHealth] = useState(null); + + // API health + const [apiHealth, setApiHealth] = useState([]); + + // Workflow health (keywords → clusters → ideas → tasks → content → publish) + const [workflowHealth, setWorkflowHealth] = useState([]); + + // Integration health + const [integrationHealth, setIntegrationHealth] = useState(null); + + // Fetch system metrics + const fetchSystemMetrics = useCallback(async () => { + try { + const data = await fetchAPI('/v1/system/status/'); + setSystemMetrics(data.system); + setServiceHealth({ + database: data.database, + redis: data.redis, + celery: data.celery, + }); + } catch (error) { + console.error('Failed to fetch system metrics:', error); + } + }, []); + + // Fetch API health (aggregated from API monitor) + const fetchApiHealth = useCallback(async () => { + const groups = [ + { name: 'Auth & User', endpoints: ['/v1/auth/me/', '/v1/auth/sites/', '/v1/auth/accounts/'] }, + { name: 'Planner', endpoints: ['/v1/planner/keywords/', '/v1/planner/clusters/', '/v1/planner/ideas/'] }, + { name: 'Writer', endpoints: ['/v1/writer/tasks/', '/v1/writer/content/', '/v1/writer/images/'] }, + { name: 'Integration', endpoints: ['/v1/integration/integrations/'] }, + ]; + + const healthChecks: ApiGroupHealth[] = []; + + for (const group of groups) { + let healthy = 0; + let warning = 0; + let error = 0; + + for (const endpoint of group.endpoints) { + try { + const startTime = Date.now(); + await fetchAPI(endpoint + (activeSite ? `?site=${activeSite.id}` : '')); + const responseTime = Date.now() - startTime; + + if (responseTime < 1000) healthy++; + else if (responseTime < 3000) warning++; + else error++; + } catch (e) { + error++; + } + } + + const total = group.endpoints.length; + const percentage = Math.round((healthy / total) * 100); + + healthChecks.push({ + name: group.name, + total, + healthy, + warning, + error, + percentage, + status: error > 0 ? 'error' : warning > 0 ? 'warning' : 'healthy', + }); + } + + setApiHealth(healthChecks); + }, [activeSite]); + + // Check workflow health (end-to-end pipeline) + const checkWorkflowHealth = useCallback(async () => { + if (!activeSite) { + setWorkflowHealth([]); + return; + } + + const workflows: WorkflowHealth[] = []; + + // Content Generation Workflow + try { + const steps = []; + + // Step 1: Keywords exist + const keywords = await fetchAPI(`/v1/planner/keywords/?site=${activeSite.id}`); + steps.push({ + name: 'Keywords Imported', + status: keywords.count > 0 ? 'healthy' : 'warning' as const, + message: `${keywords.count} keywords`, + }); + + // Step 2: Clusters exist + const clusters = await fetchAPI(`/v1/planner/clusters/?site=${activeSite.id}`); + steps.push({ + name: 'Content Clusters', + status: clusters.count > 0 ? 'healthy' : 'warning' as const, + message: `${clusters.count} clusters`, + }); + + // Step 3: Ideas generated + const ideas = await fetchAPI(`/v1/planner/ideas/?site=${activeSite.id}`); + steps.push({ + name: 'Content Ideas', + status: ideas.count > 0 ? 'healthy' : 'warning' as const, + message: `${ideas.count} ideas`, + }); + + // Step 4: Tasks created + const tasks = await fetchAPI(`/v1/writer/tasks/?site=${activeSite.id}`); + steps.push({ + name: 'Writer Tasks', + status: tasks.count > 0 ? 'healthy' : 'warning' as const, + message: `${tasks.count} tasks`, + }); + + // Step 5: Content generated + const content = await fetchAPI(`/v1/writer/content/?site=${activeSite.id}`); + steps.push({ + name: 'Content Generated', + status: content.count > 0 ? 'healthy' : 'warning' as const, + message: `${content.count} articles`, + }); + + const hasErrors = steps.some(s => s.status === 'error'); + const hasWarnings = steps.some(s => s.status === 'warning'); + + workflows.push({ + name: 'Content Generation Pipeline', + steps, + overall: hasErrors ? 'error' : hasWarnings ? 'warning' : 'healthy', + }); + } catch (error) { + workflows.push({ + name: 'Content Generation Pipeline', + steps: [{ name: 'Pipeline Check', status: 'error', message: 'Failed to check workflow' }], + overall: 'error', + }); + } + + setWorkflowHealth(workflows); + }, [activeSite]); + + // Check integration health + const checkIntegrationHealth = useCallback(async () => { + if (!activeSite) { + setIntegrationHealth(null); + return; + } + + try { + const integrations = await fetchAPI(`/v1/integration/integrations/?site_id=${activeSite.id}`); + + if (integrations.results && integrations.results.length > 0) { + const wpIntegration = integrations.results.find((i: any) => i.platform === 'wordpress'); + + if (wpIntegration) { + const health = await fetchAPI(`/v1/integration/integrations/${wpIntegration.id}/debug-status/`); + + setIntegrationHealth({ + platform: 'WordPress', + connected: wpIntegration.is_active, + last_sync: wpIntegration.last_sync_at, + sync_enabled: wpIntegration.sync_enabled, + plugin_active: health.health?.plugin_active || false, + status: wpIntegration.is_active && health.health?.plugin_active ? 'healthy' : 'warning', + }); + } else { + setIntegrationHealth(null); + } + } else { + setIntegrationHealth(null); + } + } catch (error) { + console.error('Failed to check integration:', error); + setIntegrationHealth(null); + } + }, [activeSite]); + + // Refresh all data + const refreshAll = useCallback(async () => { + setLoading(true); + await Promise.all([ + fetchSystemMetrics(), + fetchApiHealth(), + checkWorkflowHealth(), + checkIntegrationHealth(), + ]); + setLastUpdate(new Date()); + setLoading(false); + }, [fetchSystemMetrics, fetchApiHealth, checkWorkflowHealth, checkIntegrationHealth]); + + // Initial load and auto-refresh + useEffect(() => { + refreshAll(); + const interval = setInterval(refreshAll, 30000); // 30 seconds + return () => clearInterval(interval); + }, [refreshAll]); + + // Status badge component + const StatusBadge = ({ status }: { status: string }) => { + const colors = { + healthy: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', + warning: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400', + error: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', + }; + + const icons = { + healthy: CheckCircle, + warning: AlertTriangle, + error: XCircle, + }; + + const Icon = icons[status as keyof typeof icons] || AlertTriangle; + + return ( +
+ + {status.charAt(0).toUpperCase() + status.slice(1)} +
+ ); + }; + + // Progress bar component + const ProgressBar = ({ value, status }: { value: number; status: string }) => { + const colors = { + healthy: 'bg-green-500', + warning: 'bg-yellow-500', + error: 'bg-red-500', + }; + + return ( +
+
+
+ ); + }; + + return ( + <> + + +
+ {/* Header */} +
+
+

System Status

+

+ Master dashboard showing all system health metrics +

+
+
+
+ + Last updated: {lastUpdate.toLocaleTimeString()} +
+ +
+
+ + {/* System Resources & Services Health */} +
+

+ + System Resources +

+
+ {/* Compact System Metrics (70% width) */} +
+ {/* CPU */} +
+
+
+ + CPU +
+ +
+
+ {systemMetrics?.cpu.usage_percent.toFixed(1)}% +
+

{systemMetrics?.cpu.cores} cores

+ +
+ + {/* Memory */} +
+
+
+ + Memory +
+ +
+
+ {systemMetrics?.memory.usage_percent.toFixed(1)}% +
+

+ {systemMetrics?.memory.used_gb.toFixed(1)}/{systemMetrics?.memory.total_gb.toFixed(1)} GB +

+ +
+ + {/* Disk */} +
+
+
+ + Disk +
+ +
+
+ {systemMetrics?.disk.usage_percent.toFixed(1)}% +
+

+ {systemMetrics?.disk.used_gb.toFixed(1)}/{systemMetrics?.disk.total_gb.toFixed(1)} GB +

+ +
+
+ + {/* Services Stack (30% width) */} +
+
+
+ + PostgreSQL +
+ +
+ +
+
+ + Redis +
+ +
+ +
+
+ + Celery +
+
+ {serviceHealth?.celery.worker_count || 0}w + +
+
+
+
+
+ + {/* Site Selector */} +
+ +
+ + {/* API Health by Module */} +
+

+ + API Module Health +

+
+ {apiHealth.map((group) => ( +
+
+ {group.name} + +
+
+ {group.percentage}% +
+
+ {group.healthy}/{group.total} healthy +
+ +
+ ))} +
+
+ + {/* Content Workflow Health */} +
+

+ + Content Pipeline Status +

+ {workflowHealth.length === 0 ? ( +

Select a site to view workflow health

+ ) : ( +
+ {workflowHealth.map((workflow) => ( +
+
+

{workflow.name}

+ +
+
+ {workflow.steps.map((step, idx) => ( +
+
+ {step.name} + {step.status === 'healthy' ? ( + + ) : step.status === 'warning' ? ( + + ) : ( + + )} +
+ {step.message && ( +

{step.message}

+ )} + {idx < workflow.steps.length - 1 && ( +
+
+
+ )} +
+ ))} +
+
+ ))} +
+ )} +
+ + {/* WordPress Integration */} + {integrationHealth && ( +
+

+ + WordPress Integration +

+
+
+ Connection + +
+
+ Plugin Status + +
+
+ Sync Enabled + +
+
+ Last Sync + + {integrationHealth.last_sync + ? new Date(integrationHealth.last_sync).toLocaleString() + : 'Never'} + +
+
+
+ )} +
+ + ); +}