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 = `
`; 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(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));