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)
-
-
-
-
-
- | Timestamp |
- Network |
- WireGuard |
- Email |
- Calendar |
- Files |
- Routing |
- Vault |
- Alerts |
-
-
-
- {healthHistory.map((h, i) => (
- 0 ? 'bg-red-100' : ''}>
- | {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.
+ ) : (
+
+
+
+
+ | Service |
+ File Size |
+ Total Entries |
+ Errors |
+ Warnings |
+ Last Entry |
+ Rotate |
+
+
+
+ {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 (
+
+ | {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 => (
+ | {h} |
+ ))}
+
+
+
+ {healthHistory.map((h, i) => (
+ 0 ? 'bg-red-50' : ''}>
+ | {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