fix rev console to make it not buns.

This commit is contained in:
KingKDot
2026-05-19 20:31:36 -05:00
parent bd192f7076
commit 40c72acb2e
5 changed files with 121 additions and 66 deletions
+8 -4
View File
@@ -14,12 +14,15 @@
flex: 1; flex: 1;
padding: 18px; padding: 18px;
box-sizing: border-box; box-sizing: border-box;
max-width: none !important;
width: 100%;
margin: 0;
} }
.console-card { .console-card {
max-width: 1280px;
width: 100%; width: 100%;
margin: 0 auto; max-width: none !important;
margin: 0;
box-sizing: border-box; box-sizing: border-box;
background: rgba(8, 12, 22, 0.8); background: rgba(8, 12, 22, 0.8);
border: 1px solid rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.05);
@@ -40,8 +43,9 @@
} }
.console-meta { .console-meta {
display: grid; display: flex;
grid-template-columns: repeat(auto-fit, minmax(120px, max-content)); flex-wrap: wrap;
align-items: center;
gap: 14px 24px; gap: 14px 24px;
color: #a9b8d4; color: #a9b8d4;
} }
+100 -58
View File
@@ -12,6 +12,7 @@ const userLabel = document.getElementById("user-label");
const osLabel = document.getElementById("os-label"); const osLabel = document.getElementById("os-label");
const reconnectBtn = document.getElementById("reconnect-btn"); const reconnectBtn = document.getElementById("reconnect-btn");
const clearBtn = document.getElementById("clear-btn"); const clearBtn = document.getElementById("clear-btn");
const interruptBtn = document.getElementById("interrupt-btn");
const clientId = decodeURIComponent( const clientId = decodeURIComponent(
location.pathname.split("/").filter(Boolean)[0] || "", location.pathname.split("/").filter(Boolean)[0] || "",
@@ -19,49 +20,82 @@ const clientId = decodeURIComponent(
const wsProto = location.protocol === "https:" ? "wss" : "ws"; const wsProto = location.protocol === "https:" ? "wss" : "ws";
const wsUrl = `${wsProto}://${location.host}/api/clients/${encodeURIComponent(clientId)}/console/ws`; const wsUrl = `${wsProto}://${location.host}/api/clients/${encodeURIComponent(clientId)}/console/ws`;
const term = new Terminal({ const THEME = {
cursorBlink: true, background: "#050913",
fontFamily: '"JetBrains Mono", ui-monospace, Menlo, Consolas, monospace', foreground: "#e8edf2",
fontSize: 13, cursor: "#6ee7b7",
scrollback: 10000, cursorAccent: "#050913",
allowProposedApi: true, selectionBackground: "rgba(110, 231, 183, 0.25)",
theme: { black: "#1a1d29",
background: "#050913", red: "#ef4444",
foreground: "#e8edf2", green: "#22c55e",
cursor: "#6ee7b7", yellow: "#eab308",
cursorAccent: "#050913", blue: "#60a5fa",
selectionBackground: "rgba(110, 231, 183, 0.25)", magenta: "#c084fc",
black: "#1a1d29", cyan: "#22d3ee",
red: "#ef4444", white: "#e8edf2",
green: "#22c55e", brightBlack: "#475569",
yellow: "#eab308", brightRed: "#f87171",
blue: "#60a5fa", brightGreen: "#4ade80",
magenta: "#c084fc", brightYellow: "#facc15",
cyan: "#22d3ee", brightBlue: "#93c5fd",
white: "#e8edf2", brightMagenta: "#d8b4fe",
brightBlack: "#475569", brightCyan: "#67e8f9",
brightRed: "#f87171", brightWhite: "#f8fafc",
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 term = null;
let fit = null;
let ws = null; let ws = null;
let connected = false; let connected = false;
let sessionClosed = false; let sessionClosed = false;
let offlineNotified = false; let offlineNotified = false;
let lastSize = { cols: 0, rows: 0 }; let lastSize = { cols: 0, rows: 0 };
let resizeTimer = 0; let resizeTimer = 0;
let pendingSystem = [];
function isWindowsOs(os) {
return typeof os === "string" && /win/i.test(os);
}
function initTerminal(os) {
if (term) return;
const opts = {
cursorBlink: true,
fontFamily: '"JetBrains Mono", ui-monospace, Menlo, Consolas, monospace',
fontSize: 13,
scrollback: 10000,
allowProposedApi: true,
theme: THEME,
};
if (isWindowsOs(os)) {
opts.windowsPty = { backend: "conpty", buildNumber: 22621 };
}
term = new Terminal(opts);
fit = new FitAddon();
term.loadAddon(fit);
term.loadAddon(new WebLinksAddon());
term.open(containerEl);
term.onData((d) => {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
if (sessionClosed) return;
ws.send(encodeMsgpack({ type: "input", data: d }));
});
term.onResize(() => {
sendResize();
});
for (const msg of pendingSystem) term.writeln(msg);
pendingSystem.length = 0;
tryFit();
}
function tryFit() { function tryFit() {
if (!fit) return;
try { try {
fit.fit(); fit.fit();
} catch { } catch {
@@ -76,12 +110,18 @@ function setStatus(label, tone = "pill-offline") {
} }
function writeSystem(msg) { function writeSystem(msg) {
term.writeln(`\x1b[2m[${msg}]\x1b[0m`); const line = `\x1b[2m[${msg}]\x1b[0m`;
if (term) {
term.writeln(line);
} else {
pendingSystem.push(line);
}
} }
function sendResize() { function sendResize() {
if (sessionClosed) return; if (sessionClosed) return;
if (!ws || ws.readyState !== WebSocket.OPEN) return; if (!ws || ws.readyState !== WebSocket.OPEN) return;
if (!term) return;
const cols = term.cols; const cols = term.cols;
const rows = term.rows; const rows = term.rows;
if (!cols || !rows) return; if (!cols || !rows) return;
@@ -91,7 +131,7 @@ function sendResize() {
} }
function applyOutput(data) { function applyOutput(data) {
if (!data) return; if (!data || !term) return;
if (data instanceof Uint8Array) { if (data instanceof Uint8Array) {
term.write(data); term.write(data);
} else if (typeof data === "string") { } else if (typeof data === "string") {
@@ -112,9 +152,6 @@ function connect() {
ws.addEventListener("open", () => { ws.addEventListener("open", () => {
connected = true; connected = true;
setStatus("Connected", "pill-online"); setStatus("Connected", "pill-online");
term.options.disableStdin = false;
tryFit();
sendResize();
}); });
ws.addEventListener("message", (event) => { ws.addEventListener("message", (event) => {
@@ -123,10 +160,18 @@ function connect() {
switch (payload.type) { switch (payload.type) {
case "ready": case "ready":
if (clientLabel) clientLabel.textContent = payload.clientId || "unknown"; if (clientLabel) {
const full = payload.clientId || "unknown";
clientLabel.textContent = full.length > 7 ? full.slice(0, 7) + "…" : full;
clientLabel.title = full;
}
if (hostLabel) hostLabel.textContent = payload.host || "unknown"; if (hostLabel) hostLabel.textContent = payload.host || "unknown";
if (userLabel) userLabel.textContent = payload.user || "unknown"; if (userLabel) userLabel.textContent = payload.user || "unknown";
if (osLabel) osLabel.textContent = payload.os || "unknown"; if (osLabel) osLabel.textContent = payload.os || "unknown";
initTerminal(payload.os);
if (term) term.options.disableStdin = false;
tryFit();
sendResize();
setStatus("Live", "pill-online"); setStatus("Live", "pill-online");
break; break;
case "status": case "status":
@@ -137,12 +182,12 @@ function connect() {
writeSystem(payload.reason || "Client offline"); writeSystem(payload.reason || "Client offline");
} }
sessionClosed = true; sessionClosed = true;
term.options.disableStdin = true; if (term) term.options.disableStdin = true;
} else if (payload.status === "closed") { } else if (payload.status === "closed") {
setStatus("Closed", "pill-offline"); setStatus("Closed", "pill-offline");
writeSystem(payload.reason || "Console closed"); writeSystem(payload.reason || "Console closed");
sessionClosed = true; sessionClosed = true;
term.options.disableStdin = true; if (term) term.options.disableStdin = true;
} else if (payload.status === "online") { } else if (payload.status === "online") {
offlineNotified = false; offlineNotified = false;
} }
@@ -151,10 +196,10 @@ function connect() {
applyOutput(payload.data); applyOutput(payload.data);
if (payload.error) writeSystem(payload.error); if (payload.error) writeSystem(payload.error);
if (typeof payload.exitCode === "number") { if (typeof payload.exitCode === "number") {
term.writeln(`\r\n\x1b[33m[Process exited (${payload.exitCode})]\x1b[0m`); if (term) term.writeln(`\r\n\x1b[33m[Process exited (${payload.exitCode})]\x1b[0m`);
setStatus("Closed", "pill-offline"); setStatus("Closed", "pill-offline");
sessionClosed = true; sessionClosed = true;
term.options.disableStdin = true; if (term) term.options.disableStdin = true;
} }
break; break;
default: default:
@@ -167,7 +212,7 @@ function connect() {
connected = false; connected = false;
setStatus("Disconnected", "pill-offline"); setStatus("Disconnected", "pill-offline");
if (!sessionClosed) writeSystem("Connection closed"); if (!sessionClosed) writeSystem("Connection closed");
term.options.disableStdin = true; if (term) term.options.disableStdin = true;
}); });
ws.addEventListener("error", () => { ws.addEventListener("error", () => {
@@ -175,16 +220,6 @@ function connect() {
}); });
} }
term.onData((d) => {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
if (sessionClosed) return;
ws.send(encodeMsgpack({ type: "input", data: d }));
});
term.onResize(() => {
sendResize();
});
window.addEventListener("resize", () => { window.addEventListener("resize", () => {
clearTimeout(resizeTimer); clearTimeout(resizeTimer);
resizeTimer = setTimeout(tryFit, 120); resizeTimer = setTimeout(tryFit, 120);
@@ -192,16 +227,23 @@ window.addEventListener("resize", () => {
reconnectBtn?.addEventListener("click", () => { reconnectBtn?.addEventListener("click", () => {
try { ws?.close(); } catch { /* ignore */ } try { ws?.close(); } catch { /* ignore */ }
term.reset(); if (term) term.reset();
lastSize = { cols: 0, rows: 0 };
connect(); connect();
}); });
clearBtn?.addEventListener("click", () => { clearBtn?.addEventListener("click", () => {
term.clear(); if (term) term.clear();
});
interruptBtn?.addEventListener("click", () => {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
if (sessionClosed) return;
ws.send(encodeMsgpack({ type: "input", data: "\x03" }));
if (term) term.focus();
}); });
requestAnimationFrame(() => { requestAnimationFrame(() => {
tryFit();
checkFeatureAccess("console", clientId).then((ok) => ok && connect()); checkFeatureAccess("console", clientId).then((ok) => ok && connect());
}); });
+4 -1
View File
@@ -17,7 +17,7 @@
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
<link rel="stylesheet" href="/assets/main.css" /> <link rel="stylesheet" href="/assets/main.css" />
<link rel="stylesheet" href="/assets/custom.css" /> <link rel="stylesheet" href="/assets/custom.css" />
<link rel="stylesheet" href="/assets/console.css" /> <link rel="stylesheet" href="/assets/console.css?v=3" />
</head> </head>
<body class="console-body"> <body class="console-body">
<header id="top-nav"></header> <header id="top-nav"></header>
@@ -47,6 +47,9 @@
<button type="button" id="reconnect-btn" class="button ghost" title="Reconnect"> <button type="button" id="reconnect-btn" class="button ghost" title="Reconnect">
<i class="fa-solid fa-rotate-right"></i><span>Reconnect</span> <i class="fa-solid fa-rotate-right"></i><span>Reconnect</span>
</button> </button>
<button type="button" id="interrupt-btn" class="button ghost" title="Send Ctrl+C">
<i class="fa-solid fa-hand"></i><span>Ctrl+C</span>
</button>
<button type="button" id="clear-btn" class="button ghost" title="Clear screen"> <button type="button" id="clear-btn" class="button ghost" title="Clear screen">
<i class="fa-solid fa-eraser"></i><span>Clear</span> <i class="fa-solid fa-eraser"></i><span>Clear</span>
</button> </button>
@@ -345,7 +345,7 @@ export function handleConsoleViewerOpen(ws: ServerWebSocket<SocketData>) {
const effectiveSessionId = sessionId || uuidv4(); const effectiveSessionId = sessionId || uuidv4();
ws.data.sessionId = effectiveSessionId; ws.data.sessionId = effectiveSessionId;
const target = clientManager.getClient(clientId); const target = clientManager.getClient(clientId);
const session: ConsoleSession = { id: effectiveSessionId, clientId, viewer: ws, createdAt: Date.now() }; const session: ConsoleSession = { id: effectiveSessionId, clientId, viewer: ws, createdAt: Date.now(), started: false };
sessionManager.addConsoleSession(session); sessionManager.addConsoleSession(session);
safeSendViewer(ws, { safeSendViewer(ws, {
type: "ready", type: "ready",
@@ -360,7 +360,6 @@ export function handleConsoleViewerOpen(ws: ServerWebSocket<SocketData>) {
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId: effectiveSessionId }); safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId: effectiveSessionId });
return; return;
} }
startConsoleForViewer(target, effectiveSessionId);
} }
export function handleRemoteDesktopViewerOpen(ws: ServerWebSocket<SocketData>) { export function handleRemoteDesktopViewerOpen(ws: ServerWebSocket<SocketData>) {
@@ -1219,7 +1218,13 @@ export function handleConsoleViewerMessage(ws: ServerWebSocket<SocketData>, raw:
case "resize": { case "resize": {
const cols = Number(payload.cols) || 120; const cols = Number(payload.cols) || 120;
const rows = Number(payload.rows) || 36; const rows = Number(payload.rows) || 36;
sendConsoleCommand(target, "console_resize", { sessionId, cols, rows }); const session = sessionId ? sessionManager.getConsoleSession(sessionId) : undefined;
if (session && !session.started) {
session.started = true;
startConsoleForViewer(target, sessionId!, cols, rows);
} else {
sendConsoleCommand(target, "console_resize", { sessionId, cols, rows });
}
break; break;
} }
case "stop": { case "stop": {
+1
View File
@@ -35,6 +35,7 @@ export type ConsoleSession = {
clientId: string; clientId: string;
viewer: ServerWebSocket<SocketData>; viewer: ServerWebSocket<SocketData>;
createdAt: number; createdAt: number;
started?: boolean;
}; };
export type RemoteDesktopViewer = { export type RemoteDesktopViewer = {