Files
HostKeeper/app/backend/static/js/terminal.js
T
swanadiva 71864336f6 fix: terminal WebSocket connection timing + better mock fallback
- Fix xterm.js container not found: use requestAnimationFrame retry loop
  instead of single $nextTick (Alpine x-for renders asynchronously)
- Better mock fallback: use host name+IP instead of raw UUID
- Add logging for WebSocket connection path (found/not-found/hostname)
2026-07-09 18:40:35 +07:00

186 lines
4.7 KiB
JavaScript

document.addEventListener('alpine:init', () => {
const el = document.getElementById('hosts-data');
const hostOptions = el ? JSON.parse(el.textContent) : [];
const selEl = document.getElementById('selected-host');
const selectedHostId = selEl ? JSON.parse(selEl.textContent) : '';
const termTheme = {
background: '#ffffff',
foreground: '#191c1e',
cursor: '#0050cb',
cursorAccent: '#ffffff',
selectionBackground: '#0050cb33',
black: '#191c1e',
red: '#b3261e',
green: '#006e2f',
yellow: '#a86e0a',
blue: '#0050cb',
magenta: '#7e23cc',
cyan: '#006493',
white: '#191c1e',
brightBlack: '#727686',
brightRed: '#dc362e',
brightGreen: '#008a42',
brightYellow: '#c58800',
brightBlue: '#2962d5',
brightMagenta: '#9a40e8',
brightCyan: '#007ab5',
brightWhite: '#424656',
};
Alpine.data('terminalManager', () => ({
tabs: [],
activeTab: 0,
terminals: {},
sockets: {},
command: '',
init() {
this.addTab();
},
_createTerminal(id, hostName) {
const term = new Terminal({
theme: termTheme,
fontFamily: "'JetBrains Mono', monospace",
fontSize: 13,
lineHeight: 1.5,
cursorBlink: true,
cursorStyle: 'bar',
allowProposedApi: true,
scrollback: 5000,
});
this.terminals[id] = term;
return term;
},
_connectWebSocket(id, hostID, hostName) {
const ws = new WebSocket(
(location.protocol === 'https:' ? 'wss:' : 'ws:') +
'//' + location.host + '/terminal/ws/' + hostID
);
this.sockets[id] = ws;
const term = this.terminals[id];
ws.onopen = () => {
term.focus();
};
ws.onmessage = (ev) => {
term.write(ev.data);
};
ws.onclose = () => {
term.write('\r\n\x1b[33m[Connection closed]\x1b[0m\r\n');
};
ws.onerror = () => {
term.write('\r\n\x1b[31m[Connection error]\x1b[0m\r\n');
};
term.onData((data) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(data);
}
});
},
_getHostForTab() {
let host;
if (selectedHostId) {
host = hostOptions.find((h) => h.id === selectedHostId) || hostOptions[0];
}
if (!host) {
host = hostOptions.length > 0
? hostOptions[Math.floor(Math.random() * hostOptions.length)]
: { id: 'h1', name: 'localhost', ip: '127.0.0.1' };
}
return host;
},
addTab() {
const host = this._getHostForTab();
const id = 'tab-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
this.tabs.push({ id, name: host.name, host: host.ip, hostID: host.id });
this.activeTab = this.tabs.length - 1;
this._createTerminal(id, host.name);
const tryOpen = (retries) => {
const container = document.getElementById('xterm-' + id);
if (container) {
this.terminals[id].open(container);
this._connectWebSocket(id, host.id, host.name);
} else if (retries > 0) {
requestAnimationFrame(() => tryOpen(retries - 1));
}
};
this.$nextTick(() => tryOpen(10));
},
switchTab(i) {
this.activeTab = i;
this.$nextTick(() => {
const cur = this.tabs[this.activeTab];
if (cur && this.terminals[cur.id]) {
this.terminals[cur.id].focus();
}
});
},
closeTab(i) {
if (this.tabs.length <= 1) return;
const tab = this.tabs[i];
const id = tab.id;
if (this.sockets[id]) {
this.sockets[id].close();
delete this.sockets[id];
}
if (this.terminals[id]) {
this.terminals[id].dispose();
delete this.terminals[id];
}
this.tabs.splice(i, 1);
if (this.activeTab >= this.tabs.length) {
this.activeTab = this.tabs.length - 1;
}
const cur = this.tabs[this.activeTab];
if (cur && this.terminals[cur.id]) {
this.$nextTick(() => this.terminals[cur.id].focus());
}
},
submitCommand() {
const cmd = this.command.trim();
if (!cmd) return;
const cur = this.tabs[this.activeTab];
if (!cur) return;
const ws = this.sockets[cur.id];
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(cmd + '\r');
}
this.command = '';
},
insertCommand(cmd) {
this.command = cmd;
this.$nextTick(() => this.submitCommand());
},
clearBuffer() {
const cur = this.tabs[this.activeTab];
if (!cur) return;
const term = this.terminals[cur.id];
if (term) {
term.clear();
term.write('\x1b[2J\x1b[H');
}
},
}));
});