import { useState, useEffect, useRef, useCallback } from 'react'; import { Activity, FileText, AlertTriangle, Search, RefreshCw, RotateCcw, Box, Settings } from 'lucide-react'; import { monitoringAPI, logsAPI, containerAPI } from '../services/api'; const API_SERVICES = ['ALL', 'network', 'wireguard', 'routing', 'email', 'calendar', 'files', 'vault', '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 1: API Service Logs ───────────────────────────────────────────────── // These are structured JSON logs written by Python service managers. // Stored in /app/api/data/logs/ (persisted to ./data/logs/ on the host via volume mount). 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 [fileInfos, setFileInfos] = useState([]); const [rotating, setRotating] = useState(null); const [showFiles, setShowFiles] = 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(l => { try { return JSON.parse(l); } catch { return { raw_line: l }; } }); 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]); const loadFileInfos = async () => { try { setFileInfos((await logsAPI.getLogFiles()).data || []); } catch {} }; const toggleFiles = () => { if (!showFiles) loadFileInfos(); setShowFiles(v => !v); }; const rotate = async (service) => { if (!window.confirm(`Rotate log for ${service || 'all services'}?\nCurrent file will be archived.`)) return; setRotating(service || 'all'); try { await logsAPI.rotateLogs(service || null); await loadFileInfos(); } catch {} setRotating(null); }; const fmtSize = b => !b ? '0 B' : b < 1024 ? `${b} B` : b < 1048576 ? `${(b/1024).toFixed(1)} KB` : `${(b/1048576).toFixed(2)} MB`; return (
{/* Controls */}
{service !== 'ALL' && !query && ( )}
setQuery(e.target.value)} onKeyDown={e => e.key === 'Enter' && doFetch()} /> {query && }
{/* File info panel */} {showFiles && (
Log Files host path: ./data/logs/ — rotated backups saved as wireguard.log.1, wireguard.log.2
{fileInfos.map(f => ( ))} {fileInfos.length === 0 && }
FileSizeModified
{f.file} {fmtSize(f.size)} {f.modified?.slice(0, 19)} {!f.backup && ( )}
No log files found.
)} {/* Log output */}
{loading && !logs.length ? (
Loading…
) : !logs.length ? (
No entries found.
) : ( logs.map((e, i) => ) )}
{logs.length} entries
); } // ── Tab 2: Container Logs ─────────────────────────────────────────────────── // Container stdout/stderr read live via `docker logs`. // Docker itself rotates these files (json-file driver, max-size 10m, max-file 5 — configured in docker-compose.yml). 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 && !names.includes(selected)) setSelected(names[0]); }) .catch(() => {}); }, []); const doFetch = useCallback(async () => { if (!selected) return; setLoading(true); try { const res = await containerAPI.getContainerLogs(selected, tail); const raw = res.data.logs || ''; setLines(typeof raw === 'string' ? raw.split('\n').filter(Boolean) : raw); } 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 (
Live stdout/stderr from Docker. Rotation is automatic: json-file driver, 10 MB max-size, 5 backups per container — configured in docker-compose.yml.
{loading && !lines.length ? ( Loading… ) : !lines.length ? ( No output. ) : ( lines.map((l, i) =>
{l}
) )}
{lines.length} lines
); } // ── Tab 3: Verbosity Config ───────────────────────────────────────────────── const VERBOSITY_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']; // Container services that need a container recreate before a level change applies. const PENDING_RESTART_CONTAINERS = ['wireguard', 'mailserver']; function LevelRow({ name, value, original, badge, onChange }) { return ( {name} {badge && ( needs restart )} {value !== original && changed} ); } function VerbosityTab() { const [config, setConfig] = useState(null); // last-saved server state const [pending, setPending] = useState(null); // editable copy const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [msg, setMsg] = useState(''); const load = async () => { setLoading(true); try { const res = await logsAPI.getVerbosity(); setConfig(res.data); setPending(JSON.parse(JSON.stringify(res.data))); } catch (e) { setMsg(`Error: ${e.message}`); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const setRoot = (v) => setPending(p => ({ ...p, python: { ...p.python, root: v } })); const setService = (svc, v) => setPending(p => ({ ...p, python: { ...p.python, services: { ...p.python.services, [svc]: v } } })); const setContainer = (c, v) => setPending(p => ({ ...p, containers: { ...p.containers, [c]: v } })); const save = async () => { setSaving(true); setMsg(''); try { const res = await logsAPI.setVerbosity(pending); setConfig(res.data.logging); setPending(JSON.parse(JSON.stringify(res.data.logging))); const restarts = Object.entries(res.data.applied || {}) .filter(([, v]) => v === 'pending_restart') .map(([k]) => k); setMsg(restarts.length ? `Saved. ${restarts.join(', ')} will apply on next container restart.` : 'Levels saved and applied.'); } catch (e) { setMsg(`Error: ${e.response?.data?.error || e.message}`); } finally { setSaving(false); } }; if (loading || !pending) return
Loading…
; const services = Object.keys(pending.python.services).sort(); const containers = Object.keys(pending.containers).sort(); return (

Python services

Applies immediately to the running API — no restart needed. Persisted in cell_config and restored on restart.
{services.map(svc => ( setService(svc, v)} /> ))}
Service Log Level

Container services

caddy and coredns reload immediately. wireguard and mailserver are container-ENV driven — their level applies on the next container restart.
{containers.map(c => ( setContainer(c, v)} /> ))}
Container Log Level
{msg && {msg}}
); } // ── Tab 4: Health History ─────────────────────────────────────────────────── function HealthHistoryTab() { const [history, setHistory] = useState([]); const [loading, setLoading] = useState(false); const load = async () => { setLoading(true); try { setHistory((await monitoringAPI.getHealthHistory()).data || []); } catch {} setLoading(false); }; useEffect(() => { load(); }, []); // health history entries have shape: { status: {running, status}, healthy, connectivity, ... } const SvcCol = ({ data }) => { const running = data?.status?.running === true || data?.status?.status === 'online' || data?.running === true || data?.status === 'online'; return running ? OK : Down; }; return (

Health History

{loading ?
Loading…
: (
{['Timestamp','Network','WireGuard','Routing','Vault','Alerts'].map(h => ( ))} {history.map((h, i) => ( ))}
{h}
{h.timestamp} {h.alerts?.length ? 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: 'verbosity', label: 'Verbosity Config', icon: Settings }, { id: 'health', label: 'Health History', icon: Activity }, ]; export default function Logs() { const [tab, setTab] = useState('api'); return (

Logs & Monitoring

API service logs · Container stdout/stderr · Log level config · Health history

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