Files
ClawdDotNet/src/ClawdDotNet.Manager.Web/wwwroot/views/instances.js
T
RichardandClaude Opus 5.5 ad71682c2d i18n: Englisch als Hauptsprache, Sprachpakete fuer Desktop und WebUI (#661)
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>
2026-09-24 09:43:51 +02:00

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();
}