const state = { config: null, selected: 0, view: "servers", query: "", capabilities: null, }; const els = { navItems: document.querySelectorAll(".nav-item"), views: { servers: document.querySelector("#serversView"), commands: document.querySelector("#commandsView"), settings: document.querySelector("#settingsView"), }, viewTitle: document.querySelector("#viewTitle"), terminalStatus: document.querySelector("#terminalStatus"), searchInput: document.querySelector("#searchInput"), serverGrid: document.querySelector("#serverGrid"), serverForm: document.querySelector("#serverForm"), formTitle: document.querySelector("#formTitle"), deleteButton: document.querySelector("#deleteButton"), connectButton: document.querySelector("#connectButton"), addButton: document.querySelector("#addButton"), reloadButton: document.querySelector("#reloadButton"), sshButton: document.querySelector("#sshButton"), sshCommand: document.querySelector("#sshCommand"), commandsList: document.querySelector("#commandsList"), settingsForm: document.querySelector("#settingsForm"), toast: document.querySelector("#toast"), }; async function loadConfig() { const [configResponse, capabilitiesResponse] = await Promise.all([ fetch("/api/config"), fetch("/api/capabilities"), ]); const response = configResponse; if (!response.ok) throw new Error("Config konnte nicht geladen werden"); state.config = await response.json(); state.capabilities = capabilitiesResponse.ok ? await capabilitiesResponse.json() : null; state.selected = Math.min(state.selected, Math.max(0, state.config.servers.length - 1)); render(); } async function saveConfig(message = "Gespeichert") { const response = await fetch("/api/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(state.config), }); if (!response.ok) throw new Error("Config konnte nicht gespeichert werden"); state.config = await response.json(); showToast(message); render(); } function render() { renderNavigation(); renderServers(); renderServerForm(); renderCommands(); renderSettings(); } function renderNavigation() { els.navItems.forEach((item) => { item.classList.toggle("active", item.dataset.view === state.view); }); Object.entries(els.views).forEach(([name, node]) => { node.classList.toggle("active", name === state.view); }); const titles = { servers: "Server", commands: "Quick Commands", settings: "Settings", }; els.viewTitle.textContent = titles[state.view]; if (state.capabilities?.terminal_available) { els.terminalStatus.textContent = `Terminal: ${state.capabilities.terminal}`; } else { els.terminalStatus.textContent = "Kein unterstützter Terminal-Emulator gefunden"; } } function filteredServers() { const query = state.query.trim().toLowerCase(); const servers = state.config?.servers ?? []; if (!query) return servers.map((server, index) => ({ server, index })); return servers .map((server, index) => ({ server, index })) .filter(({ server }) => { return [server.name, server.host, server.user, server.group, server.auth] .join(" ") .toLowerCase() .includes(query); }); } function renderServers() { const items = filteredServers(); els.serverGrid.innerHTML = ""; if (items.length === 0) { els.serverGrid.innerHTML = `

Keine Server gefunden.

`; return; } for (const { server, index } of items) { const card = document.createElement("article"); card.className = "server-card"; card.classList.toggle("active", index === state.selected); card.innerHTML = `

${escapeHTML(server.name || "Unbenannt")}

${escapeHTML(server.user || "")}@${escapeHTML(server.host || "")}:${server.port || 22}

${escapeHTML(server.group || "Keine Gruppe")} ${escapeHTML(server.auth || "key")} ${server.kitty_fix ? `kitty` : ""}
`; card.addEventListener("click", () => { state.selected = index; els.sshCommand.hidden = true; render(); }); els.serverGrid.append(card); } } function renderServerForm() { const server = state.config?.servers?.[state.selected]; const disabled = !server; els.serverForm.querySelectorAll("input, select, button").forEach((input) => { input.disabled = disabled; }); els.connectButton.disabled = disabled || !state.capabilities?.terminal_available; if (!server) { els.formTitle.textContent = "Kein Server ausgewählt"; els.serverForm.reset(); return; } els.formTitle.textContent = server.name ? `${server.name} bearbeiten` : "Server bearbeiten"; setFormValue("name", server.name); setFormValue("host", server.host); setFormValue("user", server.user); setFormValue("port", server.port || 22); setFormValue("group", server.group); setFormValue("auth", server.auth || "key"); setFormValue("key", server.key); setFormValue("password_id", server.password_id); els.serverForm.elements.kitty_fix.checked = Boolean(server.kitty_fix); } function renderCommands() { const commands = state.config?.quick_commands ?? []; els.commandsList.innerHTML = ""; if (commands.length === 0) { els.commandsList.innerHTML = `

Keine Quick Commands konfiguriert.

`; return; } for (const command of commands) { const item = document.createElement("article"); item.className = "command-item"; item.innerHTML = `

${escapeHTML(command.name)}

${escapeHTML(command.command)} `; els.commandsList.append(item); } } function renderSettings() { const settings = state.config?.settings; if (!settings) return; els.settingsForm.elements.theme.value = settings.theme || "neon-green"; els.settingsForm.elements.term.value = settings.terminal?.term || "xterm-256color"; els.settingsForm.elements.enable_kitty_fix.checked = Boolean(settings.terminal?.enable_kitty_fix); } function addServer() { state.config.servers.push({ name: "Neuer Server", host: "", user: "root", port: 22, group: "Homelab", auth: "key", key: "", password_id: "", kitty_fix: true, }); state.selected = state.config.servers.length - 1; state.view = "servers"; render(); } function collectServerForm() { const form = els.serverForm.elements; return { name: form.name.value.trim(), host: form.host.value.trim(), user: form.user.value.trim(), port: Number(form.port.value) || 22, group: form.group.value.trim(), auth: form.auth.value, key: form.key.value.trim(), password_id: form.password_id.value.trim(), kitty_fix: form.kitty_fix.checked, }; } function setFormValue(name, value) { els.serverForm.elements[name].value = value ?? ""; } async function showSSHCommand() { const response = await fetch(`/api/ssh-command/${state.selected}`, { method: "POST" }); if (!response.ok) throw new Error("SSH Befehl konnte nicht erzeugt werden"); const data = await response.json(); els.sshCommand.textContent = data.command; els.sshCommand.hidden = false; } async function connectSSH() { const response = await fetch(`/api/connect/${state.selected}`, { method: "POST" }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "SSH Verbindung konnte nicht gestartet werden"); } els.sshCommand.textContent = data.command; els.sshCommand.hidden = false; showToast("SSH Terminal gestartet"); } function showToast(message) { els.toast.textContent = message; els.toast.hidden = false; window.setTimeout(() => { els.toast.hidden = true; }, 2200); } function escapeHTML(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } els.navItems.forEach((item) => { item.addEventListener("click", () => { state.view = item.dataset.view; render(); }); }); els.searchInput.addEventListener("input", (event) => { state.query = event.target.value; renderServers(); }); els.addButton.addEventListener("click", addServer); els.reloadButton.addEventListener("click", () => loadConfig().then(() => showToast("Neu geladen")).catch((error) => showToast(error.message))); els.sshButton.addEventListener("click", () => showSSHCommand().catch((error) => showToast(error.message))); els.connectButton.addEventListener("click", () => connectSSH().catch((error) => showToast(error.message))); els.deleteButton.addEventListener("click", async () => { if (!state.config.servers[state.selected]) return; state.config.servers.splice(state.selected, 1); state.selected = Math.max(0, state.selected - 1); await saveConfig("Server gelöscht"); }); els.serverForm.addEventListener("submit", async (event) => { event.preventDefault(); const server = collectServerForm(); if (!server.name || !server.host || !server.user) { showToast("Name, Host und User sind Pflichtfelder"); return; } state.config.servers[state.selected] = server; await saveConfig("Server gespeichert"); }); els.settingsForm.addEventListener("submit", async (event) => { event.preventDefault(); const form = els.settingsForm.elements; state.config.settings = { theme: form.theme.value.trim() || "neon-green", terminal: { term: form.term.value.trim() || "xterm-256color", enable_kitty_fix: form.enable_kitty_fix.checked, }, }; await saveConfig("Settings gespeichert"); }); loadConfig().catch((error) => showToast(error.message));