提交Git

This commit is contained in:
Peijun Zhao
2026-05-10 12:59:00 +08:00
commit b0aaea9fb3
2544 changed files with 545276 additions and 0 deletions
+576
View File
@@ -0,0 +1,576 @@
import {
startTransition,
useDeferredValue,
useEffect,
useEffectEvent,
useState
} from "react";
const API_BASE = import.meta.env.VITE_API_BASE || "";
async function request(path, options = {}) {
const response = await fetch(`${API_BASE}${path}`, {
headers: {
"Content-Type": "application/json",
...(options.headers || {})
},
...options
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(data.message || data.reason || "Request failed");
}
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 : {};
}
function asArray(value) {
return Array.isArray(value) ? value : [];
}
function asNumber(value, fallback = 0) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
}
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)) {
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;
}
}
return fallback;
}
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";
return "";
}
export default function App() {
const [overview, setOverview] = useState(null);
const [selectedSessionId, setSelectedSessionId] = useState("");
const [terminalLines, setTerminalLines] = useState(["[aluer] nebula console booted"]);
const [streamState, setStreamState] = useState("connecting");
const [lastHandshake, setLastHandshake] = useState(null);
const [auditFilter, setAuditFilter] = useState("");
const [commandInput, setCommandInput] = useState("");
const [sshForm, setSshForm] = useState({
alias: "",
host: "",
port: "22",
username: "",
password: "",
privateKeyPath: ""
});
const deferredAuditFilter = useDeferredValue(auditFilter);
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})`);
}
} catch (error) {
appendLine(setTerminalLines, `refresh failed: ${error.message}`);
}
});
useEffect(() => {
refreshOverview("boot");
}, [refreshOverview]);
useEffect(() => {
if (typeof window === "undefined" || typeof window.EventSource !== "function") {
setStreamState("degraded");
return;
}
const eventSource = new EventSource(`${API_BASE}/api/console/stream/overview`);
eventSource.addEventListener("overview", (event) => {
try {
const data = asRecord(JSON.parse(event.data));
startTransition(() => setOverview(data));
setStreamState("live");
} catch {
setStreamState("degraded");
}
});
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)) {
setSelectedSessionId(asText(sessions[0]?.sessionId));
}
}, [overview, selectedSessionId]);
const safeOverview = asRecord(overview);
const server = asRecord(safeOverview.server);
const metrics = asRecord(server.metrics);
const defense = asRecord(safeOverview.defense);
const shield = asRecord(defense.shield);
const sovereign = asRecord(defense.sovereign);
const network = asRecord(defense.network);
const ssh = asRecord(safeOverview.ssh);
const kernel = asRecord(safeOverview.kernel);
const modules = asArray(safeOverview.modules);
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 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.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.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)
}))
];
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);
async function handleShield(mode) {
try {
const result = await request("/api/console/shield/engage", {
method: "POST",
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);
}
refreshOverview("shield");
} catch (error) {
appendLine(setTerminalLines, `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}`
})
});
appendLine(setTerminalLines, `quick action ok: ${action}`);
if (asText(result.mode)) {
appendLine(setTerminalLines, `mode ${asText(result.mode)}`);
}
refreshOverview(action);
} catch (error) {
appendLine(setTerminalLines, `quick action failed: ${error.message}`);
}
}
async function connectSsh(event) {
event.preventDefault();
try {
const handshakeResponse = await request("/api/console/ssh/handshake", {
method: "POST",
body: JSON.stringify({
host: sshForm.host,
port: Number(sshForm.port || 22),
username: sshForm.username,
purpose: "react-console-connect"
})
});
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
};
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")}`);
setSelectedSessionId(asText(session.sessionId));
setSshForm((current) => ({ ...current, password: "" }));
refreshOverview("ssh-connect");
} catch (error) {
appendLine(setTerminalLines, `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");
return;
}
const command = commandInput.trim();
setCommandInput("");
appendLine(setTerminalLines, `${selectedSession?.alias || "session"} $ ${command}`);
try {
const result = await request("/api/console/ssh/execute", {
method: "POST",
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}`);
}
}
async function disconnectSsh(sessionId) {
try {
await request(`/api/console/ssh/${sessionId}`, { method: "DELETE" });
appendLine(setTerminalLines, `ssh disconnected: ${asText(sessionId).slice(0, 8)}`);
refreshOverview("ssh-disconnect");
} catch (error) {
appendLine(setTerminalLines, `disconnect failed: ${error.message}`);
}
}
return (
<div className="nebula-app">
<div className="ambient ambient-a" />
<div className="ambient ambient-b" />
<div className="mesh" />
<aside className="command-rail glass-panel">
<div>
<p className="eyebrow">ALUER FABRIC</p>
<h1>{asText(safeOverview.title, "Aluer Nebula Console")}</h1>
<p className="muted">{asText(safeOverview.subtitle, "PaperMC defense and recovery fabric")}</p>
</div>
<div className="stream-badge">
<span className={`stream-dot ${streamState}`} />
<strong>{streamState === "live" ? "Live Stream" : "Polling Fallback"}</strong>
</div>
<div className="rail-actions">
<button className="primary" type="button" onClick={() => refreshOverview("manual")}>刷新总览</button>
<button className="ghost" type="button" onClick={() => handleQuickAction("backup-now")}>立即备份</button>
<button className="ghost" type="button" onClick={() => handleQuickAction("shield-mirage")}>进入 Mirage</button>
<button className="ghost danger" type="button" onClick={() => handleQuickAction("shield-shelter")}>进入 Shelter</button>
</div>
<div className="rail-metric-stack">
<MetricCard label="TPS" value={formatNumber(metrics.tps, 1)} accent="mint" />
<MetricCard label="CPU" value={formatPercent(metrics.cpuUsage)} accent="sky" />
<MetricCard label="Memory" value={formatPercent(metrics.memoryUsage)} accent="sun" />
<MetricCard label="Threat" value={shield.riskScore || 0} accent="rose" />
</div>
</aside>
<main className="main-grid">
<section className="hero-panel glass-panel">
<div className="hero-copy">
<p className="eyebrow">MIRAGE SHIELD</p>
<h2>{asText(shield.currentMode, "OBSERVE")}</h2>
<p>{asText(shield.deterrenceMessage, "Deterrence message pending.")}</p>
<div className="hero-pills">
<InfoPill label="Workflow" value={asText(shield.recommendedMode, "OBSERVE")} />
<InfoPill label="Directive" value={asText(sovereign.kernelDirective?.workflow, "MONITOR_ONLY")} />
<InfoPill label="Handshake" value={engineHandshake.required ? "Required" : "Optional"} />
<InfoPill label="Queue" value={asText(kernel.taskBus?.queuedTasks, 0)} />
</div>
</div>
<div className="hero-side">
<div className="hero-meters">
<ProgressMeter label="Risk Score" value={asNumber(shield.riskScore)} />
<ProgressMeter label="Kernel Heat" value={asNumber(shield.kernelHeat || sovereign.kernelHeat)} />
<ProgressMeter label="Resonance" value={asNumber(shield.kernelResonance)} />
</div>
<div className="shield-actions">
{["FORTIFY", "MIRAGE", "SHELTER", "RECOVERY"].map((mode) => (
<button
key={mode}
type="button"
className={mode === "SHELTER" ? "primary danger" : "primary"}
onClick={() => handleShield(mode)}
>
{mode}
</button>
))}
</div>
</div>
</section>
<section className="glass-panel module-panel">
<div className="section-head">
<div>
<p className="eyebrow">MODULE CONSTELLATION</p>
<h3>互联能力矩阵</h3>
</div>
<span className="chip">nodes {sessions.length + 1}</span>
</div>
<div className="module-grid">
{modules.map((module) => (
<article key={asText(module?.id, Math.random())} className={`module-card ${severityClass(module?.signal)}`}>
<span className="module-id">{asText(module?.id, "module")}</span>
<h4>{asText(module?.name, "Unnamed Module")}</h4>
<p>{asText(module?.summary, "No summary available.")}</p>
<div className="module-foot">
<strong>{asText(module?.status, "UNKNOWN")}</strong>
<span>signal {asNumber(module?.signal)}</span>
</div>
</article>
))}
</div>
</section>
<section className="glass-panel node-panel">
<div className="section-head">
<div>
<p className="eyebrow">NODE FABRIC</p>
<h3>本地核心与远程节点</h3>
</div>
<span className="chip">{server.processRunning ? "local-online" : "local-degraded"}</span>
</div>
<div className="node-grid">
<article className="node-card local">
<strong>local / {asText(server.serviceName, "minecraft")}</strong>
<p>{asText(server.workingDir, "/opt/minecraft")}</p>
<div className="node-stats">
<span>RCON {server.rconConnected ? "online" : "offline"}</span>
<span>Players {asNumber(metrics.onlinePlayers)}</span>
<span>Conn {asNumber(metrics.connections)}</span>
</div>
</article>
{sessions.map((session) => (
<article
key={asText(session?.sessionId, `${asText(session?.alias, "session")}-${asText(session?.host, "unknown")}`)}
className={`node-card remote ${asText(session?.sessionId) === selectedSessionId ? "selected" : ""}`}
onClick={() => setSelectedSessionId(asText(session?.sessionId))}
>
<strong>{asText(session?.alias, "remote-session")}</strong>
<p>{asText(session?.username, "user")}@{asText(session?.host, "host")}:{asText(session?.port, 22)}</p>
<div className="node-stats">
<span>{asText(session?.fingerprint, "no fingerprint")}</span>
<button
type="button"
className="text-button"
onClick={(event) => {
event.stopPropagation();
disconnectSsh(asText(session?.sessionId));
}}
>
断开
</button>
</div>
</article>
))}
</div>
</section>
<section className="glass-panel ssh-panel">
<div className="section-head">
<div>
<p className="eyebrow">REMOTE SSH WORKBENCH</p>
<h3>远程控制台</h3>
</div>
<span className="chip">{selectedSession?.alias || "no session"}</span>
</div>
<div className={`handshake-banner ${lastHandshake?.approved ? "approved" : lastHandshake ? "denied" : ""}`}>
<strong>Sovereign Handshake</strong>
<span>
{lastHandshake
? `${lastHandshake.approved ? "approved" : "denied"} | ${asText(lastHandshake.shieldMode, "unknown")} | heat ${asNumber(lastHandshake.kernelHeat)} | risk ${asNumber(lastHandshake.riskScore)}`
: `required=${engineHandshake.required ? "yes" : "no"} | ttl ${asNumber(engineHandshake.ttlSeconds)}s`}
</span>
</div>
<form className="connect-form" onSubmit={connectSsh}>
<input placeholder="别名" value={sshForm.alias} onChange={(event) => setSshForm((current) => ({ ...current, alias: event.target.value }))} />
<input placeholder="主机" required value={sshForm.host} onChange={(event) => setSshForm((current) => ({ ...current, host: event.target.value }))} />
<input placeholder="端口" value={sshForm.port} onChange={(event) => setSshForm((current) => ({ ...current, port: event.target.value }))} />
<input placeholder="用户" required value={sshForm.username} onChange={(event) => setSshForm((current) => ({ ...current, username: event.target.value }))} />
<input placeholder="密码" type="password" value={sshForm.password} onChange={(event) => setSshForm((current) => ({ ...current, password: event.target.value }))} />
<input placeholder="私钥路径(可选)" value={sshForm.privateKeyPath} onChange={(event) => setSshForm((current) => ({ ...current, privateKeyPath: event.target.value }))} />
<button className="primary" type="submit">连接节点</button>
</form>
<form className="command-form" onSubmit={executeSsh}>
<input
placeholder="输入远程命令,例如 journalctl -u minecraft -n 50"
value={commandInput}
onChange={(event) => setCommandInput(event.target.value)}
/>
<button className="ghost" type="submit">执行命令</button>
</form>
<pre className="terminal-surface">{terminalLines.join("\n")}</pre>
</section>
<section className="glass-panel threat-panel">
<div className="section-head">
<div>
<p className="eyebrow">THREAT TRACE</p>
<h3>高风险来源与威慑</h3>
</div>
<span className="chip">critical {asNumber(network.criticalRiskIPs)}</span>
</div>
<div className="feed-stack">
{(asArray(shield.topOffenders).length ? asArray(shield.topOffenders) : asArray(network.topRiskIPs)).slice(0, 8).map((offender, index) => (
<article key={`${asText(offender?.ip, "unknown")}-${index}`} className={`feed-card ${severityClass(offender?.riskScore)}`}>
<strong>{asText(offender?.ip, "unknown")}</strong>
<p>{asText(offender?.riskLevel, "risk")} | score {asNumber(offender?.riskScore)}</p>
<p>{asText(offender?.summary || offender?.reason, "suspicious pressure source")}</p>
</article>
))}
</div>
</section>
<section className="glass-panel audit-panel">
<div className="section-head">
<div>
<p className="eyebrow">AUDIT STREAM</p>
<h3>审计与网络事件</h3>
</div>
<input
className="filter-input"
placeholder="筛选 audit / ip / workflow"
value={auditFilter}
onChange={(event) => setAuditFilter(event.target.value)}
/>
</div>
<div className="feed-stack">
{auditItems.map((item) => (
<article key={item.id} className={`feed-card ${severityClass(item.score)}`}>
<strong>{item.title}</strong>
<p>{item.details}</p>
</article>
))}
</div>
</section>
</main>
</div>
);
}
function MetricCard({ label, value, accent }) {
return (
<article className={`metric-card ${accent}`}>
<span>{asText(label)}</span>
<strong>{asText(value, "0")}</strong>
</article>
);
}
function ProgressMeter({ label, value }) {
const normalized = Math.max(0, Math.min(100, Number(value) || 0));
return (
<div className="progress-meter">
<div className="meter-head">
<span>{label}</span>
<strong>{normalized}</strong>
</div>
<div className="meter-track">
<span style={{ width: `${normalized}%` }} />
</div>
</div>
);
}
function InfoPill({ label, value }) {
return (
<div className="info-pill">
<span>{asText(label)}</span>
<strong>{asText(value, "-")}</strong>
</div>
);
}
+43
View File
@@ -0,0 +1,43 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./styles.css";
class RootErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
}
static getDerivedStateFromError(error) {
return { error };
}
componentDidCatch(error, info) {
console.error("Aluer Nebula Console crashed", error, info);
}
render() {
if (this.state.error) {
return (
<div className="fatal-shell">
<div className="fatal-card">
<p className="eyebrow">ALUER UI RECOVERY</p>
<h1>界面加载失败</h1>
<p>前端捕获到了运行时错误,但主程序仍可能正常运行。请刷新页面,或替换为最新构建包。</p>
<pre>{String(this.state.error?.message || this.state.error)}</pre>
</div>
</div>
);
}
return this.props.children;
}
}
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RootErrorBoundary>
<App />
</RootErrorBoundary>
</React.StrictMode>
);
+629
View File
@@ -0,0 +1,629 @@
:root {
--bg: #06111e;
--text: #ecf7ff;
--muted: #8fa7c2;
--border: rgba(196, 225, 255, 0.12);
--glass: rgba(9, 21, 37, 0.68);
--shadow: 0 28px 72px rgba(1, 7, 16, 0.42);
--mint: #63e7c2;
--sky: #60cbff;
--sun: #ffc86b;
--rose: #ff7f96;
}
* {
box-sizing: border-box;
}
html,
body,
#root {
min-height: 100%;
}
body {
margin: 0;
font-family: "Avenir Next", "SF Pro Display", "Segoe UI Variable", "PingFang SC", "Microsoft YaHei", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(68, 139, 255, 0.28), transparent 34%),
radial-gradient(circle at 80% 20%, rgba(56, 217, 183, 0.18), transparent 26%),
linear-gradient(160deg, #050d18 0%, #081420 48%, #040914 100%);
overflow-x: hidden;
}
.boot-shell,
.fatal-shell {
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
}
.boot-card,
.fatal-card {
width: min(560px, 100%);
border-radius: 28px;
padding: 28px;
border: 1px solid rgba(196, 225, 255, 0.12);
background: rgba(9, 21, 37, 0.74);
backdrop-filter: blur(18px) saturate(145%);
-webkit-backdrop-filter: blur(18px) saturate(145%);
box-shadow: var(--shadow);
}
.boot-card h1,
.fatal-card h1 {
margin: 10px 0;
letter-spacing: -0.03em;
}
.fatal-card pre {
margin: 18px 0 0;
padding: 14px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.05);
color: var(--sun);
white-space: pre-wrap;
word-break: break-word;
}
body::selection {
background: rgba(96, 203, 255, 0.3);
}
::-webkit-scrollbar {
width: 11px;
height: 11px;
}
::-webkit-scrollbar-thumb {
background: rgba(130, 163, 196, 0.36);
border-radius: 999px;
border: 2px solid rgba(4, 9, 20, 0.4);
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.03);
}
button,
input {
font: inherit;
}
.nebula-app {
position: relative;
min-height: 100vh;
display: grid;
grid-template-columns: 320px 1fr;
gap: 20px;
padding: 20px;
}
.ambient,
.mesh {
position: fixed;
pointer-events: none;
}
.ambient {
width: 360px;
height: 360px;
border-radius: 50%;
filter: blur(26px);
opacity: 0.42;
animation: drift 18s ease-in-out infinite;
}
.ambient-a {
top: 28px;
left: 4%;
background: radial-gradient(circle, rgba(82, 143, 255, 0.56), transparent 70%);
}
.ambient-b {
right: 10%;
bottom: 2%;
background: radial-gradient(circle, rgba(88, 229, 202, 0.32), transparent 70%);
animation-delay: -6s;
}
.mesh {
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
background-size: 32px 32px;
mask-image: radial-gradient(circle at center, rgba(255, 255, 255, 0.75), transparent 82%);
}
.glass-panel {
position: relative;
z-index: 1;
border-radius: 30px;
background: var(--glass);
border: 1px solid var(--border);
backdrop-filter: blur(18px) saturate(145%);
-webkit-backdrop-filter: blur(18px) saturate(145%);
box-shadow: var(--shadow);
overflow: hidden;
transition: transform 240ms ease, border-color 240ms ease, box-shadow 240ms ease;
}
.glass-panel::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.09), transparent 32%, transparent 65%, rgba(99, 231, 194, 0.05));
pointer-events: none;
}
.glass-panel:hover {
transform: translateY(-2px);
border-color: rgba(99, 231, 194, 0.18);
box-shadow: 0 34px 82px rgba(1, 7, 16, 0.46);
}
.command-rail {
padding: 24px;
display: grid;
align-content: start;
gap: 18px;
position: sticky;
top: 20px;
height: calc(100vh - 40px);
}
.command-rail h1,
.main-grid h2,
.main-grid h3,
.module-card h4 {
margin: 8px 0;
letter-spacing: -0.03em;
}
.eyebrow,
.module-id {
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.18em;
font-size: 0.76rem;
}
.muted {
color: var(--muted);
line-height: 1.5;
}
.stream-badge,
.chip,
.metric-card,
.module-card,
.node-card,
.feed-card,
.terminal-surface {
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.05);
}
.stream-badge {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-radius: 20px;
}
.stream-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #9faec2;
box-shadow: 0 0 18px rgba(159, 174, 194, 0.5);
}
.stream-dot.live {
background: var(--mint);
box-shadow: 0 0 22px rgba(99, 231, 194, 0.78);
}
.stream-dot.degraded,
.stream-dot.connecting {
background: var(--sun);
box-shadow: 0 0 22px rgba(255, 200, 107, 0.68);
}
.rail-actions,
.shield-actions {
display: grid;
gap: 10px;
}
.rail-metric-stack {
display: grid;
gap: 12px;
}
.metric-card {
padding: 16px;
border-radius: 22px;
}
.metric-card span {
display: block;
color: var(--muted);
font-size: 0.8rem;
}
.metric-card strong {
display: block;
margin-top: 10px;
font-size: 2rem;
}
.metric-card.mint strong { color: var(--mint); }
.metric-card.sky strong { color: var(--sky); }
.metric-card.sun strong { color: var(--sun); }
.metric-card.rose strong { color: var(--rose); }
.main-grid {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
}
.hero-panel,
.module-panel,
.node-panel,
.ssh-panel,
.threat-panel,
.audit-panel {
padding: 22px;
}
.hero-panel {
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 24px;
min-height: 320px;
}
.hero-panel::after {
content: "";
position: absolute;
right: -60px;
top: -50px;
width: 240px;
height: 240px;
background: radial-gradient(circle, rgba(99, 231, 194, 0.18), transparent 68%);
pointer-events: none;
}
.hero-copy p:last-child {
color: #d8edff;
margin: 0;
line-height: 1.6;
}
.hero-copy {
display: grid;
align-content: start;
gap: 14px;
}
.hero-side {
display: grid;
align-content: space-between;
gap: 18px;
}
.hero-pills {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.info-pill {
padding: 14px 16px;
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.05);
}
.info-pill span {
display: block;
color: var(--muted);
font-size: 0.78rem;
}
.info-pill strong {
display: block;
margin-top: 8px;
font-size: 1rem;
color: #f2f9ff;
}
.hero-meters,
.feed-stack,
.module-grid {
display: grid;
gap: 14px;
}
.module-panel,
.ssh-panel {
grid-column: 1 / -1;
}
.section-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 16px;
}
.chip {
display: inline-flex;
align-items: center;
padding: 9px 12px;
border-radius: 999px;
color: #d6effd;
font-size: 0.84rem;
}
.progress-meter {
display: grid;
gap: 8px;
}
.meter-head {
display: flex;
justify-content: space-between;
align-items: center;
}
.meter-head span {
color: var(--muted);
}
.meter-track {
height: 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
overflow: hidden;
}
.meter-track span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #53bcff, #63e7c2 55%, #ffc86b 100%);
transition: width 360ms ease;
}
.primary,
.ghost,
.text-button {
border: 0;
cursor: pointer;
transition: transform 180ms ease, background 180ms ease, opacity 180ms ease;
}
.primary,
.ghost {
border-radius: 16px;
padding: 12px 16px;
}
.primary {
background: linear-gradient(135deg, #36d8b0, #1da8f1);
color: #031019;
box-shadow: 0 14px 28px rgba(45, 168, 241, 0.18);
}
.primary.danger {
background: linear-gradient(135deg, #ff9d7e, #ff617f);
}
.ghost {
background: rgba(255, 255, 255, 0.07);
color: var(--text);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.ghost.danger {
border-color: rgba(255, 127, 150, 0.3);
color: #ffdce3;
}
.text-button {
background: transparent;
color: var(--sky);
padding: 0;
}
button:hover {
transform: translateY(-1px);
}
.module-grid {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.module-card,
.node-card,
.feed-card {
border-radius: 22px;
padding: 16px;
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease;
}
.module-card:hover,
.node-card:hover,
.feed-card:hover {
transform: translateY(-1px);
}
.module-card p,
.node-card p,
.feed-card p {
margin: 6px 0 0;
color: var(--muted);
line-height: 1.5;
}
.module-card.warning,
.node-card.selected,
.feed-card.warning {
border-color: rgba(255, 200, 107, 0.28);
}
.module-card.danger,
.feed-card.danger {
border-color: rgba(255, 127, 150, 0.38);
}
.module-foot,
.node-stats {
display: flex;
justify-content: space-between;
gap: 12px;
margin-top: 14px;
color: #d7ecfb;
font-size: 0.86rem;
}
.node-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px;
}
.node-card {
cursor: pointer;
}
.node-card.local {
background: linear-gradient(135deg, rgba(99, 231, 194, 0.12), rgba(31, 112, 255, 0.08));
}
.node-card.selected {
box-shadow: inset 0 0 0 1px rgba(255, 200, 107, 0.18);
}
.connect-form,
.command-form {
display: grid;
gap: 10px;
}
.handshake-banner {
display: grid;
gap: 6px;
margin-bottom: 14px;
padding: 14px 16px;
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.05);
}
.handshake-banner strong {
font-size: 0.92rem;
}
.handshake-banner span {
color: var(--muted);
font-size: 0.86rem;
}
.handshake-banner.approved {
border-color: rgba(99, 231, 194, 0.32);
}
.handshake-banner.denied {
border-color: rgba(255, 127, 150, 0.36);
}
.connect-form {
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.command-form {
grid-template-columns: 1fr auto;
margin-top: 12px;
}
input {
width: 100%;
min-width: 0;
padding: 12px 14px;
color: var(--text);
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.06);
outline: none;
}
input:focus {
border-color: rgba(96, 203, 255, 0.42);
box-shadow: 0 0 0 4px rgba(96, 203, 255, 0.08);
}
.terminal-surface {
margin-top: 12px;
min-height: 320px;
max-height: 420px;
overflow: auto;
padding: 18px;
border-radius: 24px;
font-family: "SF Mono", "Cascadia Code", "JetBrains Mono", monospace;
font-size: 0.9rem;
line-height: 1.6;
white-space: pre-wrap;
background:
linear-gradient(180deg, rgba(3, 11, 20, 0.92), rgba(6, 18, 30, 0.88)),
radial-gradient(circle at top right, rgba(96, 203, 255, 0.08), transparent 30%);
}
.filter-input {
max-width: 260px;
}
@keyframes drift {
0%,
100% { transform: translate3d(0, 0, 0); }
50% { transform: translate3d(0, -18px, 0) scale(1.04); }
}
@media (max-width: 1220px) {
.nebula-app {
grid-template-columns: 1fr;
}
.command-rail {
order: 2;
position: static;
height: auto;
}
}
@media (max-width: 900px) {
.main-grid,
.connect-form,
.node-grid {
grid-template-columns: 1fr;
}
.hero-panel,
.hero-pills {
grid-template-columns: 1fr;
}
.section-head {
flex-direction: column;
}
}