// ========== CONTAINER EXEC / SHELL (WebSocket + xterm.js) ========== (function() { injectModal('exec-modal', `

Terminal

`); const modal = document.getElementById('exec-modal'); const termEl = document.getElementById('exec-terminal'); const closeBtn = document.getElementById('exec-close'); let term = null; let ws = null; let fitAddon = null; function cleanup() { if (ws) { try { ws.close(); } catch (_) {} ws = null; } if (term) { try { term.dispose(); } catch (_) {} term = null; } fitAddon = null; termEl.innerHTML = ''; } function openExec(containerId, containerName) { cleanup(); document.getElementById('exec-title').textContent = `Terminal — ${containerName || containerId}`; modal?.classList.add('show'); // Ensure xterm is available if (typeof Terminal === 'undefined') { termEl.innerHTML = '
xterm.js not loaded
'; return; } term = new Terminal({ cursorBlink: true, fontSize: 14, fontFamily: "'Cascadia Code', 'Fira Code', 'Consolas', monospace", theme: { background: '#1e1e1e', foreground: '#d4d4d4', cursor: '#aeafad', selectionBackground: '#264f78', }, scrollback: 5000, }); // Fit addon if (typeof FitAddon !== 'undefined') { fitAddon = new FitAddon.FitAddon(); term.loadAddon(fitAddon); } term.open(termEl); if (fitAddon) { // Small delay for DOM to settle setTimeout(() => fitAddon.fit(), 50); } // Connect WebSocket const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(`${protocol}//${location.host}/ws/exec/${encodeURIComponent(containerId)}`); ws.binaryType = 'arraybuffer'; ws.onopen = () => { term.writeln('\x1b[32mConnecting...\x1b[0m'); // Send initial resize if (fitAddon) { const dims = fitAddon.proposeDimensions(); if (dims) { ws.send(JSON.stringify({ type: 'resize', cols: dims.cols, rows: dims.rows })); } } }; ws.onmessage = (e) => { if (typeof e.data === 'string') { try { const msg = JSON.parse(e.data); if (msg.type === 'connected') { term.writeln(`\x1b[32mConnected (${msg.shell})\x1b[0m\r\n`); return; } if (msg.type === 'error') { term.writeln(`\x1b[31mError: ${msg.message}\x1b[0m`); return; } if (msg.type === 'exit') { term.writeln('\r\n\x1b[33mSession ended.\x1b[0m'); return; } } catch (_) {} // Plain text term.write(e.data); } else { // Binary data term.write(new Uint8Array(e.data)); } }; ws.onclose = () => { if (term) term.writeln('\r\n\x1b[33mDisconnected.\x1b[0m'); }; ws.onerror = () => { if (term) term.writeln('\r\n\x1b[31mConnection error.\x1b[0m'); }; // Terminal input → WebSocket term.onData((data) => { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(data); } }); // Handle resize term.onResize(({ cols, rows }) => { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'resize', cols, rows })); } }); // Re-fit on window resize const resizeHandler = () => { if (fitAddon) fitAddon.fit(); }; window.addEventListener('resize', resizeHandler); // Store handler for cleanup modal._resizeHandler = resizeHandler; } closeBtn?.addEventListener('click', () => { cleanup(); if (modal._resizeHandler) { window.removeEventListener('resize', modal._resizeHandler); } modal?.classList.remove('show'); }); // Also close on backdrop click modal?.addEventListener('click', (e) => { if (e.target === modal) { cleanup(); if (modal._resizeHandler) { window.removeEventListener('resize', modal._resizeHandler); } modal?.classList.remove('show'); } }); // Export window.openExecModal = openExec; })();