Umbau durch Gemini Flash, Durchsicht und Nacharbeit durch Claude. Umfang: - Desktop: LocalizationManager (JSON-Pakete, Rueckfall auf Englisch, Umschalten zur Laufzeit), Localize-Markup-Erweiterung, Sprachwahl in den Einstellungen (AppSettings.Language, Vorgabe "en"); 317 Schluessel je Sprache - WebUI: alle Ansichten auf t()/data-i18n, 656 Schluessel je Sprache - Agenten-Werkzeuge: Beschreibungen, Schemata und Meldungen auf Englisch - Tests an die englischen Meldungen angepasst, keine Pruefung abgeschwaecht Nacharbeit bei der Durchsicht: - ~25 Kommentarbloecke mit Begruendungen waren ersatzlos entfallen, darunter sicherheitsrelevante (NTFS-Datenstrom beim shared:-Praefix, Bremse gegen Agentenschleifen, doppelter POST beim Anlegen einer Instanz). Wieder eingesetzt: auf Englisch, wo die Datei umgestellt ist, sonst im Wortlaut. - Drei WebUI-Schluessel fehlten (logs.paused, instances.instanceOfflineError, instances.noInstanceActiveError). Weil t() bei fehlendem Schluessel den Schluessel selbst liefert, griff der "|| err.message"-Rueckfall nie — die Oberflaeche haette den Schluesselnamen angezeigt. Geprueft: Schluesselgleichheit en/de, Platzhalter je Schluessel, jeder im Code benutzte Schluessel vorhanden, node --check fuer alle Module, 1.220 Tests gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
417 lines
12 KiB
JavaScript
417 lines
12 KiB
JavaScript
// 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();
|
|
}
|