improved rev console

This commit is contained in:
KingKDot
2026-05-13 22:32:49 -05:00
parent 27225566f4
commit 6ae44625f8
17 changed files with 446 additions and 471 deletions
+49 -11
View File
@@ -1,26 +1,15 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "overlord-server",
"dependencies": {
"@fontsource/inter": "^5.2.8",
"@fontsource/jetbrains-mono": "^5.2.8",
"@fortawesome/fontawesome-free": "6.5.2",
"@msgpack/msgpack": "^3.0.0",
"@solana/web3.js": "^1.98.4",
"ace-builds": "1.36.5",
"adm-zip": "^0.5.12",
"animejs": "3.2.2",
"ansi-to-html": "0.7.2",
"chart.js": "4.4.0",
"codemirror": "5.65.16",
"flag-icons": "latest",
"geoip-lite": "latest",
"highlight.js": "11.9.0",
"jose": "^6.1.3",
"leaflet": "1.9.4",
"msgpackr": "1.11.8",
"sharp": "^0.34.5",
"uuid": "^9.0.1",
@@ -28,13 +17,26 @@
"winston": "^3.11.0",
},
"devDependencies": {
"@fontsource/inter": "^5.2.8",
"@fontsource/jetbrains-mono": "^5.2.8",
"@fortawesome/fontawesome-free": "6.5.2",
"@tailwindcss/cli": "^4.1.18",
"@types/bun": "^1.3.9",
"@types/geoip-lite": "latest",
"@types/uuid": "^9.0.7",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"ace-builds": "1.36.5",
"animejs": "3.2.2",
"autoprefixer": "^10.4.23",
"chart.js": "4.4.0",
"clean-css": "^5.3.3",
"codemirror": "5.65.16",
"flag-icons": "latest",
"highlight.js": "11.9.0",
"html-minifier-terser": "^7.2.0",
"leaflet": "1.9.4",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"terser": "^5.46.1",
@@ -225,6 +227,12 @@
"@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="],
"@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="],
"@xterm/addon-web-links": ["@xterm/addon-web-links@0.12.0", "", {}, "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw=="],
"@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="],
"ace-builds": ["ace-builds@1.36.5", "", {}, "sha512-mZ5KVanRT6nLRDLqtG/1YQQLX/gZVC/v526cm1Ru/MTSlrbweSmqv2ZT0d2GaHpJq035MwCMIrj+LgDAUnDXrg=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
@@ -537,6 +545,8 @@
"yauzl": ["yauzl@3.2.1", "", { "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" } }, "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A=="],
"@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@solana/errors/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"@solana/errors/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
@@ -577,6 +587,34 @@
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"color/color-convert/color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
}
}
+3
View File
@@ -35,6 +35,9 @@
"@types/bun": "^1.3.9",
"@types/geoip-lite": "latest",
"@types/uuid": "^9.0.7",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"ace-builds": "1.36.5",
"animejs": "3.2.2",
"autoprefixer": "^10.4.23",
+68 -103
View File
@@ -10,58 +10,39 @@
flex-direction: column;
}
.console-topbar {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 14px 18px;
background: rgba(7, 12, 22, 0.9);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
backdrop-filter: blur(10px);
}
.console-topbar .brand {
display: inline-flex;
align-items: center;
gap: 10px;
font-weight: 600;
letter-spacing: 0.3px;
}
.console-topbar .console-actions {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.console-main {
flex: 1;
padding: 18px;
display: grid;
place-items: center;
box-sizing: border-box;
}
.console-card {
width: min(1100px, 96vw);
max-width: 1280px;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background: rgba(8, 12, 22, 0.8);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 14px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4);
display: flex;
flex-direction: column;
gap: 14px;
padding: 16px;
gap: 12px;
padding: 14px;
}
.console-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.console-meta {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 10px;
grid-template-columns: repeat(auto-fit, minmax(120px, max-content));
gap: 14px 24px;
color: #a9b8d4;
}
@@ -70,67 +51,37 @@
color: #e5edff;
}
.console-output {
min-height: 420px;
max-height: 70vh;
.console-toolbar {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.console-terminal {
min-height: 65vh;
width: 100%;
max-width: 100%;
min-width: 0;
background: #050913;
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 12px;
padding: 14px;
overflow: auto;
overflow-x: auto;
box-sizing: border-box;
white-space: normal;
text-align: left;
overflow-wrap: normal;
word-break: normal;
line-height: 1.5;
font-size: 14px;
color: #e8edf2;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
}
.console-pre {
margin: 0;
white-space: pre;
display: block;
width: max-content;
min-width: 0;
text-align: left;
}
.console-input-row {
display: flex;
gap: 10px;
align-items: center;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 10px;
padding: 10px 12px;
box-sizing: border-box;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
overflow: hidden;
}
.console-input-row .prompt {
color: #6ee7b7;
.console-terminal .xterm,
.console-terminal .xterm-viewport,
.console-terminal .xterm-screen {
background: transparent !important;
}
.console-input-row textarea {
flex: 1;
resize: vertical;
min-height: 44px;
max-height: 160px;
background: transparent;
border: none;
color: #e8edf2;
font-size: 14px;
outline: none;
.console-terminal .xterm-viewport::-webkit-scrollbar {
width: 10px;
}
.console-buttons {
display: flex;
gap: 8px;
.console-terminal .xterm-viewport::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.12);
border-radius: 8px;
}
.btn,
@@ -160,12 +111,35 @@
background: rgba(255, 255, 255, 0.06);
}
.status-pill {
padding: 8px 12px;
.pill {
padding: 6px 10px;
border-radius: 999px;
font-size: 13px;
font-size: 12px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.04);
display: inline-flex;
align-items: center;
gap: 6px;
}
.pill i {
font-size: 8px;
}
.pill.pill-online {
color: #6ee7b7;
border-color: rgba(110, 231, 183, 0.35);
background: rgba(110, 231, 183, 0.08);
}
.pill.pill-offline {
color: #fca5a5;
border-color: rgba(252, 165, 165, 0.35);
background: rgba(252, 165, 165, 0.08);
}
.pill.pill-ghost {
color: #cbd5e1;
}
@media (max-width: 720px) {
@@ -173,21 +147,12 @@
background: #050913;
}
.console-main {
padding: 12px;
padding: 10px;
}
.console-card {
padding: 12px;
padding: 10px;
}
.console-input-row {
flex-direction: column;
align-items: stretch;
}
.console-buttons {
width: 100%;
justify-content: flex-end;
}
.console-buttons .btn,
.console-buttons .button {
width: auto;
.console-terminal {
min-height: 70vh;
}
}
+134 -75
View File
@@ -1,33 +1,72 @@
import { ansiToHtml } from "./ansi.js";
import { Terminal } from "/vendor/xterm/xterm.mjs";
import { FitAddon } from "/vendor/xterm/addon-fit.mjs";
import { WebLinksAddon } from "/vendor/xterm/addon-web-links.mjs";
import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
import { checkFeatureAccess } from "./feature-gate.js";
const outputEl = document.getElementById("console-output");
const containerEl = document.getElementById("terminal");
const statusPill = document.getElementById("status-pill");
const clientLabel = document.getElementById("client-label");
const hostLabel = document.getElementById("host-label");
const userLabel = document.getElementById("user-label");
const osLabel = document.getElementById("os-label");
const form = document.getElementById("console-form");
const input = document.getElementById("console-input");
const ctrlcBtn = document.getElementById("ctrlc-btn");
const reconnectBtn = document.getElementById("reconnect-btn");
const clearBtn = document.getElementById("clear-btn");
const clientId = decodeURIComponent(
location.pathname.split("/").filter(Boolean)[0] || "",
);
const wsProto = location.protocol === "https:" ? "wss" : "ws";
const wsUrl = `${wsProto}://${location.host}/api/clients/${encodeURIComponent(clientId)}/console/ws`;
const term = new Terminal({
cursorBlink: true,
fontFamily: '"JetBrains Mono", ui-monospace, Menlo, Consolas, monospace',
fontSize: 13,
scrollback: 10000,
allowProposedApi: true,
theme: {
background: "#050913",
foreground: "#e8edf2",
cursor: "#6ee7b7",
cursorAccent: "#050913",
selectionBackground: "rgba(110, 231, 183, 0.25)",
black: "#1a1d29",
red: "#ef4444",
green: "#22c55e",
yellow: "#eab308",
blue: "#60a5fa",
magenta: "#c084fc",
cyan: "#22d3ee",
white: "#e8edf2",
brightBlack: "#475569",
brightRed: "#f87171",
brightGreen: "#4ade80",
brightYellow: "#facc15",
brightBlue: "#93c5fd",
brightMagenta: "#d8b4fe",
brightCyan: "#67e8f9",
brightWhite: "#f8fafc",
},
});
const fit = new FitAddon();
term.loadAddon(fit);
term.loadAddon(new WebLinksAddon());
term.open(containerEl);
let ws = null;
let connected = false;
let outputBuffer = "";
let hasOutput = false;
let sessionClosed = false;
let offlineNotified = false;
let lastSize = { cols: 0, rows: 0 };
let resizeTimer = 0;
const urlParams = new URLSearchParams(window.location.search);
const prefilledCommand = urlParams.get("cmd");
if (prefilledCommand && input) {
setTimeout(() => {
input.value = prefilledCommand;
input.focus();
}, 1000);
function tryFit() {
try {
fit.fit();
} catch {
/* ignore: container not yet measured */
}
}
function setStatus(label, tone = "pill-offline") {
@@ -36,32 +75,46 @@ function setStatus(label, tone = "pill-offline") {
statusPill.innerHTML = `<i class="fa-solid fa-circle"></i> ${label}`;
}
function appendSystem(text) {
outputBuffer += `\n[system] ${text}\n`;
renderOutput();
function writeSystem(msg) {
term.writeln(`\x1b[2m[${msg}]\x1b[0m`);
}
function appendOutput(text) {
outputBuffer += text;
renderOutput();
function sendResize() {
if (sessionClosed) return;
if (!ws || ws.readyState !== WebSocket.OPEN) return;
const cols = term.cols;
const rows = term.rows;
if (!cols || !rows) return;
if (cols === lastSize.cols && rows === lastSize.rows) return;
lastSize = { cols, rows };
ws.send(encodeMsgpack({ type: "resize", cols, rows }));
}
function renderOutput() {
if (!outputEl) return;
const normalized = outputBuffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
outputEl.innerHTML = `<pre class="console-pre">${ansiToHtml(normalized)}</pre>`;
outputEl.scrollTop = outputEl.scrollHeight;
function applyOutput(data) {
if (!data) return;
if (data instanceof Uint8Array) {
term.write(data);
} else if (typeof data === "string") {
term.write(data);
} else if (data?.buffer instanceof ArrayBuffer) {
term.write(new Uint8Array(data.buffer, data.byteOffset ?? 0, data.byteLength));
}
}
function connect() {
appendSystem("Connecting to console...");
setStatus("Connecting...", "pill-offline");
sessionClosed = false;
setStatus("Connecting...", "pill-ghost");
writeSystem("Connecting...");
ws = new WebSocket(wsUrl);
ws.binaryType = "arraybuffer";
ws.addEventListener("open", () => {
connected = true;
setStatus("Connected", "pill-online");
term.options.disableStdin = false;
tryFit();
sendResize();
});
ws.addEventListener("message", (event) => {
@@ -74,32 +127,36 @@ function connect() {
if (hostLabel) hostLabel.textContent = payload.host || "unknown";
if (userLabel) userLabel.textContent = payload.user || "unknown";
if (osLabel) osLabel.textContent = payload.os || "unknown";
setStatus("Connected", "pill-online");
setStatus("Live", "pill-online");
break;
case "status":
if (payload.status === "offline") {
setStatus("Offline", "pill-offline");
appendSystem(payload.reason || "Client offline");
} else if (payload.status === "connecting" && !hasOutput) {
setStatus("Connecting...", "pill-ghost");
if (!offlineNotified) {
offlineNotified = true;
writeSystem(payload.reason || "Client offline");
}
sessionClosed = true;
term.options.disableStdin = true;
} else if (payload.status === "closed") {
setStatus("Closed", "pill-offline");
appendSystem(payload.reason || "Console closed");
writeSystem(payload.reason || "Console closed");
sessionClosed = true;
term.options.disableStdin = true;
} else if (payload.status === "online") {
offlineNotified = false;
}
break;
case "output": {
if (payload.data) {
hasOutput = true;
setStatus("Live", "pill-online");
appendOutput(payload.data);
}
if (payload.error) appendSystem(payload.error);
case "output":
applyOutput(payload.data);
if (payload.error) writeSystem(payload.error);
if (typeof payload.exitCode === "number") {
appendSystem(`Process exited (${payload.exitCode})`);
term.writeln(`\r\n\x1b[33m[Process exited (${payload.exitCode})]\x1b[0m`);
setStatus("Closed", "pill-offline");
sessionClosed = true;
term.options.disableStdin = true;
}
break;
}
default:
break;
}
@@ -109,7 +166,8 @@ function connect() {
if (!connected) return;
connected = false;
setStatus("Disconnected", "pill-offline");
appendSystem("Connection closed");
if (!sessionClosed) writeSystem("Connection closed");
term.options.disableStdin = true;
});
ws.addEventListener("error", () => {
@@ -117,40 +175,41 @@ function connect() {
});
}
function sendInput(value) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
appendSystem("Socket not ready");
return;
}
ws.send(encodeMsgpack({ type: "input", data: value }));
}
term.onData((d) => {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
if (sessionClosed) return;
ws.send(encodeMsgpack({ type: "input", data: d }));
});
function wireInput() {
if (!form || !input) return;
term.onResize(() => {
sendResize();
});
form.addEventListener("submit", (e) => {
e.preventDefault();
const value = input.value;
if (!value.trim()) {
input.value = "";
return;
window.addEventListener("resize", () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(tryFit, 120);
});
reconnectBtn?.addEventListener("click", () => {
try { ws?.close(); } catch { /* ignore */ }
term.reset();
connect();
});
clearBtn?.addEventListener("click", () => {
term.clear();
});
requestAnimationFrame(() => {
tryFit();
checkFeatureAccess("console", clientId).then((ok) => ok && connect());
});
const prefilledCommand = new URLSearchParams(window.location.search).get("cmd");
if (prefilledCommand) {
setTimeout(() => {
if (ws?.readyState === WebSocket.OPEN && !sessionClosed) {
ws.send(encodeMsgpack({ type: "input", data: prefilledCommand + "\r" }));
}
const text = value.endsWith("\n") ? value : `${value}\n`;
sendInput(text);
input.value = "";
input.focus();
});
input.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
form.dispatchEvent(new Event("submit"));
}
});
ctrlcBtn?.addEventListener("click", () => {
sendInput("\u0003");
});
}, 1200);
}
wireInput();
checkFeatureAccess("console", clientId).then(ok => ok && connect());
+29 -40
View File
@@ -11,8 +11,9 @@
<link rel="stylesheet" href="/vendor/inter/700.css" />
<link rel="stylesheet" href="/vendor/jetbrains-mono/400.css" />
<link rel="stylesheet" href="/vendor/jetbrains-mono/600.css" />
<link rel="stylesheet" href="/assets/tailwind.css" />
<link rel="stylesheet" href="/assets/tailwind.css" />
<link rel="stylesheet" href="/vendor/fontawesome/css/all.min.css" />
<link rel="stylesheet" href="/vendor/xterm/xterm.css" />
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
<link rel="stylesheet" href="/assets/main.css" />
<link rel="stylesheet" href="/assets/custom.css" />
@@ -22,49 +23,37 @@
<header id="top-nav"></header>
<main class="console-main">
<div class="console-card">
<div class="console-meta">
<div class="console-target">
<div class="muted text-sm">Client</div>
<div class="console-target-id" id="client-label">-</div>
<div class="console-header">
<div class="console-meta">
<div class="console-target">
<div class="muted text-sm">Client</div>
<div class="console-target-id" id="client-label">-</div>
</div>
<div class="console-target">
<div class="muted text-sm">Host</div>
<div class="console-target-id" id="host-label">-</div>
</div>
<div class="console-target">
<div class="muted text-sm">User</div>
<div class="console-target-id" id="user-label">-</div>
</div>
<div class="console-target">
<div class="muted text-sm">OS</div>
<div class="console-target-id" id="os-label">-</div>
</div>
</div>
<div class="console-target">
<div class="muted text-sm">Host</div>
<div class="console-target-id" id="host-label">-</div>
</div>
<div class="console-target">
<div class="muted text-sm">User</div>
<div class="console-target-id" id="user-label">-</div>
</div>
<div class="console-target">
<div class="muted text-sm">OS</div>
<div class="console-target-id" id="os-label">-</div>
<div class="console-toolbar">
<span id="status-pill" class="pill pill-offline"><i class="fa-solid fa-circle"></i> Offline</span>
<button type="button" id="reconnect-btn" class="button ghost" title="Reconnect">
<i class="fa-solid fa-rotate-right"></i><span>Reconnect</span>
</button>
<button type="button" id="clear-btn" class="button ghost" title="Clear screen">
<i class="fa-solid fa-eraser"></i><span>Clear</span>
</button>
</div>
</div>
<div
id="console-output"
class="console-output"
aria-live="polite"
></div>
<form id="console-form" class="console-input-row">
<div class="prompt">$</div>
<textarea
id="console-input"
rows="1"
autocomplete="off"
spellcheck="false"
placeholder="Type a command and press Enter"
></textarea>
<div class="console-buttons">
<button type="submit" class="button primary">
<i class="fa-solid fa-paper-plane"></i><span>Send</span>
</button>
<button type="button" id="ctrlc-btn" class="button ghost">
<i class="fa-solid fa-ban"></i><span>Ctrl+C</span>
</button>
</div>
</form>
<div id="terminal" class="console-terminal"></div>
</div>
</main>
<script defer src="/vendor/msgpackr/msgpackr.js"></script>
+21
View File
@@ -207,6 +207,27 @@ if (hljsBuild.success) {
}
rmSync(hljsTmp, { force: true });
/* ── xterm.js ────────────────────────────────────────────────────── */
console.log("Copying xterm.js ...");
const xtermRoot = path.join(NM, "@xterm");
copyFile(
path.join(xtermRoot, "xterm", "lib", "xterm.mjs"),
path.join(VENDOR, "xterm", "xterm.mjs"),
);
copyFile(
path.join(xtermRoot, "xterm", "css", "xterm.css"),
path.join(VENDOR, "xterm", "xterm.css"),
);
copyFile(
path.join(xtermRoot, "addon-fit", "lib", "addon-fit.mjs"),
path.join(VENDOR, "xterm", "addon-fit.mjs"),
);
copyFile(
path.join(xtermRoot, "addon-web-links", "lib", "addon-web-links.mjs"),
path.join(VENDOR, "xterm", "addon-web-links.mjs"),
);
/* ── ansi-to-html (bundle for ESM import) ────────────────────────── */
console.log("Bundling ansi-to-html ...");
+2
View File
@@ -19,6 +19,8 @@ const MIME_TYPES: Record<string, string> = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".map": "application/json",
".json": "application/json",
".png": "image/png",
".jpg": "image/jpeg",
@@ -1181,8 +1181,6 @@ export function sendHVNCCommand(target: ClientInfo, commandType: string, payload
return broadcastFrameToViewers(sessionManager.getWebcamSessionsForClient(clientId), buf, header);
};
const textDecoder = new TextDecoder();
export function handleConsoleViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
const payload = decodeViewerPayload(raw);
if (!payload) return;
@@ -1193,7 +1191,6 @@ export function handleConsoleViewerMessage(ws: ServerWebSocket<SocketData>, raw:
const { clientId, sessionId } = ws.data;
const target = clientManager.getClient(clientId);
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
return;
}
@@ -1225,11 +1222,10 @@ export function handleConsoleOutput(clientId: string, payload: any) {
const session = sessionManager.getConsoleSession(sessionId);
if (!session) return;
if (session.clientId !== clientId) return;
const data = payload.data ? textDecoder.decode(payload.data as Uint8Array) : "";
safeSendViewer(session.viewer, {
type: "output",
sessionId,
data,
data: payload.data ?? null,
exitCode: payload.exitCode,
error: payload.error,
});