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.
setService(e.target.value)}>
{API_SERVICES.map(s => {s === 'ALL' ? 'ALL services' : s} )}
setLevel(e.target.value)}>
{LEVELS.map(l => {l} )}
{service !== 'ALL' && (
setLines(Number(e.target.value))}>
{[50, 100, 200, 500].map(n => {n} lines )}
)}
setQuery(e.target.value)}
onKeyDown={e => e.key === 'Enter' && doFetch()}
/>
{query && { setQuery(''); }}>✕ }
setAutoRefresh(v => !v)}
>
{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.
setSelected(e.target.value)}>
{(containers.length > 0 ? containers : ['cell-api']).map(c => (
{c}
))}
setTail(Number(e.target.value))}>
{[50, 100, 200, 500].map(n => {n} lines )}
setAutoRefresh(v => !v)}
>
{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.
) : (
Name
Size
Last Modified
Rotate
{rows.map(f => (
{f.label}
{fmtSize(f.size)}
{f.modified?.slice(0, 19)}
rotate(f)}
disabled={rotating === f.name}
>
))}
)}
);
return (
Log Files & Rotation
Refresh
rotate(null)}
disabled={rotating === 'all'}
>
Rotate All
{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
Refresh
{loading ?
Loading…
: (
{['Timestamp', 'Network', 'WireGuard', 'Email', 'Calendar', 'Files', 'Routing', 'Vault', 'Alerts'].map(h => (
{h}
))}
{healthHistory.map((h, i) => (
0 ? 'bg-red-50' : ''}>
{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 }) => (
setTab(id)}
>
{label}
))}
{tab === 'api' &&
}
{tab === 'container' &&
}
{tab === 'statistics' &&
}
{tab === 'health' &&
}
);
}
export default Logs;