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.
}
onClick={() => {
fetchLogs();
if (selectedFile) fetchEntries(selectedFile);
}}
sx={gradientButtonSx}
>
Refresh
{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 } }}
/>
}
onClick={handleRetentionSave}
disabled={!selectedDomainData || disableRetentionSave}
sx={{
...gradientButtonSx,
opacity: !selectedDomainData || disableRetentionSave ? 0.5 : 1,
}}
>
Save Retention
}
onClick={() => handleDelete("file")}
sx={{
color: "#f97316",
borderColor: "rgba(249,115,22,0.6)",
textTransform: "none",
fontWeight: 600,
}}
>
Delete File
}
onClick={() => handleDelete("family")}
sx={{
color: "#f43f5e",
borderColor: "rgba(244,63,94,0.6)",
textTransform: "none",
fontWeight: 600,
}}
>
Purge Domain
>
)}
{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)" : ""}.
)}
);
}