(() => { const $ = (sel) => document.querySelector(sel); let term = null; let fitAddon = null; let socket = null; let connected = false; let connectInFlight = false; let currentHost = ""; function protoWs() { return location.protocol === "https:" ? "wss" : "ws"; } function rememberUser(user) { if (user) localStorage.setItem("cockpit_ssh_user", user); } function rememberedUser() { return localStorage.getItem("cockpit_ssh_user") || ""; } function openSshModal(opts = {}) { const modal = $("#ssh-modal"); const scrim = $("#scrim"); if (!modal) return; currentHost = opts.host || ""; $("#ssh-host").value = currentHost; $("#ssh-port").value = opts.port || 22; $("#ssh-user").value = opts.username || rememberedUser(); $("#ssh-pass").value = ""; $("#ssh-device").textContent = opts.name || currentHost || "SSH session"; $("#ssh-sub").textContent = opts.model ? `${opts.model}${opts.ip ? " · " + opts.ip : ""}` : opts.ip || currentHost || "—"; $("#ssh-status").textContent = "Enter username and password, then Connect."; $("#ssh-status").className = "ssh-status"; $("#ssh-connect-btn").disabled = false; $("#ssh-term-wrap").classList.add("hidden"); $("#ssh-login").classList.remove("hidden"); modal.classList.remove("hidden"); modal.setAttribute("aria-hidden", "false"); scrim?.classList.add("open"); if (scrim) scrim.dataset.mode = "ssh"; setTimeout(() => { const u = $("#ssh-user"); if (u && !u.value) u.focus(); else $("#ssh-pass")?.focus(); }, 50); } function closeSshModal() { disconnectSsh(); const modal = $("#ssh-modal"); modal?.classList.add("hidden"); modal?.setAttribute("aria-hidden", "true"); const scrim = $("#scrim"); if (scrim?.dataset.mode === "ssh") { scrim.classList.remove("open"); delete scrim.dataset.mode; } } function setStatus(msg, kind) { const el = $("#ssh-status"); if (!el) return; el.textContent = msg; el.className = "ssh-status" + (kind ? " " + kind : ""); } function ensureTerminal() { if (term) return term; if (!window.Terminal) { setStatus("Terminal library missing — hard refresh (Ctrl+Shift+R) or check /vendor/xterm/", "error"); $("#ssh-login")?.classList.remove("hidden"); return null; } term = new window.Terminal({ cursorBlink: true, fontFamily: "IBM Plex Mono, ui-monospace, Menlo, monospace", fontSize: 13, lineHeight: 1.2, theme: { background: "#071018", foreground: "#e8f4ff", cursor: "#3dffe0", selectionBackground: "rgba(0,168,232,0.35)", }, allowProposedApi: true, }); const FitCtor = window.FitAddon?.FitAddon || window.FitAddon; fitAddon = FitCtor ? new FitCtor() : null; if (fitAddon) term.loadAddon(fitAddon); term.open($("#ssh-term")); term.onData((data) => { if (!socket || socket.readyState !== WebSocket.OPEN || !connected) return; socket.send(data); }); window.addEventListener("resize", () => { try { fitAddon?.fit(); sendResize(); } catch (_) {} }); return term; } function sendResize() { if (!socket || socket.readyState !== WebSocket.OPEN || !term) return; socket.send( JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows, }) ); } function disconnectSsh() { connected = false; try { socket?.close(); } catch (_) {} socket = null; try { term?.reset(); } catch (_) {} } async function connectSsh() { if (connectInFlight) return; const host = ($("#ssh-host")?.value || "").trim(); const username = ($("#ssh-user")?.value || "").trim(); const password = $("#ssh-pass")?.value || ""; const port = Number($("#ssh-port")?.value || 22); if (!host || !username) { setStatus("Host and username are required", "error"); return; } if (!password) { setStatus("Password is required", "error"); $("#ssh-pass")?.focus(); return; } rememberUser(username); connectInFlight = true; disconnectSsh(); $("#ssh-login").classList.add("hidden"); $("#ssh-term-wrap").classList.remove("hidden"); const t = ensureTerminal(); if (!t) return; t.reset(); t.focus(); try { fitAddon?.fit(); } catch (_) {} setStatus(`Connecting as ${username}@${host}:${port}…`, "info"); $("#ssh-connect-btn").disabled = true; const ws = new WebSocket(`${protoWs()}://${location.host}/ws/ssh`); socket = ws; ws.onopen = () => { ws.send( JSON.stringify({ type: "auth", host, port, username, password, cols: t.cols || 120, rows: t.rows || 36, term: "xterm-256color", }) ); // clear password from DOM after send if ($("#ssh-pass")) $("#ssh-pass").value = ""; }; ws.onmessage = (ev) => { const data = String(ev.data || ""); if (data.startsWith("{") && data.includes('"type"')) { try { const msg = JSON.parse(data); if (msg.type === "ready") { connected = true; connectInFlight = false; setStatus(msg.message || "Connected", "ok"); $("#ssh-connect-btn").disabled = false; $("#ssh-connect-btn").textContent = "Reconnect"; try { fitAddon?.fit(); sendResize(); t.focus(); } catch (_) {} return; } if (msg.type === "status") { setStatus(msg.message || "", "info"); return; } if (msg.type === "error") { connected = false; connectInFlight = false; setStatus(msg.message || "SSH error", "error"); $("#ssh-connect-btn").disabled = false; $("#ssh-login").classList.remove("hidden"); return; } if (msg.type === "pong") return; } catch (_) { // fall through as terminal output } } t.write(data); }; ws.onerror = () => { setStatus("WebSocket error", "error"); $("#ssh-connect-btn").disabled = false; $("#ssh-login").classList.remove("hidden"); }; ws.onclose = () => { connected = false; connectInFlight = false; setStatus("Disconnected", "info"); $("#ssh-connect-btn").disabled = false; $("#ssh-connect-btn").textContent = "Connect"; }; } function openSshForNode(node) { if (!node?.ip) { alert("No management IP for this device"); return; } openSshModal({ host: node.ip, ip: node.ip, name: node.name, model: node.model, username: rememberedUser(), }); } function bind() { $("#btn-ssh-close")?.addEventListener("click", closeSshModal); $("#btn-ssh-disconnect")?.addEventListener("click", () => { disconnectSsh(); $("#ssh-login").classList.remove("hidden"); setStatus("Disconnected. Enter credentials to reconnect.", "info"); }); $("#ssh-form")?.addEventListener("submit", (e) => { e.preventDefault(); connectSsh(); }); $("#ssh-connect-btn")?.addEventListener("click", (e) => { e.preventDefault(); connectSsh(); }); // Scrim close for SSH $("#scrim")?.addEventListener("click", () => { if ($("#scrim")?.dataset.mode === "ssh") closeSshModal(); }); window.addEventListener("keydown", (e) => { if (e.key === "Escape" && !$("#ssh-modal")?.classList.contains("hidden")) { closeSshModal(); } }); } bind(); window.cockpitSsh = { open: openSshForNode, openModal: openSshModal, close: closeSshModal, }; })();