From b457b9e5f33e7d419ae7ad5a83a739396d7f2959 Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 24 Feb 2026 16:35:20 -0500 Subject: [PATCH] feat(04-01): create AlertBanner and AdminMonitoringDashboard components - AlertBanner filters to active service_down/service_degraded alerts only - AlertBanner renders red banner with AlertTriangle icon and X Acknowledge button - AdminMonitoringDashboard fetches health+analytics concurrently via Promise.all - Health panel shows 1x4 grid of service status cards with colored dots - Analytics panel shows 1x5 stat cards with range selector and Refresh button - Both components follow existing Tailwind/lucide-react/cn() patterns --- .../components/AdminMonitoringDashboard.tsx | 178 ++++++++++++++++++ frontend/src/components/AlertBanner.tsx | 44 +++++ 2 files changed, 222 insertions(+) create mode 100644 frontend/src/components/AdminMonitoringDashboard.tsx create mode 100644 frontend/src/components/AlertBanner.tsx 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;