diff --git a/webui/src/pages/Logs.jsx b/webui/src/pages/Logs.jsx index af51446..515091f 100644 --- a/webui/src/pages/Logs.jsx +++ b/webui/src/pages/Logs.jsx @@ -1,164 +1,500 @@ -import { useState, useEffect } from 'react'; -import { Activity, Clock, FileText, AlertTriangle } from 'lucide-react'; -import { monitoringAPI } from '../services/api'; - -function Logs() { - const [backendLog, setBackendLog] = useState(''); - const [healthHistory, setHealthHistory] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [tab, setTab] = useState('logs'); - - useEffect(() => { - fetchData(); - }, []); - - const fetchData = async () => { - setIsLoading(true); - try { - const [logRes, healthRes] = await Promise.all([ - monitoringAPI.getBackendLogs(100), - monitoringAPI.getHealthHistory(), - ]); - setBackendLog(logRes.data.log || ''); - setHealthHistory(healthRes.data || []); - } catch (error) { - console.error('Failed to fetch monitoring data:', error); - } finally { - setIsLoading(false); - } - }; - - if (isLoading) { - return ( -
-
-
- ); - } - - return ( -
-
-

System Monitoring

-

- View backend logs and health history -

-
- -
- - -
- - {tab === 'logs' && ( -
-
- -

Backend Logs (last 100 lines)

-
-
-
{backendLog || 'No logs available.'}
-
-
- )} - - {tab === 'health' && ( -
-
- -

Health History (last 100 checks)

-
-
- - - - - - - - - - - - - - - - {healthHistory.map((h, i) => ( - 0 ? 'bg-red-100' : ''}> - - - - - - - - - - - ))} - -
TimestampNetworkWireGuardEmailCalendarFilesRoutingVaultAlerts
{h.timestamp} - {h.network?.status === 'online' || h.network?.running === true ? - OK : - Down - } - - {h.wireguard?.status === 'online' || h.wireguard?.running === true ? - OK : - Down - } - - {h.email?.status === 'online' || h.email?.running === true ? - OK : - Down - } - - {h.calendar?.status === 'online' || h.calendar?.running === true ? - OK : - Down - } - - {h.files?.status === 'online' || h.files?.running === true ? - OK : - Down - } - - {h.routing?.status === 'online' || h.routing?.running === true ? - OK : - Down - } - - {h.vault?.status === 'online' || h.vault?.running === true ? - OK : - Down - } - - {h.alerts && h.alerts.length > 0 ? ( -
- {h.alerts.map((a, j) => ( - {a} - ))} -
- ) : ( - None - )} -
-
-
- )} -
- ); -} - -export default Logs; \ No newline at end of file +import { useState, useEffect, useRef, useCallback } from 'react'; +import { + Activity, Clock, FileText, AlertTriangle, Search, RefreshCw, + RotateCcw, Box, BarChart2, Download, ChevronDown, ChevronUp, + Filter +} from 'lucide-react'; +import { monitoringAPI, logsAPI, containerAPI } from '../services/api'; + +const SERVICES = ['network', 'wireguard', 'routing', 'email', 'calendar', 'files', 'vault', 'container']; +const LEVELS = ['ALL', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']; +const LEVEL_COLORS = { + DEBUG: 'text-gray-400', + INFO: 'text-blue-400', + WARNING: 'text-yellow-400', + ERROR: 'text-red-400', + CRITICAL: 'text-red-600 font-bold', +}; + +function LevelBadge({ level }) { + const color = LEVEL_COLORS[level?.toUpperCase()] || 'text-gray-300'; + return [{level}]; +} + +function LogLine({ entry }) { + if (entry.raw_line) { + return
{entry.raw_line}
; + } + return ( +
+ {entry.timestamp?.slice(0, 19)} + + {entry.service && [{entry.service}]} + {entry.message} +
+ ); +} + +// ── Tab: Service Logs ─────────────────────────────────────────────────────── +function ServiceLogsTab() { + const [service, setService] = useState('network'); + const [level, setLevel] = useState('ALL'); + const [lines, setLines] = useState(100); + const [query, setQuery] = useState(''); + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(false); + const [autoRefresh, setAutoRefresh] = useState(false); + const [searchMode, setSearchMode] = useState(false); + const intervalRef = useRef(null); + const bottomRef = useRef(null); + + const fetch = useCallback(async () => { + setLoading(true); + try { + if (searchMode && query) { + const res = await logsAPI.searchLogs({ + query, + services: [service], + level: level === 'ALL' ? undefined : level, + }); + setLogs(res.data.results || []); + } else { + const res = await logsAPI.getServiceLogs(service, level, lines); + const raw = res.data.logs || []; + const parsed = raw.map(line => { + try { return JSON.parse(line); } catch { return { raw_line: line }; } + }); + setLogs(parsed.reverse()); + } + } catch (e) { + setLogs([{ raw_line: `Error: ${e.message}` }]); + } finally { + setLoading(false); + } + }, [service, level, lines, query, searchMode]); + + useEffect(() => { + fetch(); + }, [service, level, lines]); + + useEffect(() => { + if (autoRefresh) { + intervalRef.current = setInterval(fetch, 5000); + } else { + clearInterval(intervalRef.current); + } + return () => clearInterval(intervalRef.current); + }, [autoRefresh, fetch]); + + return ( +
+ {/* Controls */} +
+ + + + + + +
+ setQuery(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') { setSearchMode(true); fetch(); } }} + /> + + {query && ( + + )} +
+ + + + +
+ + {/* Log output */} +
+ {loading && logs.length === 0 ? ( +
Loading…
+ ) : logs.length === 0 ? ( +
No log entries found.
+ ) : ( + logs.map((entry, i) => ) + )} +
+
+
{logs.length} entries
+
+ ); +} + +// ── Tab: Container Logs ───────────────────────────────────────────────────── +function ContainerLogsTab() { + const [containers, setContainers] = useState([]); + const [selected, setSelected] = useState('cell-api'); + const [tail, setTail] = useState(100); + const [logs, setLogs] = useState(''); + const [loading, setLoading] = useState(false); + const [autoRefresh, setAutoRefresh] = useState(false); + const intervalRef = useRef(null); + + useEffect(() => { + containerAPI.listContainers() + .then(res => { + const names = (res.data || []).map(c => c.name || c.Names?.[0]?.replace('/', '')).filter(Boolean); + setContainers(names); + if (names.length > 0 && !names.includes(selected)) setSelected(names[0]); + }) + .catch(() => {}); + }, []); + + const fetch = useCallback(async () => { + setLoading(true); + try { + const res = await containerAPI.getContainerLogs(selected, tail); + setLogs(res.data.logs || ''); + } catch (e) { + setLogs(`Error: ${e.message}`); + } finally { + setLoading(false); + } + }, [selected, tail]); + + useEffect(() => { fetch(); }, [selected, tail]); + + useEffect(() => { + if (autoRefresh) { + intervalRef.current = setInterval(fetch, 5000); + } else { + clearInterval(intervalRef.current); + } + return () => clearInterval(intervalRef.current); + }, [autoRefresh, fetch]); + + return ( +
+
+ + + + + + + +
+ +
+ {loading ? 'Loading…' : logs || 'No logs.'} +
+
+ ); +} + +// ── Tab: Statistics & Rotation ────────────────────────────────────────────── +function StatisticsTab() { + const [stats, setStats] = useState({}); + const [loading, setLoading] = useState(false); + const [rotating, setRotating] = useState(null); + + const fetch = async () => { + setLoading(true); + try { + const res = await logsAPI.getStatistics(); + setStats(res.data || {}); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetch(); }, []); + + const rotate = async (service) => { + setRotating(service); + try { + await logsAPI.rotateLogs(service || null); + await fetch(); + } catch (e) { + console.error(e); + } finally { + setRotating(null); + } + }; + + const fmtSize = (bytes) => { + if (!bytes) return '0 B'; + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1024 / 1024).toFixed(2)} MB`; + }; + + const services = Object.keys(stats); + + return ( +
+
+

Log File Statistics

+
+ + +
+
+ + {loading ? ( +
Loading…
+ ) : services.length === 0 ? ( +
No log statistics available.
+ ) : ( +
+ + + + + + + + + + + + + + {services.map(svc => { + const s = stats[svc]; + const hasError = s?.error; + const errorCount = s?.level_counts?.ERROR || 0; + const warnCount = s?.level_counts?.WARNING || 0; + return ( + + + + + + + + + + ); + })} + +
ServiceFile SizeTotal EntriesErrorsWarningsLast EntryRotate
{svc} + {hasError ? '—' : fmtSize(s.file_size)} + + {hasError ? {s.error} : s.total_entries} + 0 ? 'text-red-600 font-bold' : ''}`}> + {hasError ? '—' : errorCount} + 0 ? 'text-yellow-600' : ''}`}> + {hasError ? '—' : warnCount} + + {hasError ? '—' : (s.last_entry?.slice(0, 19) || '—')} + + +
+
+ )} +
+ ); +} + +// ── Tab: Health History ───────────────────────────────────────────────────── +function HealthHistoryTab() { + const [healthHistory, setHealthHistory] = useState([]); + const [loading, setLoading] = useState(false); + + const fetch = async () => { + setLoading(true); + try { + const res = await monitoringAPI.getHealthHistory(); + setHealthHistory(res.data || []); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetch(); }, []); + + const ServiceCol = ({ data }) => { + const ok = data?.status === 'online' || data?.running === true; + return ok + ? OK + : Down; + }; + + return ( +
+
+

Health History (last 100 checks)

+ +
+ {loading ? ( +
Loading…
+ ) : ( +
+ + + + {['Timestamp', 'Network', 'WireGuard', 'Email', 'Calendar', 'Files', 'Routing', 'Vault', 'Alerts'].map(h => ( + + ))} + + + + {healthHistory.map((h, i) => ( + 0 ? 'bg-red-50' : ''}> + + + + + + + + + + + ))} + +
{h}
{h.timestamp} + {h.alerts?.length > 0 ? ( + h.alerts.map((a, j) => ( + + {a} + + )) + ) : ( + + )} +
+
+ )} +
+ ); +} + +// ── Main Logs Page ────────────────────────────────────────────────────────── +const TABS = [ + { id: 'service', label: 'Service Logs', icon: FileText }, + { id: 'container', label: 'Container Logs', icon: Box }, + { id: 'statistics', label: 'Statistics & Rotation', icon: BarChart2 }, + { id: 'health', label: 'Health History', icon: Activity }, +]; + +function Logs() { + const [tab, setTab] = useState('service'); + + return ( +
+
+

Logs & Monitoring

+

Search service logs, view container output, and manage log rotation.

+
+ + {/* Tabs */} +
+ {TABS.map(({ id, label, icon: Icon }) => ( + + ))} +
+ +
+ {tab === 'service' && } + {tab === 'container' && } + {tab === 'statistics' && } + {tab === 'health' && } +
+
+ ); +} + +export default Logs; diff --git a/webui/src/services/api.js b/webui/src/services/api.js index 4c9fd5b..e78d7b3 100644 --- a/webui/src/services/api.js +++ b/webui/src/services/api.js @@ -186,6 +186,17 @@ export const monitoringAPI = { getHealthHistory: () => api.get('/api/health/history'), }; +// Logs API +export const logsAPI = { + getServiceLogs: (service, level = 'ALL', lines = 100) => + api.get(`/api/logs/services/${service}`, { params: { level, lines } }), + searchLogs: (data) => api.post('/api/logs/search', data), + exportLogs: (data) => api.post('/api/logs/export', data), + getStatistics: (service) => + api.get('/api/logs/statistics', service ? { params: { service } } : {}), + rotateLogs: (service) => api.post('/api/logs/rotate', service ? { service } : {}), +}; + // Container Management API export const containerAPI = { // Containers