fix rev console to make it not buns.
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user