{asText(module?.name, "Unnamed Module")}
-{asText(module?.summary, "No summary available.")}
+ {modules.map((mod) => ( +{asText(mod?.name, "Unnamed Module")}
+{asText(mod?.summary, "No summary available.")}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0c5d8c2..5e6abda 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,6 +3,7 @@ import { useDeferredValue, useEffect, useEffectEvent, + useReducer, useState } from "react"; @@ -23,19 +24,8 @@ async function request(path, options = {}) { return data; } -function appendLine(setTerminalLines, line) { - setTerminalLines((current) => { - const next = [...current, `[${new Date().toLocaleTimeString("zh-CN", { hour12: false })}] ${asText(line, "event")}`]; - return next.slice(-120); - }); -} - -function isRecord(value) { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - function asRecord(value) { - return isRecord(value) ? value : {}; + return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {}; } function asArray(value) { @@ -48,79 +38,84 @@ function asNumber(value, fallback = 0) { } function asText(value, fallback = "") { - if (value === null || value === undefined) { - return fallback; - } - if (typeof value === "string") { - return value; - } - if (typeof value === "number" || typeof value === "boolean") { - return String(value); - } - if (isRecord(value)) { + if (value === null || value === undefined) return fallback; + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") return String(value); + if (asRecord(value)) { for (const key of ["summary", "message", "reason", "workflow", "mode", "status", "name", "id"]) { const nested = value[key]; - if (typeof nested === "string" || typeof nested === "number" || typeof nested === "boolean") { - return String(nested); - } - } - try { - return JSON.stringify(value); - } catch { - return fallback; + if (typeof nested === "string" || typeof nested === "number" || typeof nested === "boolean") return String(nested); } + try { return JSON.stringify(value); } catch { return fallback; } } return fallback; } -function formatPercent(value) { - return `${asNumber(value).toFixed(1)}%`; -} - -function formatNumber(value, digits = 0) { - return asNumber(value).toFixed(digits); -} - +function formatPercent(value) { return `${asNumber(value).toFixed(1)}%`; } +function formatNumber(value, digits = 0) { return asNumber(value).toFixed(digits); } function severityClass(score) { - const normalized = asNumber(score); - if (normalized >= 85) return "danger"; - if (normalized >= 60) return "warning"; + const n = asNumber(score); + if (n >= 85) return "danger"; + if (n >= 60) return "warning"; return ""; } +function terminalReducer(state, action) { + switch (action.type) { + case "append": + return [...state, `[${new Date().toLocaleTimeString("zh-CN", { hour12: false })}] ${action.line}`].slice(-120); + case "clear": + return []; + default: + return state; + } +} + export default function App() { const [overview, setOverview] = useState(null); + const [health, setHealth] = useState(null); const [selectedSessionId, setSelectedSessionId] = useState(""); - const [terminalLines, setTerminalLines] = useState(["[aluer] nebula console booted"]); + const [terminalLines, dispatchTerminal] = useReducer(terminalReducer, ["[aluer] nebula console booted"]); const [streamState, setStreamState] = useState("connecting"); const [lastHandshake, setLastHandshake] = useState(null); const [auditFilter, setAuditFilter] = useState(""); const [commandInput, setCommandInput] = useState(""); + const [autoRefresh, setAutoRefresh] = useState(true); + const [refreshInterval, setRefreshInterval] = useState(6); + const [showHealth, setShowHealth] = useState(false); const [sshForm, setSshForm] = useState({ - alias: "", - host: "", - port: "22", - username: "", - password: "", - privateKeyPath: "" + alias: "", host: "", port: "22", username: "", password: "", privateKeyPath: "" }); const deferredAuditFilter = useDeferredValue(auditFilter); + const addLine = (line) => dispatchTerminal({ type: "append", line }); + const refreshOverview = useEffectEvent(async (reason = "manual") => { try { const data = asRecord(await request("/api/console/overview")); startTransition(() => setOverview(data)); - if (reason !== "stream") { - appendLine(setTerminalLines, `overview refreshed (${reason})`); - } + if (reason !== "stream") addLine(`overview refreshed (${reason})`); } catch (error) { - appendLine(setTerminalLines, `refresh failed: ${error.message}`); + addLine(`refresh failed: ${error.message}`); } }); + const refreshHealth = useEffectEvent(async () => { + try { + const data = asRecord(await request("/api/health")); + startTransition(() => setHealth(data)); + } catch { + // health endpoint may not be deployed yet + } + }); + + useEffect(() => { refreshOverview("boot"); refreshHealth(); }, [refreshOverview, refreshHealth]); + useEffect(() => { - refreshOverview("boot"); - }, [refreshOverview]); + if (!autoRefresh) return; + const id = setInterval(() => { refreshOverview("poll"); refreshHealth(); }, refreshInterval * 1000); + return () => clearInterval(id); + }, [autoRefresh, refreshInterval, refreshOverview, refreshHealth]); useEffect(() => { if (typeof window === "undefined" || typeof window.EventSource !== "function") { @@ -133,30 +128,22 @@ export default function App() { const data = asRecord(JSON.parse(event.data)); startTransition(() => setOverview(data)); setStreamState("live"); - } catch { - setStreamState("degraded"); - } + } catch { setStreamState("degraded"); } }); - eventSource.onerror = () => { - setStreamState("degraded"); - }; - return () => { - eventSource.close(); - }; + eventSource.onerror = () => setStreamState("degraded"); + return () => eventSource.close(); }, []); useEffect(() => { const sessions = asArray(overview?.ssh?.sessions); - if (!sessions.length) { - setSelectedSessionId(""); - return; - } - if (!selectedSessionId || !sessions.some((session) => asText(session?.sessionId) === selectedSessionId)) { + if (!sessions.length) { setSelectedSessionId(""); return; } + if (!selectedSessionId || !sessions.some((s) => asText(s?.sessionId) === selectedSessionId)) { setSelectedSessionId(asText(sessions[0]?.sessionId)); } }, [overview, selectedSessionId]); const safeOverview = asRecord(overview); + const safeHealth = asRecord(health); const server = asRecord(safeOverview.server); const metrics = asRecord(server.metrics); const defense = asRecord(safeOverview.defense); @@ -169,70 +156,57 @@ export default function App() { const audit = asRecord(safeOverview.audit); const sessions = asArray(ssh.sessions); const engineHandshake = asRecord(ssh.engineHandshake); - const selectedSession = sessions.find((session) => asText(session?.sessionId) === selectedSessionId); + const selectedSession = sessions.find((s) => asText(s?.sessionId) === selectedSessionId); + const healthComponents = asRecord(safeHealth.components); + const healthSystem = asRecord(safeHealth.system); + const mergedAuditItems = [ - ...asArray(audit.events).map((event) => ({ - id: `${asText(event?.category, "event")}-${asText(event?.action, "action")}-${asText(event?.timestamp, Date.now())}`, - title: `${asText(event?.category, "EVENT")} / ${asText(event?.action, "ACTION")}`, - details: `${asText(event?.player, "system")} | ${asText(event?.details, "")}`, - score: asText(event?.category).includes("ALERT") ? 90 : 45 + ...asArray(audit.events).map((e) => ({ + id: `${asText(e?.category, "event")}-${asText(e?.action, "action")}-${asText(e?.timestamp, Date.now())}`, + title: `${asText(e?.category, "EVENT")} / ${asText(e?.action, "ACTION")}`, + details: `${asText(e?.player, "system")} | ${asText(e?.details, "")}`, + score: asText(e?.category).includes("ALERT") ? 90 : 45 })), - ...asArray(audit.networkIncidents).map((incident, index) => ({ - id: `${asText(incident?.type, "incident")}-${asText(incident?.ip, "unknown")}-${index}`, - title: `${asText(incident?.type, "incident")} / ${asText(incident?.ip, "unknown")}`, - details: asText(incident?.details || incident?.summary, "network anomaly"), - score: asNumber(incident?.riskScore, 70) + ...asArray(audit.networkIncidents).map((inc, i) => ({ + id: `${asText(inc?.type, "incident")}-${asText(inc?.ip, "unknown")}-${i}`, + title: `${asText(inc?.type, "incident")} / ${asText(inc?.ip, "unknown")}`, + details: asText(inc?.details || inc?.summary, "network anomaly"), + score: asNumber(inc?.riskScore, 70) })), - ...asArray(audit.shieldTransitions).map((transition, index) => ({ - id: `${asText(transition?.mode, "shield")}-${asText(transition?.timestamp, Date.now())}-${index}`, - title: `SHIELD / ${asText(transition?.mode, "TRANSITION")}`, - details: `${asText(transition?.reason, "policy update")} | risk ${asText(transition?.riskScore, 80)}`, - score: asNumber(transition?.riskScore, 80) + ...asArray(audit.shieldTransitions).map((t, i) => ({ + id: `${asText(t?.mode, "shield")}-${asText(t?.timestamp, Date.now())}-${i}`, + title: `SHIELD / ${asText(t?.mode, "TRANSITION")}`, + details: `${asText(t?.reason, "policy update")} | risk ${asText(t?.riskScore, 80)}`, + score: asNumber(t?.riskScore, 80) })) ]; const auditQuery = deferredAuditFilter.trim().toLowerCase(); const auditItems = !auditQuery - ? mergedAuditItems.slice(0, 18) - : mergedAuditItems.filter((item) => - `${item.title} ${item.details}`.toLowerCase().includes(auditQuery) - ).slice(0, 18); + ? mergedAuditItems.slice(0, 24) + : mergedAuditItems.filter((item) => `${item.title} ${item.details}`.toLowerCase().includes(auditQuery)).slice(0, 24); async function handleShield(mode) { try { const result = await request("/api/console/shield/engage", { method: "POST", - body: JSON.stringify({ - mode, - reason: `react-console-${mode.toLowerCase()}` - }) + body: JSON.stringify({ mode, reason: `react-console-${mode.toLowerCase()}` }) }); - appendLine(setTerminalLines, `shield engaged: ${asText(result.mode, "unknown")}`); - if (asText(result.deterrenceMessage)) { - appendLine(setTerminalLines, result.deterrenceMessage); - } + addLine(`shield engaged: ${asText(result.mode, "unknown")}`); + if (asText(result.deterrenceMessage)) addLine(result.deterrenceMessage); refreshOverview("shield"); - } catch (error) { - appendLine(setTerminalLines, `shield error: ${error.message}`); - } + } catch (error) { addLine(`shield error: ${error.message}`); } } async function handleQuickAction(action) { try { const result = await request("/api/console/quick-action", { method: "POST", - body: JSON.stringify({ - action, - reason: `react-console-${action}` - }) + body: JSON.stringify({ action, reason: `react-console-${action}` }) }); - appendLine(setTerminalLines, `quick action ok: ${action}`); - if (asText(result.mode)) { - appendLine(setTerminalLines, `mode ${asText(result.mode)}`); - } + addLine(`quick action ok: ${action}`); + if (asText(result.mode)) addLine(`mode ${asText(result.mode)}`); refreshOverview(action); - } catch (error) { - appendLine(setTerminalLines, `quick action failed: ${error.message}`); - } + } catch (error) { addLine(`quick action failed: ${error.message}`); } } async function connectSsh(event) { @@ -249,88 +223,64 @@ export default function App() { }); const handshake = asRecord(handshakeResponse.handshake); setLastHandshake(handshake); - appendLine( - setTerminalLines, - `handshake ${handshake.approved ? "approved" : "denied"} | mode=${asText(handshake.shieldMode, "unknown")} heat=${asNumber(handshake.kernelHeat)} risk=${asNumber(handshake.riskScore)}` - ); - appendLine(setTerminalLines, asText(handshake.message, "handshake completed")); - if (!handshake.approved) { - return; - } - - const payload = { - ...sshForm, - port: Number(sshForm.port || 22), - handshakeToken: handshake.token - }; + addLine(`handshake ${handshake.approved ? "approved" : "denied"} | mode=${asText(handshake.shieldMode, "unknown")} heat=${asNumber(handshake.kernelHeat)} risk=${asNumber(handshake.riskScore)}`); + addLine(asText(handshake.message, "handshake completed")); + if (!handshake.approved) return; + const payload = { ...sshForm, port: Number(sshForm.port || 22), handshakeToken: handshake.token }; const result = await request("/api/console/ssh/connect", { method: "POST", body: JSON.stringify(payload) }); const session = asRecord(result.session); - appendLine(setTerminalLines, `ssh connected: ${asText(session.alias, "remote-session")}`); + addLine(`ssh connected: ${asText(session.alias, "remote-session")}`); setSelectedSessionId(asText(session.sessionId)); - setSshForm((current) => ({ ...current, password: "" })); + setSshForm((c) => ({ ...c, password: "" })); refreshOverview("ssh-connect"); - } catch (error) { - appendLine(setTerminalLines, `ssh connect failed: ${error.message}`); - } + } catch (error) { addLine(`ssh connect failed: ${error.message}`); } } async function executeSsh(event) { event.preventDefault(); if (!selectedSessionId || !commandInput.trim()) { - appendLine(setTerminalLines, "select a session and enter a command first"); + addLine("select a session and enter a command first"); return; } const command = commandInput.trim(); setCommandInput(""); - appendLine(setTerminalLines, `${selectedSession?.alias || "session"} $ ${command}`); + addLine(`${selectedSession?.alias || "session"} $ ${command}`); try { const result = await request("/api/console/ssh/execute", { method: "POST", - body: JSON.stringify({ - sessionId: selectedSessionId, - command - }) + body: JSON.stringify({ sessionId: selectedSessionId, command }) }); const sshResult = asRecord(result.result); - if (sshResult.flagged) { - appendLine(setTerminalLines, `guard: ${asText(sshResult.guardType, "unknown")} severity=${asNumber(sshResult.guardSeverity)}`); - } - if (asText(sshResult.stdout)) { - appendLine(setTerminalLines, sshResult.stdout); - } - if (asText(sshResult.stderr)) { - appendLine(setTerminalLines, `stderr: ${asText(sshResult.stderr)}`); - } - appendLine(setTerminalLines, `exit=${asNumber(sshResult.exitStatus)} in ${asNumber(sshResult.durationMs)}ms`); - } catch (error) { - appendLine(setTerminalLines, `ssh execute failed: ${error.message}`); - } + if (sshResult.flagged) addLine(`guard: ${asText(sshResult.guardType, "unknown")} severity=${asNumber(sshResult.guardSeverity)}`); + if (asText(sshResult.stdout)) addLine(sshResult.stdout); + if (asText(sshResult.stderr)) addLine(`stderr: ${asText(sshResult.stderr)}`); + addLine(`exit=${asNumber(sshResult.exitStatus)} in ${asNumber(sshResult.durationMs)}ms`); + } catch (error) { addLine(`ssh execute failed: ${error.message}`); } } async function disconnectSsh(sessionId) { try { await request(`/api/console/ssh/${sessionId}`, { method: "DELETE" }); - appendLine(setTerminalLines, `ssh disconnected: ${asText(sessionId).slice(0, 8)}`); + addLine(`ssh disconnected: ${asText(sessionId).slice(0, 8)}`); refreshOverview("ssh-disconnect"); - } catch (error) { - appendLine(setTerminalLines, `disconnect failed: ${error.message}`); - } + } catch (error) { addLine(`disconnect failed: ${error.message}`); } } return (
PERFORMANCE TREND
+{asText(module?.summary, "No summary available.")}
+ {modules.map((mod) => ( +{asText(mod?.summary, "No summary available.")}
{asText(session?.username, "user")}@{asText(session?.host, "host")}:{asText(session?.port, 22)}
{terminalLines.join("\n")}
{asText(offender?.riskLevel, "risk")} | score {asNumber(offender?.riskScore)}
{asText(offender?.summary || offender?.reason, "suspicious pressure source")}
@@ -521,12 +496,8 @@ export default function App() {AUDIT STREAM