import { useState, useEffect, useRef, useCallback } from 'react'; import { Activity, Clock, FileText, AlertTriangle, Search, RefreshCw, RotateCcw, Box, BarChart2 } from 'lucide-react'; import { monitoringAPI, logsAPI, containerAPI } from '../services/api'; const API_SERVICES = ['ALL', 'network', 'wireguard', 'routing', 'email', 'calendar', 'files', 'vault', 'container', 'api']; const LEVELS = ['ALL', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']; const LEVEL_COLORS = { DEBUG: 'text-gray-500', INFO: 'text-blue-400', WARNING: 'text-yellow-400', ERROR: 'text-red-400', CRITICAL: 'text-red-500 font-bold', }; function LevelBadge({ level }) { const cls = LEVEL_COLORS[level?.toUpperCase()] || 'text-gray-400'; return [{level || '?'}]; } function LogLine({ entry }) { if (!entry || entry.raw_line !== undefined) { return
{entry?.raw_line || ''}
; } return (
{String(entry.timestamp || '').slice(0, 19)} {entry.service && [{entry.service}]} {entry.message || ''}
); } // ── Tab: API Service Logs ─────────────────────────────────────────────────── function ApiServiceLogsTab() { const [service, setService] = useState('ALL'); 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 intervalRef = useRef(null); const doFetch = useCallback(async () => { setLoading(true); try { const allSvcs = API_SERVICES.filter(s => s !== 'ALL'); if (service === 'ALL' || query) { const res = await logsAPI.searchLogs({ query: query || '', services: service === 'ALL' ? allSvcs : [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]); useEffect(() => { doFetch(); }, [service, level, lines]); useEffect(() => { if (autoRefresh) { intervalRef.current = setInterval(doFetch, 5000); } else { clearInterval(intervalRef.current); } return () => clearInterval(intervalRef.current); }, [autoRefresh, doFetch]); return (

These are structured logs written by the API backend for each service manager (wireguard, network, routing, etc.). They are stored in /app/data/logs/<service>.log and can be rotated from the Statistics tab.

{service !== 'ALL' && ( )}
setQuery(e.target.value)} onKeyDown={e => e.key === 'Enter' && doFetch()} /> {query && }
{loading && logs.length === 0 ? (
Loading…
) : logs.length === 0 ? (
No 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 [lines, setLines] = 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) .sort(); setContainers(names); if (names.length > 0 && !names.includes(selected)) setSelected(names[0]); }) .catch(() => {}); }, []); const doFetch = useCallback(async () => { if (!selected) return; setLoading(true); try { const res = await logsAPI.getStoredContainerLogs(selected, tail); setLines(res.data.lines || []); } catch (e) { setLines([`Error: ${e.message}`]); } finally { setLoading(false); } }, [selected, tail]); useEffect(() => { doFetch(); }, [selected, tail]); useEffect(() => { if (autoRefresh) { intervalRef.current = setInterval(doFetch, 5000); } else { clearInterval(intervalRef.current); } return () => clearInterval(intervalRef.current); }, [autoRefresh, doFetch]); return (

Container stdout/stderr collected from Docker and stored in /app/data/logs/container_<name>.log. Each fetch appends new lines since last collection. Rotate from the Statistics tab.

{loading && lines.length === 0 ? ( Loading… ) : lines.length === 0 ? ( No stored logs. Click refresh to collect. ) : ( lines.map((l, i) =>
{l}
) )}
{lines.length} lines stored
); } // ── Tab: Statistics & Rotation ────────────────────────────────────────────── function StatisticsTab() { const [files, setFiles] = useState([]); const [loading, setLoading] = useState(false); const [rotating, setRotating] = useState(null); const [msg, setMsg] = useState(''); const doFetch = async () => { setLoading(true); try { const res = await logsAPI.getLogFiles(); setFiles(res.data || []); } catch (e) { console.error(e); } finally { setLoading(false); } }; useEffect(() => { doFetch(); }, []); const rotate = async (file) => { const label = file ? file.label : 'all log files'; if (!window.confirm(`Rotate logs for ${label}?\nThe current file will be archived and a new one started.`)) return; const key = file ? file.name : 'all'; setRotating(key); setMsg(''); try { if (file) { await logsAPI.rotateLogs(file.name, file.kind); } else { // Rotate all: service logs via old endpoint, container logs individually await Promise.all(files.map(f => logsAPI.rotateLogs(f.name, f.kind))); } setMsg('Rotation complete.'); await doFetch(); } catch (e) { setMsg(`Error: ${e.message}`); } finally { setRotating(null); } }; const fmtSize = bytes => { if (!bytes) return '0 B'; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 ** 2).toFixed(2)} MB`; }; const serviceFiles = files.filter(f => f.kind === 'service'); const containerFiles = files.filter(f => f.kind === 'container'); const FileTable = ({ rows, title }) => (

{title}

{rows.length === 0 ? (

No files yet.

) : ( {rows.map(f => ( ))}
Name Size Last Modified Rotate
{f.label} {fmtSize(f.size)} {f.modified?.slice(0, 19)}
)}
); return (

Log Files & Rotation

{msg &&
{msg}
} {loading ?
Loading…
: ( <> )}
); } // ── Tab: Health History ───────────────────────────────────────────────────── function HealthHistoryTab() { const [healthHistory, setHealthHistory] = useState([]); const [loading, setLoading] = useState(false); const doFetch = async () => { setLoading(true); try { const res = await monitoringAPI.getHealthHistory(); setHealthHistory(res.data || []); } catch (e) { console.error(e); } finally { setLoading(false); } }; useEffect(() => { doFetch(); }, []); const SvcCol = ({ data }) => (data?.status === 'online' || data?.running === true) ? OK : Down; return (

Health History

{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 ──────────────────────────────────────────────────────────────────── const TABS = [ { id: 'api', label: 'API 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('api'); return (

Logs & Monitoring

API service logs, container stdout/stderr, rotation, and health history.

{TABS.map(({ id, label, icon: Icon }) => ( ))}
{tab === 'api' && } {tab === 'container' && } {tab === 'statistics' && } {tab === 'health' && }
); } export default Logs;