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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
69718122f3
commit
ad71682c2d
@@ -1,9 +1,9 @@
|
||||
// ClawdDotNet — Instanzverwaltung
|
||||
//
|
||||
// Leitfaden: Übersicht, Anlegen, Starten, Anhalten, Desktop-Fenster nach vorn holen,
|
||||
// Auswahl der aktiven Instanz für das WebUI. Kein innerHTML, strikt textContent.
|
||||
// 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,
|
||||
@@ -28,7 +28,7 @@ export async function rendereInstanzen() {
|
||||
|
||||
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
const titelGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
titelGruppe.append(erstelleElement("h1", { text: "Instanzverwaltung" }));
|
||||
titelGruppe.append(erstelleElement("h1", { text: t("instances.title") }));
|
||||
|
||||
const aktionenGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
|
||||
@@ -36,22 +36,22 @@ export async function rendereInstanzen() {
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Instanzliste manuell aktualisieren",
|
||||
title: t("instances.refresh"),
|
||||
onclick: () => ladeInstanzen()
|
||||
},
|
||||
erstelleIcon(PFADE.aktualisieren, 16),
|
||||
erstelleElement("span", { text: " Aktualisieren" })
|
||||
erstelleElement("span", { text: " " + t("instances.refresh") })
|
||||
);
|
||||
|
||||
const neuBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche primaer",
|
||||
title: "Neue Instanz anlegen",
|
||||
title: t("instances.newInstance"),
|
||||
onclick: oeffneNeueInstanzDialog
|
||||
},
|
||||
erstelleIcon(PFADE.plus, 16),
|
||||
erstelleElement("span", { text: " Neue Instanz" })
|
||||
erstelleElement("span", { text: " " + t("instances.newInstance") })
|
||||
);
|
||||
|
||||
aktionenGruppe.append(aktualisierenBtn, neuBtn);
|
||||
@@ -64,7 +64,6 @@ export async function rendereInstanzen() {
|
||||
|
||||
await ladeInstanzen();
|
||||
|
||||
// Hintergrund-Polling alle 4 Sekunden, da sich Zustände ohne Klick ändern können
|
||||
aktualisierungsIntervall = setInterval(() => {
|
||||
ladeInstanzen(true);
|
||||
}, 4000);
|
||||
@@ -86,7 +85,7 @@ async function ladeInstanzen(istHintergrund = false) {
|
||||
|
||||
if (!istHintergrund && raster.children.length === 0) {
|
||||
raster.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Instanzen…" })
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("common.loading") })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,10 +96,10 @@ async function ladeInstanzen(istHintergrund = false) {
|
||||
|
||||
if (!Array.isArray(instanzen) || instanzen.length === 0) {
|
||||
raster.append(
|
||||
erstelleKarte("Keine Instanzen gefunden", [
|
||||
erstelleKarte(t("common.none"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Es sind noch keine Instanzen eingerichtet. Lege mit 'Neue Instanz' eine an."
|
||||
text: "..."
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -114,87 +113,79 @@ async function ladeInstanzen(istHintergrund = false) {
|
||||
const zustandInfo = instanzZustandInfo(instanz.zustand);
|
||||
const istLaufend = instanz.zustand === "Running";
|
||||
const istStumm = instanz.zustand === "Silent";
|
||||
const istAbgestuerzt = instanz.zustand === "Crashed";
|
||||
|
||||
// Instanzkarte
|
||||
const karteKopfAktionen = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
if (istAktiv) {
|
||||
karteKopfAktionen.append(erstellePlakette("Aktiv im WebUI", "erfolg"));
|
||||
karteKopfAktionen.append(erstellePlakette(t("instances.activeBadge"), "erfolg"));
|
||||
}
|
||||
karteKopfAktionen.append(erstellePlakette(zustandInfo.text, zustandInfo.art));
|
||||
|
||||
// Details
|
||||
const dl = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dl.append(
|
||||
erstelleElement("dt", { text: "Instanz-Kennung" }),
|
||||
erstelleElement("dt", { text: "ID" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: instanz.id })),
|
||||
erstelleElement("dt", { text: "Zustand" }),
|
||||
erstelleElement("dt", { text: t("common.status") }),
|
||||
erstelleElement("dd", { text: zustandInfo.text }),
|
||||
erstelleElement("dt", { text: "Prozess (PID)" }),
|
||||
erstelleElement("dt", { text: "PID" }),
|
||||
erstelleElement("dd", { text: instanz.prozess ? String(instanz.prozess) : "—" }),
|
||||
erstelleElement("dt", { text: "Autostart durch Manager" }),
|
||||
erstelleElement("dt", { text: t("instances.autostart") }),
|
||||
erstelleElement("dd", {
|
||||
text: instanz.autostart ? "Aktiviert" : "Aus",
|
||||
text: instanz.autostart ? (t("common.yes")) : (t("common.no")),
|
||||
klasse: instanz.autostart ? "" : "gedaempft"
|
||||
})
|
||||
);
|
||||
|
||||
// Aktionsleiste
|
||||
const fussAktionen = erstelleElement("div", { klasse: "instanz-aktionen" });
|
||||
|
||||
// Auswählen-Knopf
|
||||
const waehlenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: istAktiv ? "schaltflaeche klein" : "schaltflaeche primaer klein",
|
||||
disabled: istAktiv,
|
||||
title: istAktiv ? "Diese Instanz wird aktuell im WebUI bedient" : "Diese Instanz für das WebUI auswählen",
|
||||
title: istAktiv ? t("instances.activeBadge") : t("instances.selectActive"),
|
||||
onclick: () => {
|
||||
setzeAktiveInstanz(instanz.id);
|
||||
zeigeMeldung(`Instanz '${instanz.name}' im WebUI ausgewählt.`, "info");
|
||||
zeigeMeldung(`'${instanz.name}' selected.`, "info");
|
||||
ladeInstanzen();
|
||||
}
|
||||
},
|
||||
erstelleElement("span", { text: istAktiv ? "Ausgewählt" : "Auswählen" })
|
||||
erstelleElement("span", { text: istAktiv ? t("instances.activeBadge") : t("instances.selectActive") })
|
||||
);
|
||||
|
||||
// Starten-Knopf
|
||||
const startenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach klein",
|
||||
disabled: istLaufend,
|
||||
title: istLaufend ? "Instanz läuft bereits" : "Instanz starten",
|
||||
title: t("instances.start"),
|
||||
onclick: () => starteInstanz(instanz)
|
||||
},
|
||||
erstelleIcon(PFADE.abspielen, 14),
|
||||
erstelleElement("span", { text: " Starten" })
|
||||
erstelleElement("span", { text: " " + t("instances.start") })
|
||||
);
|
||||
|
||||
// Anhalten-Knopf
|
||||
const anhaltenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche gefahr flach klein",
|
||||
disabled: !istLaufend && !istStumm,
|
||||
title: (!istLaufend && !istStumm) ? "Instanz läuft nicht" : "Instanz geordnet anhalten",
|
||||
title: t("instances.stop"),
|
||||
onclick: () => stoppeInstanz(instanz)
|
||||
},
|
||||
erstelleIcon(PFADE.stop, 14),
|
||||
erstelleElement("span", { text: " Anhalten" })
|
||||
erstelleElement("span", { text: " " + t("instances.stop") })
|
||||
);
|
||||
|
||||
// Fenster nach vorn
|
||||
const fensterBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach klein",
|
||||
disabled: !istLaufend,
|
||||
title: !istLaufend ? "Instanz läuft nicht" : "Desktop-Fenster dieser Instanz in den Vordergrund holen",
|
||||
title: t("instances.bringToFront"),
|
||||
onclick: () => holeFensterNachVorn(instanz)
|
||||
},
|
||||
erstelleIcon(PFADE.fenster, 14),
|
||||
erstelleElement("span", { text: " Fenster" })
|
||||
erstelleElement("span", { text: " " + t("instances.bringToFront") })
|
||||
);
|
||||
|
||||
fussAktionen.append(waehlenBtn, startenBtn, anhaltenBtn, fensterBtn);
|
||||
@@ -209,7 +200,7 @@ async function ladeInstanzen(istHintergrund = false) {
|
||||
} catch (err) {
|
||||
if (!istHintergrund) {
|
||||
raster.replaceChildren(
|
||||
erstelleElement("div", { klasse: "fehler-zentriert", text: `Fehler beim Laden: ${err.message}` })
|
||||
erstelleElement("div", { klasse: "fehler-zentriert", text: `Error: ${err.message}` })
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -220,17 +211,16 @@ async function starteInstanz(instanz) {
|
||||
const antwort = await api(`/verwaltung/instanzen/${encodeURIComponent(instanz.id)}/start`, {
|
||||
method: "POST"
|
||||
});
|
||||
zeigeMeldung(antwort?.meldung || `Instanz '${instanz.name}' wird gestartet…`, "erfolg");
|
||||
// Wenn bisher keine Instanz im WebUI gewählt war, diese direkt wählen
|
||||
zeigeMeldung(antwort?.meldung || `Starting '${instanz.name}'…`, "erfolg");
|
||||
if (!holeAktiveInstanz()) {
|
||||
setzeAktiveInstanz(instanz.id);
|
||||
}
|
||||
await ladeInstanzen();
|
||||
} catch (err) {
|
||||
if (err.lizenzFehlt) {
|
||||
zeigeMeldung("Start abgelehnt: Ohne gültige Lizenz startet keine Instanz (HTTP 402).", "gefahr", 6000);
|
||||
zeigeMeldung("License required (HTTP 402).", "gefahr", 6000);
|
||||
} else {
|
||||
zeigeMeldung(`Start fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(`Failed to start: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,47 +235,40 @@ function oeffneSicherheitsStoppDialog(instanz) {
|
||||
|
||||
const inhalt = erstelleElement("div", { klasse: "formular-raster" });
|
||||
|
||||
// Sicherheits-Warnbox
|
||||
const sicherheitsBox = erstelleElement("div", { klasse: "sicherheits-box" });
|
||||
const boxTitel = erstelleElement(
|
||||
"h3",
|
||||
{},
|
||||
erstelleIcon(PFADE.ausrufezeichen, 18),
|
||||
erstelleElement("span", { text: "Achtung: Produktivinstanz beenden" })
|
||||
erstelleElement("span", { text: " Stop Instance" })
|
||||
);
|
||||
const boxText1 = erstelleElement("p", {
|
||||
text: "ClawdDotNet-Instanzen sind auf Dauerbetrieb ausgelegt. Das Beenden stoppt alle aktiven Agenten, Automationen, Web-Zugriffe und Hintergrundaufgaben dieser Instanz geordnet."
|
||||
text: `Stopping instance '${instanz.name}' will gracefully shut down all active agents and processes.`
|
||||
});
|
||||
const boxText2 = erstelleElement("p", {
|
||||
text: "Um ein versehentliches Beenden durch unbedachtes Klicken zu verhindern, ist die Bestätigung für 10 Sekunden gesperrt."
|
||||
});
|
||||
sicherheitsBox.append(boxTitel, boxText1, boxText2);
|
||||
sicherheitsBox.append(boxTitel, boxText1);
|
||||
|
||||
// Instanzdetails
|
||||
const dl = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dl.append(
|
||||
erstelleElement("dt", { text: "Instanz" }),
|
||||
erstelleElement("dt", { text: "Instance" }),
|
||||
erstelleElement("dd", {}, erstelleElement("strong", { text: instanz.name })),
|
||||
erstelleElement("dt", { text: "Kennung" }),
|
||||
erstelleElement("dt", { text: "ID" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: instanz.id })),
|
||||
erstelleElement("dt", { text: "Prozess (PID)" }),
|
||||
erstelleElement("dt", { text: "PID" }),
|
||||
erstelleElement("dd", { text: instanz.prozess ? String(instanz.prozess) : "—" })
|
||||
);
|
||||
|
||||
// Countdown-Anzeige
|
||||
const countdownBox = erstelleElement("div", {
|
||||
klasse: "sicherheits-countdown",
|
||||
text: `Bestätigung gesperrt: Noch ${verbleibend} Sekunden…`
|
||||
text: `Confirmation locked: ${verbleibend}s…`
|
||||
});
|
||||
|
||||
inhalt.append(sicherheitsBox, dl, countdownBox);
|
||||
|
||||
// Aktionen
|
||||
const abbrechenBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: () => modal.schliessen(),
|
||||
text: "Abbrechen (Instanz läuft weiter)"
|
||||
text: t("common.cancel")
|
||||
});
|
||||
|
||||
const anhaltenBtn = erstelleElement(
|
||||
@@ -296,11 +279,11 @@ function oeffneSicherheitsStoppDialog(instanz) {
|
||||
disabled: true
|
||||
},
|
||||
erstelleIcon(PFADE.stop, 14),
|
||||
erstelleElement("span", { text: ` Instanz anhalten (${verbleibend}s)` })
|
||||
erstelleElement("span", { text: ` ${t("instances.stop")} (${verbleibend}s)` })
|
||||
);
|
||||
|
||||
const modal = erstelleModal(
|
||||
`Instanz anhalten — ${instanz.name}`,
|
||||
`${t("instances.stop")} — ${instanz.name}`,
|
||||
inhalt,
|
||||
[abbrechenBtn, anhaltenBtn],
|
||||
{
|
||||
@@ -316,17 +299,17 @@ function oeffneSicherheitsStoppDialog(instanz) {
|
||||
timerId = setInterval(() => {
|
||||
verbleibend--;
|
||||
if (verbleibend > 0) {
|
||||
countdownBox.textContent = `Bestätigung gesperrt: Noch ${verbleibend} Sekunden…`;
|
||||
countdownBox.textContent = `Confirmation locked: ${verbleibend}s…`;
|
||||
const textSpan = anhaltenBtn.querySelector("span");
|
||||
if (textSpan) textSpan.textContent = ` Instanz anhalten (${verbleibend}s)`;
|
||||
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 = " Instanz jetzt endgültig anhalten";
|
||||
if (textSpan) textSpan.textContent = ` ${t("instances.stop")}`;
|
||||
countdownBox.className = "sicherheits-countdown abgelaufen";
|
||||
countdownBox.textContent = "Sperre aufgehoben — Bestätigung jetzt möglich.";
|
||||
countdownBox.textContent = "Unlocked — confirmation ready.";
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
@@ -334,19 +317,19 @@ function oeffneSicherheitsStoppDialog(instanz) {
|
||||
if (verbleibend > 0) return;
|
||||
anhaltenBtn.disabled = true;
|
||||
const textSpan = anhaltenBtn.querySelector("span");
|
||||
if (textSpan) textSpan.textContent = " Wird angehalten…";
|
||||
if (textSpan) textSpan.textContent = " Stopping…";
|
||||
|
||||
try {
|
||||
const antwort = await api(`/verwaltung/instanzen/${encodeURIComponent(instanz.id)}/stop`, {
|
||||
method: "POST"
|
||||
});
|
||||
modal.schliessen();
|
||||
zeigeMeldung(antwort?.meldung || `Instanz '${instanz.name}' beendet.`, "info");
|
||||
zeigeMeldung(antwort?.meldung || `'${instanz.name}' stopped.`, "info");
|
||||
await ladeInstanzen();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Anhalten fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(`Stop failed: ${err.message}`, "gefahr");
|
||||
anhaltenBtn.disabled = false;
|
||||
if (textSpan) textSpan.textContent = " Instanz jetzt endgültig anhalten";
|
||||
if (textSpan) textSpan.textContent = ` ${t("instances.stop")}`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -358,9 +341,9 @@ async function holeFensterNachVorn(instanz) {
|
||||
const antwort = await api(`/verwaltung/instanzen/${encodeURIComponent(instanz.id)}/fenster`, {
|
||||
method: "POST"
|
||||
});
|
||||
zeigeMeldung(antwort?.meldung || `Fenster von '${instanz.name}' nach vorn geholt.`, "info");
|
||||
zeigeMeldung(antwort?.meldung || `Brought '${instanz.name}' to front.`, "info");
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fensteraufruf fehlgeschlagen: ${err.message}`, "warnung");
|
||||
zeigeMeldung(`Window focus failed: ${err.message}`, "warnung");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,17 +356,17 @@ function oeffneNeueInstanzDialog() {
|
||||
const nameInput = erstelleElement("input", {
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "z.B. Produktion oder Agenten-Labor",
|
||||
placeholder: t("instances.instanceNamePlaceholder"),
|
||||
autocomplete: "off"
|
||||
});
|
||||
|
||||
const hinweis = erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Der Instanzmanager legt dafür ein eigenes Verzeichnis mit getrennten Protokollen und Einstellungen an."
|
||||
text: "..."
|
||||
});
|
||||
|
||||
formular.append(
|
||||
erstelleElement("label", { text: "Name der neuen Instanz:" }),
|
||||
erstelleElement("label", { text: t("instances.instanceNameLabel") + ":" }),
|
||||
nameInput,
|
||||
hinweis
|
||||
);
|
||||
@@ -392,20 +375,20 @@ function oeffneNeueInstanzDialog() {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: () => modal.schliessen(),
|
||||
text: "Abbrechen"
|
||||
text: t("common.cancel")
|
||||
});
|
||||
|
||||
// Kein eigenes onclick und kein form-Attribut: erstelleModal verknüpft Submit-Knöpfe
|
||||
// im Fuß selbst mit dem Formular. Beides zusammen löste requestSubmit() zweimal aus —
|
||||
// ein Klick, zwei POST, zwei Instanzen. `disabled` hilft dagegen nicht, denn
|
||||
// form.requestSubmit() schert sich nicht um den Zustand eines Knopfes.
|
||||
// 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: "Instanz anlegen"
|
||||
text: t("instances.createTitle")
|
||||
});
|
||||
|
||||
const modal = erstelleModal("Neue Instanz anlegen", formular, [abbrechenBtn, erstellenBtn]);
|
||||
const modal = erstelleModal(t("instances.createTitle"), formular, [abbrechenBtn, erstellenBtn]);
|
||||
|
||||
formular.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -420,10 +403,10 @@ function oeffneNeueInstanzDialog() {
|
||||
body: { name }
|
||||
});
|
||||
modal.schliessen();
|
||||
zeigeMeldung(antwort?.meldung || `Instanz '${name}' erfolgreich angelegt.`, "erfolg");
|
||||
zeigeMeldung(antwort?.meldung || t("instances.created"), "erfolg");
|
||||
await ladeInstanzen();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fehler beim Anlegen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("instances.createError", { error: err.message }), "gefahr");
|
||||
} finally {
|
||||
erstellenBtn.disabled = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user