diff --git a/frontend/src/components/AdminMonitoringDashboard.tsx b/frontend/src/components/AdminMonitoringDashboard.tsx new file mode 100644 index 0000000..5095434 --- /dev/null +++ b/frontend/src/components/AdminMonitoringDashboard.tsx @@ -0,0 +1,178 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Activity, Clock, RefreshCw } from 'lucide-react'; +import { + adminService, + ServiceHealthEntry, + AnalyticsSummary, +} from '../services/adminService'; +import { cn } from '../utils/cn'; + +const statusStyles: Record< + ServiceHealthEntry['status'], + { dot: string; label: string; text: string } +> = { + healthy: { dot: 'bg-green-500', label: 'Healthy', text: 'text-green-700' }, + degraded: { dot: 'bg-yellow-500', label: 'Degraded', text: 'text-yellow-700' }, + down: { dot: 'bg-red-500', label: 'Down', text: 'text-red-700' }, + unknown: { dot: 'bg-gray-400', label: 'Unknown', text: 'text-gray-600' }, +} as const; + +const serviceDisplayName: Record = { + document_ai: 'Document AI', + llm_api: 'LLM API', + supabase: 'Supabase', + firebase_auth: 'Firebase Auth', +}; + +const AdminMonitoringDashboard: React.FC = () => { + const [health, setHealth] = useState([]); + const [analytics, setAnalytics] = useState(null); + const [range, setRange] = useState('24h'); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const loadData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const [healthData, analyticsData] = await Promise.all([ + adminService.getHealth(), + adminService.getAnalytics(range), + ]); + setHealth(healthData); + setAnalytics(analyticsData); + } catch { + setError('Failed to load monitoring data'); + } finally { + setLoading(false); + } + }, [range]); + + useEffect(() => { + loadData(); + }, [loadData]); + + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+

{error}

+ +
+ ); + } + + return ( +
+ {/* Service Health Panel */} +
+
+

+ + Service Health +

+
+
+ {health.map((entry) => { + const styles = statusStyles[entry.status] ?? statusStyles.unknown; + const displayName = serviceDisplayName[entry.service] ?? entry.service; + return ( +
+
+
+ {displayName} +
+

{styles.label}

+ {entry.latencyMs !== null && ( +

{entry.latencyMs}ms

+ )} +
+ + + {entry.checkedAt + ? new Date(entry.checkedAt).toLocaleString() + : 'Never checked'} + +
+
+ ); + })} +
+
+ + {/* Processing Analytics Panel */} + {analytics && ( +
+
+

Processing Analytics

+
+ + + +
+
+
+
+

Total Uploads

+

{analytics.totalUploads}

+
+
+

Succeeded

+

{analytics.succeeded}

+
+
+

Failed

+

{analytics.failed}

+
+
+

Success Rate

+

+ {(analytics.successRate * 100).toFixed(1)}% +

+
+
+

Avg Processing Time

+

+ {analytics.avgProcessingMs !== null + ? `${(analytics.avgProcessingMs / 1000).toFixed(1)}s` + : 'N/A'} +

+
+
+
+ )} +
+ ); +}; + +export { AdminMonitoringDashboard }; +export default AdminMonitoringDashboard; diff --git a/frontend/src/components/AlertBanner.tsx b/frontend/src/components/AlertBanner.tsx new file mode 100644 index 0000000..44d6eab --- /dev/null +++ b/frontend/src/components/AlertBanner.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { AlertTriangle, X } from 'lucide-react'; +import { AlertEvent } from '../services/adminService'; +import { cn } from '../utils/cn'; + +interface AlertBannerProps { + alerts: AlertEvent[]; + onAcknowledge: (id: string) => Promise; +} + +const AlertBanner: React.FC = ({ alerts, onAcknowledge }) => { + const criticalAlerts = alerts.filter( + (a) => + a.status === 'active' && + (a.alert_type === 'service_down' || a.alert_type === 'service_degraded') + ); + + if (criticalAlerts.length === 0) return null; + + return ( +
+ {criticalAlerts.map((alert) => ( +
+
+ + + {alert.service_name}: {alert.message ?? alert.alert_type} + +
+ +
+ ))} +
+ ); +}; + +export { AlertBanner }; +export default AlertBanner;