// ClawdDotNet — Instanzverwaltung // // Overview, create, start, stop, bring desktop window to front, active instance selection. import { api, holeAktiveInstanz, setzeAktiveInstanz } from "../modules/api.js"; import { t } from "../modules/i18n.js"; import { erstelleElement, erstelleIcon, PFADE, erstellePlakette, erstelleKarte, erstelleModal, instanzZustandInfo, zeigeMeldung } from "../modules/dom.js"; let aktualisierungsIntervall = null; export async function rendereInstanzen() { const container = document.getElementById("ansicht-inhalt"); if (!container) return; container.replaceChildren(); stoppePolling(); const layout = erstelleElement("div", { klasse: "instanzen-ansicht" }); const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" }); const titelGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" }); titelGruppe.append(erstelleElement("h1", { text: t("instances.title") })); const aktionenGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" }); const aktualisierenBtn = erstelleElement( "button", { klasse: "schaltflaeche flach", title: t("instances.refresh"), onclick: () => ladeInstanzen() }, erstelleIcon(PFADE.aktualisieren, 16), erstelleElement("span", { text: " " + t("instances.refresh") }) ); const neuBtn = erstelleElement( "button", { klasse: "schaltflaeche primaer", title: t("instances.newInstance"), onclick: oeffneNeueInstanzDialog }, erstelleIcon(PFADE.plus, 16), erstelleElement("span", { text: " " + t("instances.newInstance") }) ); aktionenGruppe.append(aktualisierenBtn, neuBtn); kopfZeile.append(titelGruppe, aktionenGruppe); const raster = erstelleElement("div", { klasse: "instanzen-raster", id: "instanzen-raster" }); layout.append(kopfZeile, raster); container.append(layout); await ladeInstanzen(); aktualisierungsIntervall = setInterval(() => { ladeInstanzen(true); }, 4000); } export function stoppePolling() { if (aktualisierungsIntervall) { clearInterval(aktualisierungsIntervall); aktualisierungsIntervall = null; } } async function ladeInstanzen(istHintergrund = false) { const raster = document.getElementById("instanzen-raster"); if (!raster) { stoppePolling(); return; } if (!istHintergrund && raster.children.length === 0) { raster.replaceChildren( erstelleElement("div", { klasse: "hinweis-zentriert", text: t("common.loading") }) ); } try { const instanzen = await api("/verwaltung/instanzen"); if (!raster) return; raster.replaceChildren(); if (!Array.isArray(instanzen) || instanzen.length === 0) { raster.append( erstelleKarte(t("common.none"), [ erstelleElement("p", { klasse: "gedaempft", text: "..." }) ]) ); return; } const aktiveId = holeAktiveInstanz(); for (const instanz of instanzen) { const istAktiv = instanz.id === aktiveId; const zustandInfo = instanzZustandInfo(instanz.zustand); const istLaufend = instanz.zustand === "Running"; const istStumm = instanz.zustand === "Silent"; const karteKopfAktionen = erstelleElement("div", { klasse: "gruppe-horizontal" }); if (istAktiv) { karteKopfAktionen.append(erstellePlakette(t("instances.activeBadge"), "erfolg")); } karteKopfAktionen.append(erstellePlakette(zustandInfo.text, zustandInfo.art)); const dl = erstelleElement("dl", { klasse: "datenliste" }); dl.append( erstelleElement("dt", { text: "ID" }), erstelleElement("dd", {}, erstelleElement("code", { text: instanz.id })), erstelleElement("dt", { text: t("common.status") }), erstelleElement("dd", { text: zustandInfo.text }), erstelleElement("dt", { text: "PID" }), erstelleElement("dd", { text: instanz.prozess ? String(instanz.prozess) : "—" }), erstelleElement("dt", { text: t("instances.autostart") }), erstelleElement("dd", { text: instanz.autostart ? (t("common.yes")) : (t("common.no")), klasse: instanz.autostart ? "" : "gedaempft" }) ); const fussAktionen = erstelleElement("div", { klasse: "instanz-aktionen" }); const waehlenBtn = erstelleElement( "button", { klasse: istAktiv ? "schaltflaeche klein" : "schaltflaeche primaer klein", disabled: istAktiv, title: istAktiv ? t("instances.activeBadge") : t("instances.selectActive"), onclick: () => { setzeAktiveInstanz(instanz.id); zeigeMeldung(`'${instanz.name}' selected.`, "info"); ladeInstanzen(); } }, erstelleElement("span", { text: istAktiv ? t("instances.activeBadge") : t("instances.selectActive") }) ); const startenBtn = erstelleElement( "button", { klasse: "schaltflaeche flach klein", disabled: istLaufend, title: t("instances.start"), onclick: () => starteInstanz(instanz) }, erstelleIcon(PFADE.abspielen, 14), erstelleElement("span", { text: " " + t("instances.start") }) ); const anhaltenBtn = erstelleElement( "button", { klasse: "schaltflaeche gefahr flach klein", disabled: !istLaufend && !istStumm, title: t("instances.stop"), onclick: () => stoppeInstanz(instanz) }, erstelleIcon(PFADE.stop, 14), erstelleElement("span", { text: " " + t("instances.stop") }) ); const fensterBtn = erstelleElement( "button", { klasse: "schaltflaeche flach klein", disabled: !istLaufend, title: t("instances.bringToFront"), onclick: () => holeFensterNachVorn(instanz) }, erstelleIcon(PFADE.fenster, 14), erstelleElement("span", { text: " " + t("instances.bringToFront") }) ); fussAktionen.append(waehlenBtn, startenBtn, anhaltenBtn, fensterBtn); const karte = erstelleKarte(instanz.name, [dl, fussAktionen], { aktionen: karteKopfAktionen, klasse: istAktiv ? "instanz-karte aktiv" : "instanz-karte" }); raster.append(karte); } } catch (err) { if (!istHintergrund) { raster.replaceChildren( erstelleElement("div", { klasse: "fehler-zentriert", text: `Error: ${err.message}` }) ); } } } async function starteInstanz(instanz) { try { const antwort = await api(`/verwaltung/instanzen/${encodeURIComponent(instanz.id)}/start`, { method: "POST" }); zeigeMeldung(antwort?.meldung || `Starting '${instanz.name}'…`, "erfolg"); if (!holeAktiveInstanz()) { setzeAktiveInstanz(instanz.id); } await ladeInstanzen(); } catch (err) { if (err.lizenzFehlt) { zeigeMeldung("License required (HTTP 402).", "gefahr", 6000); } else { zeigeMeldung(`Failed to start: ${err.message}`, "gefahr"); } } } function stoppeInstanz(instanz) { oeffneSicherheitsStoppDialog(instanz); } function oeffneSicherheitsStoppDialog(instanz) { let verbleibend = 10; let timerId = null; const inhalt = erstelleElement("div", { klasse: "formular-raster" }); const sicherheitsBox = erstelleElement("div", { klasse: "sicherheits-box" }); const boxTitel = erstelleElement( "h3", {}, erstelleIcon(PFADE.ausrufezeichen, 18), erstelleElement("span", { text: " Stop Instance" }) ); const boxText1 = erstelleElement("p", { text: `Stopping instance '${instanz.name}' will gracefully shut down all active agents and processes.` }); sicherheitsBox.append(boxTitel, boxText1); const dl = erstelleElement("dl", { klasse: "datenliste" }); dl.append( erstelleElement("dt", { text: "Instance" }), erstelleElement("dd", {}, erstelleElement("strong", { text: instanz.name })), erstelleElement("dt", { text: "ID" }), erstelleElement("dd", {}, erstelleElement("code", { text: instanz.id })), erstelleElement("dt", { text: "PID" }), erstelleElement("dd", { text: instanz.prozess ? String(instanz.prozess) : "—" }) ); const countdownBox = erstelleElement("div", { klasse: "sicherheits-countdown", text: `Confirmation locked: ${verbleibend}s…` }); inhalt.append(sicherheitsBox, dl, countdownBox); const abbrechenBtn = erstelleElement("button", { type: "button", klasse: "schaltflaeche flach", onclick: () => modal.schliessen(), text: t("common.cancel") }); const anhaltenBtn = erstelleElement( "button", { type: "button", klasse: "schaltflaeche gefahr", disabled: true }, erstelleIcon(PFADE.stop, 14), erstelleElement("span", { text: ` ${t("instances.stop")} (${verbleibend}s)` }) ); const modal = erstelleModal( `${t("instances.stop")} — ${instanz.name}`, inhalt, [abbrechenBtn, anhaltenBtn], { onSchliessen: () => { if (timerId) { clearInterval(timerId); timerId = null; } } } ); timerId = setInterval(() => { verbleibend--; if (verbleibend > 0) { countdownBox.textContent = `Confirmation locked: ${verbleibend}s…`; const textSpan = anhaltenBtn.querySelector("span"); if (textSpan) textSpan.textContent = ` ${t("instances.stop")} (${verbleibend}s)`; } else { clearInterval(timerId); timerId = null; anhaltenBtn.disabled = false; const textSpan = anhaltenBtn.querySelector("span"); if (textSpan) textSpan.textContent = ` ${t("instances.stop")}`; countdownBox.className = "sicherheits-countdown abgelaufen"; countdownBox.textContent = "Unlocked — confirmation ready."; } }, 1000); anhaltenBtn.addEventListener("click", async () => { if (verbleibend > 0) return; anhaltenBtn.disabled = true; const textSpan = anhaltenBtn.querySelector("span"); if (textSpan) textSpan.textContent = " Stopping…"; try { const antwort = await api(`/verwaltung/instanzen/${encodeURIComponent(instanz.id)}/stop`, { method: "POST" }); modal.schliessen(); zeigeMeldung(antwort?.meldung || `'${instanz.name}' stopped.`, "info"); await ladeInstanzen(); } catch (err) { zeigeMeldung(`Stop failed: ${err.message}`, "gefahr"); anhaltenBtn.disabled = false; if (textSpan) textSpan.textContent = ` ${t("instances.stop")}`; } }); modal.oeffnen(); } async function holeFensterNachVorn(instanz) { try { const antwort = await api(`/verwaltung/instanzen/${encodeURIComponent(instanz.id)}/fenster`, { method: "POST" }); zeigeMeldung(antwort?.meldung || `Brought '${instanz.name}' to front.`, "info"); } catch (err) { zeigeMeldung(`Window focus failed: ${err.message}`, "warnung"); } } function oeffneNeueInstanzDialog() { const formular = erstelleElement("form", { klasse: "formular-raster", id: "neue-instanz-formular" }); const nameInput = erstelleElement("input", { type: "text", required: true, placeholder: t("instances.instanceNamePlaceholder"), autocomplete: "off" }); const hinweis = erstelleElement("p", { klasse: "hinweis", text: "..." }); formular.append( erstelleElement("label", { text: t("instances.instanceNameLabel") + ":" }), nameInput, hinweis ); const abbrechenBtn = erstelleElement("button", { type: "button", klasse: "schaltflaeche flach", onclick: () => modal.schliessen(), text: t("common.cancel") }); // No onclick and no form attribute of its own: erstelleModal wires submit buttons in the // footer to the form itself. Both together fired requestSubmit() twice — one click, two // POSTs, two instances. `disabled` does not help, because form.requestSubmit() ignores // a button's state. const erstellenBtn = erstelleElement("button", { type: "submit", klasse: "schaltflaeche primaer", text: t("instances.createTitle") }); const modal = erstelleModal(t("instances.createTitle"), formular, [abbrechenBtn, erstellenBtn]); formular.addEventListener("submit", async (e) => { e.preventDefault(); const name = nameInput.value.trim(); if (!name) return; erstellenBtn.disabled = true; try { const antwort = await api("/verwaltung/instanzen", { method: "POST", body: { name } }); modal.schliessen(); zeigeMeldung(antwort?.meldung || t("instances.created"), "erfolg"); await ladeInstanzen(); } catch (err) { zeigeMeldung(t("instances.createError", { error: err.message }), "gefahr"); } finally { erstellenBtn.disabled = false; } }); modal.oeffnen(); }