import React, { useState, useEffect, useRef } from 'react'; import axios from 'axios'; import { formatLocalTime } from './utils/timeUtils'; import './Observability.css'; function Observability() { const [activeTab, setActiveTab] = useState('metrics'); const [metrics, setMetrics] = useState(null); const [errors, setErrors] = useState([]); const [auditLogs, setAuditLogs] = useState([]); const [auditSummary, setAuditSummary] = useState(null); const [expandedAuditRow, setExpandedAuditRow] = useState(null); const [filters, setFilters] = useState({ entityType: '', entityId: '', userId: '', action: '' }); const wsRef = useRef(null); const wsReconnectTimer = useRef(null); const wsReconnectDelay = useRef(1000); useEffect(() => { loadData(); const interval = setInterval(loadData, 30000); return () => clearInterval(interval); // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeTab, filters]); useEffect(() => { const connect = () => { try { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const ws = new WebSocket(`${protocol}//${window.location.host}`); ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.type === 'metrics' && activeTab === 'metrics') { setMetrics(msg.data); } } catch (_) {} }; ws.onopen = () => { wsReconnectDelay.current = 1000; }; ws.onclose = () => { wsRef.current = null; wsReconnectTimer.current = setTimeout(() => { wsReconnectDelay.current = Math.min(wsReconnectDelay.current * 2, 30000); connect(); }, wsReconnectDelay.current); }; ws.onerror = () => {}; wsRef.current = ws; } catch (_) {} }; connect(); return () => { clearTimeout(wsReconnectTimer.current); if (wsRef.current) wsRef.current.close(); }; }, [activeTab]); const loadData = async () => { try { if (activeTab === 'metrics') { const resp = await axios.get('/api/observability/metrics'); setMetrics(resp.data); } else if (activeTab === 'errors') { const resp = await axios.get('/api/observability/errors?limit=50'); setErrors(resp.data.errors); } else if (activeTab === 'audit') { const auditResp = await axios.get('/api/observability/audit-logs', { params: filters }); setAuditLogs(auditResp.data.logs); setAuditSummary(auditResp.data.summary); } } catch (error) { // Error loading data } }; return (

System Observability

{/* Metrics Tab */} {activeTab === 'metrics' && metrics && (
Server Uptime
{metrics.uptimeFormatted || `${Math.floor(metrics.uptime / 3600)}h`}
Total API Requests
{metrics.apiStats.reduce((sum, s) => sum + s.totalRequests, 0)}
Errors Logged
0 ? 'is-danger' : 'is-ok'}`}> {metrics.errorCount}
Unique Errors
{metrics.uniqueErrors}

API Endpoint Performance

{metrics.apiStats.map((stat, i) => ( ))}
Endpoint Requests Success Rate Avg Response Min/Max
{stat.method} {stat.endpoint} {stat.totalRequests} = 95 ? 'is-good' : 'is-warn'}`}> {stat.successRate}% {stat.avgResponseTime}ms {stat.minResponseTime}ms / {stat.maxResponseTime}ms
)} {/* Errors Tab */} {activeTab === 'errors' && (
{errors.length === 0 ? (

No errors logged

) : (
{errors.map((error, i) => ( ))}
Error Message Context Occurred At
{error.error_message} {error.context ? JSON.stringify(JSON.parse(error.context)).substring(0, 50) : '-'} {formatLocalTime(error.occurred_at)}
)}
)} {/* Audit Logs Tab */} {activeTab === 'audit' && (
{auditSummary && (

Summary (Last 7 Days): {auditSummary.totalEvents} events

{auditSummary.actionBreakdown.map(action => (

{action.action}: {action.count}

))}
)}
setFilters({ ...filters, entityType: e.target.value })} className="obs-filter-input" /> setFilters({ ...filters, entityId: e.target.value })} className="obs-filter-input" /> setFilters({ ...filters, userId: e.target.value })} className="obs-filter-input" /> setFilters({ ...filters, action: e.target.value })} className="obs-filter-input" />
{auditLogs.length === 0 ? (

No audit logs found

) : (
{auditLogs.map((log, i) => ( setExpandedAuditRow(expandedAuditRow === i ? null : i)}> {expandedAuditRow === i && ( )} ))}
Action Entity User Changes Time
{log.action} {log.entity_type} #{log.entity_id} {log.user_id} {expandedAuditRow === i ? '▼ Hide' : '▶ Show'} Changes {formatLocalTime(log.created_at)}
Full Changes:
                              {JSON.stringify(log.changes, null, 2)}
                            
)}
)}
); } export default Observability;