import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Box, Paper, Typography, Stack, Button, IconButton, Tooltip, FormControl, InputLabel, Select, MenuItem, TextField, Divider, Chip, CircularProgress, ToggleButtonGroup, ToggleButton, Alert, InputAdornment, } from "@mui/material"; import { ReceiptLong as LogsIcon, Refresh as RefreshIcon, DeleteOutline as DeleteIcon, Save as SaveIcon, Visibility as VisibilityIcon, VisibilityOff as VisibilityOffIcon, History as HistoryIcon, } from "@mui/icons-material"; import { AgGridReact } from "ag-grid-react"; import { ModuleRegistry, AllCommunityModule, themeQuartz } from "ag-grid-community"; import Prism from "prismjs"; import "prismjs/components/prism-bash"; import "prismjs/themes/prism-okaidia.css"; import Editor from "react-simple-code-editor"; ModuleRegistry.registerModules([AllCommunityModule]); const AURORA_SHELL = { background: "radial-gradient(120% 80% at 0% 0%, rgba(77, 172, 255, 0.22) 0%, rgba(4, 7, 17, 0.0) 60%)," + "radial-gradient(100% 80% at 100% 0%, rgba(214, 130, 255, 0.25) 0%, rgba(6, 9, 20, 0.0) 62%)," + "linear-gradient(135deg, rgba(3,6,15,1) 0%, rgba(8,12,25,0.97) 50%, rgba(13,7,20,0.95) 100%)", panel: "linear-gradient(135deg, rgba(10, 16, 31, 0.98) 0%, rgba(6, 10, 24, 0.95) 60%, rgba(15, 6, 26, 0.97) 100%)", border: "rgba(148, 163, 184, 0.28)", text: "#e2e8f0", muted: "#94a3b8", accent: "#7db7ff", }; const gradientButtonSx = { textTransform: "none", fontWeight: 600, color: "#041125", borderRadius: 999, backgroundImage: "linear-gradient(135deg, #7dd3fc 0%, #c084fc 100%)", boxShadow: "0 10px 25px rgba(124, 58, 237, 0.4)", "&:hover": { backgroundImage: "linear-gradient(135deg, #8be9ff 0%, #d5a8ff 100%)", boxShadow: "0 14px 32px rgba(124, 58, 237, 0.45)", }, }; const quartzTheme = themeQuartz.withParams({ accentColor: "#7db7ff", backgroundColor: "#05070f", foregroundColor: "#f8fafc", headerBackgroundColor: "#0b1527", browserColorScheme: "dark", fontFamily: { googleFont: "IBM Plex Sans" }, headerFontSize: 14, borderColor: "rgba(148,163,184,0.28)", }); const quartzThemeClass = quartzTheme.themeName || "ag-theme-quartz"; const gridFontFamily = '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif'; const gridThemeStyle = { width: "100%", height: "100%", "--ag-foreground-color": "#f1f5f9", "--ag-background-color": "#050b16", "--ag-header-background-color": "#0f1a2e", "--ag-odd-row-background-color": "rgba(255,255,255,0.02)", "--ag-row-hover-color": "rgba(109, 196, 255, 0.12)", "--ag-border-color": "rgba(148,163,184,0.28)", "--ag-font-family": gridFontFamily, }; function formatBytes(bytes) { if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB", "PB"]; const idx = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); const value = bytes / Math.pow(1024, idx); return `${value.toFixed(value >= 10 || idx === 0 ? 0 : 1)} ${units[idx]}`; } function formatTimestamp(ts) { if (!ts) return "n/a"; try { const d = new Date(ts); if (!Number.isNaN(d.getTime())) { return d.toLocaleString(); } } catch { /* noop */ } return ts; } export default function LogManagement({ isAdmin = false }) { const [logs, setLogs] = useState([]); const [defaultRetention, setDefaultRetention] = useState(30); const [selectedDomain, setSelectedDomain] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const [retentionDraft, setRetentionDraft] = useState(""); const [listLoading, setListLoading] = useState(false); const [entryLoading, setEntryLoading] = useState(false); const [entries, setEntries] = useState([]); const [entriesMeta, setEntriesMeta] = useState(null); const [gridMode, setGridMode] = useState("structured"); const [error, setError] = useState(null); const [actionMessage, setActionMessage] = useState(null); const [quickFilter, setQuickFilter] = useState(""); const gridRef = useRef(null); const logMap = useMemo(() => { const map = new Map(); logs.forEach((log) => map.set(log.file, log)); return map; }, [logs]); const rawContent = useMemo(() => entries.map((entry) => entry.raw || "").join("\n"), [entries]); const columnDefs = useMemo( () => [ { headerName: "Timestamp", field: "timestamp", minWidth: 200 }, { headerName: "Level", field: "level", width: 110, valueFormatter: (p) => (p.value || "").toUpperCase() }, { headerName: "Scope", field: "scope", minWidth: 120 }, { headerName: "Service", field: "service", minWidth: 160 }, { headerName: "Message", field: "message", flex: 1, minWidth: 300 }, ], [] ); const defaultColDef = useMemo( () => ({ sortable: true, filter: true, resizable: true, wrapText: false, cellStyle: { fontFamily: gridFontFamily, fontSize: "0.9rem", color: "#e2e8f0" }, }), [] ); const applyQuickFilter = useCallback( (value) => { setQuickFilter(value); const api = gridRef.current?.api; if (api) api.setGridOption("quickFilterText", value); }, [setQuickFilter] ); const fetchLogs = useCallback(async () => { if (!isAdmin) return; setListLoading(true); setError(null); setActionMessage(null); try { const resp = await fetch("/api/server/logs", { credentials: "include" }); if (!resp.ok) throw new Error(`Failed to load logs (HTTP ${resp.status})`); const data = await resp.json(); setLogs(Array.isArray(data?.logs) ? data.logs : []); setDefaultRetention(Number.isFinite(data?.default_retention_days) ? data.default_retention_days : 30); if (Array.isArray(data?.retention_deleted) && data.retention_deleted.length > 0) { setActionMessage(`Removed ${data.retention_deleted.length} expired log files automatically.`); } } catch (err) { setError(String(err)); } finally { setListLoading(false); } }, [isAdmin]); const fetchEntries = useCallback( async (file) => { if (!file) return; setEntryLoading(true); setError(null); try { const resp = await fetch(`/api/server/logs/${encodeURIComponent(file)}/entries?limit=1000`, { credentials: "include", }); if (!resp.ok) throw new Error(`Failed to load log entries (HTTP ${resp.status})`); const data = await resp.json(); setEntries(Array.isArray(data?.entries) ? data.entries : []); setEntriesMeta(data); applyQuickFilter(""); } catch (err) { setEntries([]); setEntriesMeta(null); setError(String(err)); } finally { setEntryLoading(false); } }, [applyQuickFilter] ); useEffect(() => { if (isAdmin) fetchLogs(); }, [fetchLogs, isAdmin]); useEffect(() => { if (!logs.length) { setSelectedDomain(null); return; } setSelectedDomain((prev) => (prev && logMap.has(prev) ? prev : logs[0].file)); }, [logs, logMap]); useEffect(() => { if (!selectedDomain) { setSelectedFile(null); return; } const domain = logMap.get(selectedDomain); if (!domain) return; const firstVersion = domain.versions?.[0]?.file || domain.file; setSelectedFile((prev) => { if (domain.versions?.some((v) => v.file === prev)) return prev; return firstVersion; }); setRetentionDraft(String(domain.retention_days ?? defaultRetention)); }, [defaultRetention, logMap, selectedDomain]); useEffect(() => { if (selectedFile) fetchEntries(selectedFile); }, [fetchEntries, selectedFile]); const selectedDomainData = selectedDomain ? logMap.get(selectedDomain) : null; const handleRetentionSave = useCallback(async () => { if (!selectedDomainData) return; const trimmed = String(retentionDraft || "").trim(); const payloadValue = trimmed ? parseInt(trimmed, 10) : null; setActionMessage(null); setError(null); try { const resp = await fetch("/api/server/logs/retention", { method: "PUT", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ retention: { [selectedDomainData.file]: Number.isFinite(payloadValue) ? payloadValue : null, }, }), }); if (!resp.ok) throw new Error(`Failed to update retention (HTTP ${resp.status})`); const data = await resp.json(); setLogs(Array.isArray(data?.logs) ? data.logs : logs); if (Array.isArray(data?.retention_deleted) && data.retention_deleted.length > 0) { setActionMessage(`Retention applied. Removed ${data.retention_deleted.length} expired files.`); } else { setActionMessage("Retention policy saved."); } } catch (err) { setError(String(err)); } }, [logs, retentionDraft, selectedDomainData]); const handleDelete = useCallback( async (scope) => { if (!selectedFile) return; const confirmMessage = scope === "family" ? "Delete this log domain and all rotated files?" : "Delete the currently selected log file?"; if (!window.confirm(confirmMessage)) return; setActionMessage(null); setError(null); try { const target = scope === "family" ? selectedDomainData?.file || selectedFile : selectedFile; const resp = await fetch(`/api/server/logs/${encodeURIComponent(target)}?scope=${scope}`, { method: "DELETE", credentials: "include", }); if (!resp.ok) throw new Error(`Failed to delete log (HTTP ${resp.status})`); const data = await resp.json(); setLogs(Array.isArray(data?.logs) ? data.logs : logs); setEntries([]); setEntriesMeta(null); setActionMessage("Log files deleted."); } catch (err) { setError(String(err)); } }, [logs, selectedDomainData, selectedFile] ); const disableRetentionSave = !selectedDomainData || (String(selectedDomainData.retention_days ?? defaultRetention) === retentionDraft.trim() && retentionDraft.trim() !== ""); if (!isAdmin) { return ( Administrator permissions are required to view log management. ); } return ( Log Management Analyze engine logs and adjust log retention periods for different engine services. {error && ( {error} )} {actionMessage && ( {actionMessage} )} Log Domain {selectedDomainData && ( <> Log File Overview Size {formatBytes(selectedDomainData.size_bytes || 0)} Last Updated {formatTimestamp(selectedDomainData.modified)} Rotations {selectedDomainData.rotation_count || 0} Retention {(selectedDomainData.retention_days ?? defaultRetention) || defaultRetention} days Retention Policy setRetentionDraft(e.target.value)} helperText={`Leave blank to inherit default (${defaultRetention} days).`} InputProps={{ inputProps: { min: 0 } }} /> )} {listLoading && ( )} val && setGridMode(val)} sx={{ backgroundColor: "rgba(0,0,0,0.25)", borderRadius: 999, p: 0.2, }} > Structured Raw applyQuickFilter(e.target.value)} sx={{ minWidth: 220, flexGrow: 1, "& .MuiOutlinedInput-root": { bgcolor: "rgba(255,255,255,0.05)", borderRadius: 999, color: AURORA_SHELL.text, }, "& .MuiInputBase-input": { color: AURORA_SHELL.text, }, }} InputProps={{ startAdornment: ( ), }} /> selectedFile && fetchEntries(selectedFile)} disabled={!selectedFile || entryLoading} sx={{ color: AURORA_SHELL.accent }} > {gridMode === "structured" ? ( {entryLoading ? ( ) : (
)}
) : ( {entryLoading ? ( ) : ( {}} highlight={(code) => Prism.highlight(code, Prism.languages.bash, "bash")} padding={16} textareaId="raw-log-viewer" textareaProps={{ readOnly: true }} style={{ fontFamily: '"IBM Plex Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace', fontSize: 13, minHeight: "100%", height: "100%", color: "#e2e8f0", background: "transparent", overflow: "auto", }} readOnly /> )} )} {entriesMeta && ( Showing {entriesMeta.returned_lines} of {entriesMeta.total_lines} lines from {entriesMeta.file} {entriesMeta.truncated ? " (truncated to the most recent lines)" : ""}. )}
); }