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
@@ -8,6 +8,7 @@
|
||||
|
||||
import { api, holeAktiveInstanz } from "../modules/api.js";
|
||||
import { abonnieren } from "../modules/events.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleKarte,
|
||||
@@ -60,7 +61,7 @@ export async function rendereAgentenChats() {
|
||||
const kopf = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
const titelGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
titelGruppe.append(
|
||||
erstelleElement("h1", { text: "Agenten-Kommunikation (A2A)" })
|
||||
erstelleElement("h1", { text: t("agentChats.title") })
|
||||
);
|
||||
|
||||
const filterGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
@@ -68,43 +69,43 @@ export async function rendereAgentenChats() {
|
||||
const agentSelect = erstelleElement("select", {
|
||||
klasse: "eingabe",
|
||||
stil: { minWidth: "180px" },
|
||||
title: "Nach beteiligtem Agenten filtern (Absender oder Empfänger)"
|
||||
title: t("agentChats.filterTooltip")
|
||||
});
|
||||
agentSelect.append(erstelleElement("option", { value: "", text: "Alle Agenten" }));
|
||||
agentSelect.append(erstelleElement("option", { value: "", text: t("agentChats.allAgents") }));
|
||||
|
||||
const vonEingabe = erstelleElement("input", {
|
||||
klasse: "eingabe",
|
||||
type: "date",
|
||||
title: "Nachrichten ab diesem Tag"
|
||||
title: t("agentChats.fromTooltip")
|
||||
});
|
||||
const bisEingabe = erstelleElement("input", {
|
||||
klasse: "eingabe",
|
||||
type: "date",
|
||||
title: "Nachrichten bis einschließlich diesem Tag"
|
||||
title: t("agentChats.toTooltip")
|
||||
});
|
||||
const suchEingabe = erstelleElement("input", {
|
||||
klasse: "eingabe",
|
||||
type: "search",
|
||||
placeholder: "Volltext…",
|
||||
title: "Suche im Nachrichtentext und in den Agentennamen"
|
||||
placeholder: t("agentChats.searchPlaceholder"),
|
||||
title: t("agentChats.searchTooltip")
|
||||
});
|
||||
|
||||
const aktualisierenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Kommunikation neu laden",
|
||||
title: t("agentChats.refreshTooltip"),
|
||||
onclick: () => ladeVerlauf()
|
||||
},
|
||||
erstelleIcon(PFADE.aktualisieren, 16),
|
||||
erstelleElement("span", { text: " Aktualisieren" })
|
||||
erstelleElement("span", { text: ` ${t("agentChats.refresh")}` })
|
||||
);
|
||||
|
||||
filterGruppe.append(
|
||||
agentSelect,
|
||||
erstelleElement("span", { klasse: "gedaempft", text: "von" }),
|
||||
erstelleElement("span", { klasse: "gedaempft", text: t("agentChats.from") }),
|
||||
vonEingabe,
|
||||
erstelleElement("span", { klasse: "gedaempft", text: "bis" }),
|
||||
erstelleElement("span", { klasse: "gedaempft", text: t("agentChats.to") }),
|
||||
bisEingabe,
|
||||
suchEingabe,
|
||||
aktualisierenBtn
|
||||
@@ -114,10 +115,10 @@ export async function rendereAgentenChats() {
|
||||
|
||||
if (!instanzId) {
|
||||
layout.append(
|
||||
erstelleKarte("Keine Instanz ausgewählt", [
|
||||
erstelleKarte(t("agentChats.noInstance"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Bitte wähle oben im Kopfbereich eine Instanz aus, um deren Agenten-Kommunikation anzuzeigen."
|
||||
text: t("agentChats.selectInstancePrompt")
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -126,10 +127,10 @@ export async function rendereAgentenChats() {
|
||||
}
|
||||
|
||||
// Infokarte
|
||||
const infoKarte = erstelleKarte("Interne Kommunikation", [
|
||||
const infoKarte = erstelleKarte(t("agentChats.infoTitle"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Hier wird der direkte Nachrichtenaustausch zwischen Agenten protokolliert, wenn Agenten über das Werkzeug AgentComm (Aktion send_message) miteinander sprechen."
|
||||
text: t("agentChats.infoHint")
|
||||
})
|
||||
]);
|
||||
layout.append(infoKarte);
|
||||
@@ -167,7 +168,7 @@ export async function rendereAgentenChats() {
|
||||
|
||||
async function ladeVerlauf() {
|
||||
verlaufContainer.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Kommunikationsverlauf…" })
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("agentChats.loading") })
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -181,10 +182,10 @@ export async function rendereAgentenChats() {
|
||||
rendereNachrichten();
|
||||
} catch (err) {
|
||||
verlaufContainer.replaceChildren(
|
||||
erstelleKarte("Fehler", [
|
||||
erstelleKarte(t("common.unknown"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "fehler-meldung",
|
||||
text: `Kommunikation konnte nicht geladen werden: ${err.message}`
|
||||
text: t("agentChats.loadFailed", { error: err.message })
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -216,17 +217,17 @@ export async function rendereAgentenChats() {
|
||||
const filterAktiv = agentSelect.value || vonEingabe.value || bisEingabe.value || suchEingabe.value.trim();
|
||||
|
||||
trefferZeile.textContent = filterAktiv
|
||||
? `${gefiltert.length} von ${alleNachrichten.length} Nachrichten`
|
||||
: `${alleNachrichten.length} Nachrichten`;
|
||||
? t("agentChats.matchCount", { filtered: gefiltert.length, total: alleNachrichten.length })
|
||||
: t("agentChats.totalCount", { total: alleNachrichten.length });
|
||||
|
||||
if (gefiltert.length === 0) {
|
||||
verlaufContainer.replaceChildren(
|
||||
erstelleKarte("Keine Nachrichten", [
|
||||
erstelleKarte(t("agentChats.empty"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: filterAktiv
|
||||
? "Für diese Filter liegt keine interne Kommunikation vor."
|
||||
: "In dieser Instanz wurde bisher keine interne Agent-zu-Agent-Kommunikation protokolliert."
|
||||
? t("agentChats.emptyFilter")
|
||||
: t("agentChats.emptyInstance")
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -245,12 +246,12 @@ export async function rendereAgentenChats() {
|
||||
const kopf = erstelleElement("div", { klasse: "a2a-eintrag-kopf" });
|
||||
const absenderGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
|
||||
const quelleName = m.sourceAgentName || m.sourceAgentId || "unbekannt";
|
||||
const zielName = m.targetAgentName || m.targetAgentId || "unbekannt";
|
||||
const quelleName = m.sourceAgentName || m.sourceAgentId || t("common.unknown");
|
||||
const zielName = m.targetAgentName || m.targetAgentId || t("common.unknown");
|
||||
|
||||
absenderGruppe.append(
|
||||
erstellePlakette(quelleName, istAntwort ? "erfolg" : "neutral"),
|
||||
erstelleElement("span", { klasse: "a2a-pfeil", text: istAntwort ? "➔ Antwort an" : "➔" }),
|
||||
erstelleElement("span", { klasse: "a2a-pfeil", text: istAntwort ? t("agentChats.replyTo") : t("agentChats.arrow") }),
|
||||
erstellePlakette(zielName, istAntwort ? "neutral" : "primaer")
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import { abonnieren } from "../modules/events.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleIcon,
|
||||
@@ -30,7 +31,7 @@ export async function rendereAgenten(parameter = {}) {
|
||||
|
||||
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
kopfZeile.append(
|
||||
erstelleElement("h1", { text: "Agenten" }),
|
||||
erstelleElement("h1", { text: t("agents.title") }),
|
||||
erstelleElement(
|
||||
"button",
|
||||
{
|
||||
@@ -38,7 +39,7 @@ export async function rendereAgenten(parameter = {}) {
|
||||
onclick: oeffneNeuerAgentDialog
|
||||
},
|
||||
erstelleIcon(PFADE.plus, 16),
|
||||
erstelleElement("span", { text: " Neuer Agent" })
|
||||
erstelleElement("span", { text: ` ${t("agents.newAgent")}` })
|
||||
)
|
||||
);
|
||||
|
||||
@@ -66,7 +67,7 @@ async function ladeUndRendereListe(zielId = null) {
|
||||
if (!listeContainer) return;
|
||||
|
||||
listeContainer.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Agenten…" })
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("agents.loading") })
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -76,12 +77,12 @@ async function ladeUndRendereListe(zielId = null) {
|
||||
if (!agenten || agenten.length === 0) {
|
||||
const leerKasten = erstelleElement("div", { klasse: "hinweis-zentriert" });
|
||||
leerKasten.append(
|
||||
erstelleElement("p", { klasse: "gedaempft", text: "In dieser Instanz sind noch keine Agenten eingerichtet." }),
|
||||
erstelleElement("p", { klasse: "gedaempft", text: t("agents.empty") }),
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche primaer klein",
|
||||
onclick: oeffneNeuerAgentDialog,
|
||||
style: "margin-top: 10px;"
|
||||
}, erstelleIcon(PFADE.plus, 14), erstelleElement("span", { text: " Ersten Agenten anlegen" }))
|
||||
}, erstelleIcon(PFADE.plus, 14), erstelleElement("span", { text: ` ${t("agents.createFirst")}` }))
|
||||
);
|
||||
listeContainer.append(leerKasten);
|
||||
rendereDetails(null);
|
||||
@@ -105,20 +106,20 @@ async function ladeUndRendereListe(zielId = null) {
|
||||
const top = erstelleElement("div", { klasse: "agent-karte-top" });
|
||||
const name = erstelleElement("strong", { text: a.displayName });
|
||||
const status = a.isRunning
|
||||
? erstellePlakette("Beschäftigt", "warnung")
|
||||
: erstellePlakette("Bereit", "neutral");
|
||||
? erstellePlakette(t("status.busy"), "warnung")
|
||||
: erstellePlakette(t("status.ready"), "neutral");
|
||||
top.append(name, status);
|
||||
|
||||
const meta = erstelleElement("div", { klasse: "agent-karte-meta" });
|
||||
meta.append(
|
||||
erstelleElement("span", { klasse: "gedaempft", text: `ID: ${a.agentId}` }),
|
||||
erstelleElement("span", { klasse: "gedaempft", text: ` · Modell: ${a.model}` })
|
||||
erstelleElement("span", { klasse: "gedaempft", text: `${t("agents.id")}: ${a.agentId}` }),
|
||||
erstelleElement("span", { klasse: "gedaempft", text: ` · ${t("agents.model")}: ${a.model}` })
|
||||
);
|
||||
|
||||
const werkzeugeZaehler = a.tools ? a.tools.filter((t) => t.enabled).length : 0;
|
||||
const werkzeugeText = erstelleElement("div", {
|
||||
klasse: "agent-karte-werkzeuge gedaempft",
|
||||
text: `${werkzeugeZaehler} Werkzeuge aktiv`
|
||||
text: t("agents.toolsActive", { count: werkzeugeZaehler })
|
||||
});
|
||||
|
||||
eintrag.append(top, meta, werkzeugeText);
|
||||
@@ -128,9 +129,9 @@ async function ladeUndRendereListe(zielId = null) {
|
||||
rendereDetails(auswahlId);
|
||||
} catch (err) {
|
||||
const text = err.instanzUnerreichbar
|
||||
? "Die ausgewählte Instanz läuft derzeit nicht. Bitte starte sie zuerst im Kopfbereich oder unter 'Instanzen'."
|
||||
? (t("instances.instanceOfflineError") || err.message)
|
||||
: (err.instanzKonflikt
|
||||
? "Keine aktive Instanz gewählt. Bitte wähle oben eine Instanz aus."
|
||||
? (t("instances.noInstanceActiveError") || err.message)
|
||||
: err.message);
|
||||
listeContainer.replaceChildren(
|
||||
erstelleElement("div", { klasse: "fehler-zentriert", text })
|
||||
@@ -146,8 +147,8 @@ async function rendereDetails(agentId) {
|
||||
|
||||
if (!agentId) {
|
||||
detailContainer.append(
|
||||
erstelleKarte("Agent-Details", [
|
||||
erstelleElement("p", { klasse: "gedaempft", text: "Wähle links einen Agenten aus." })
|
||||
erstelleKarte(t("agents.details"), [
|
||||
erstelleElement("p", { klasse: "gedaempft", text: t("agents.selectPrompt") })
|
||||
])
|
||||
);
|
||||
return;
|
||||
@@ -163,24 +164,24 @@ async function rendereDetails(agentId) {
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Mit diesem Agenten chatten",
|
||||
title: t("agents.chatTooltip"),
|
||||
onclick: () => {
|
||||
window.location.hash = `#/chat/${encodeURIComponent(agent.agentId)}`;
|
||||
}
|
||||
},
|
||||
erstelleIcon(PFADE.chat, 14),
|
||||
erstelleElement("span", { text: " Chatten" })
|
||||
erstelleElement("span", { text: ` ${t("agents.chat")}` })
|
||||
);
|
||||
|
||||
const loeschenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche gefahr flach",
|
||||
title: "Diesen Agenten löschen",
|
||||
title: t("agents.deleteTooltip"),
|
||||
onclick: () => loescheAgent(agent.agentId, agent.displayName)
|
||||
},
|
||||
erstelleIcon(PFADE.muelleimer, 14),
|
||||
erstelleElement("span", { text: " Löschen" })
|
||||
erstelleElement("span", { text: ` ${t("agents.delete")}` })
|
||||
);
|
||||
|
||||
aktionen.append(zumChatBtn, loeschenBtn);
|
||||
@@ -188,10 +189,10 @@ async function rendereDetails(agentId) {
|
||||
// Stammdaten-Karte
|
||||
const stammdatenListe = erstelleElement("dl", { klasse: "datenliste" });
|
||||
const felder = [
|
||||
["Name", agent.displayName],
|
||||
["Kennung (ID)", agent.agentId],
|
||||
["Modell", agent.model],
|
||||
["Beschreibung", agent.description || "—"]
|
||||
[t("agents.name"), agent.displayName],
|
||||
[t("agents.id"), agent.agentId],
|
||||
[t("agents.model"), agent.model],
|
||||
[t("agents.description"), agent.description || "—"]
|
||||
];
|
||||
|
||||
for (const [bez, wert] of felder) {
|
||||
@@ -208,7 +209,7 @@ async function rendereDetails(agentId) {
|
||||
|
||||
if (!agent.tools || agent.tools.length === 0) {
|
||||
werkzeugeListe.append(
|
||||
erstelleElement("p", { klasse: "gedaempft", text: "Keine Werkzeuge für diesen Agenten konfiguriert." })
|
||||
erstelleElement("p", { klasse: "gedaempft", text: t("agents.noTools") })
|
||||
);
|
||||
} else {
|
||||
for (const tool of agent.tools) {
|
||||
@@ -228,11 +229,11 @@ async function rendereDetails(agentId) {
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach klein",
|
||||
title: "Werkzeug-Einstellungen einsehen",
|
||||
title: t("agents.configTooltip"),
|
||||
onclick: () => zeigeWerkzeugEinstellungen(agent.agentId, tool.name)
|
||||
},
|
||||
erstelleIcon(PFADE.einstellungen, 14),
|
||||
erstelleElement("span", { text: " Konfiguration" })
|
||||
erstelleElement("span", { text: ` ${t("agents.configuration")}` })
|
||||
);
|
||||
|
||||
const toggleLabel = erstelleElement("label", { klasse: "schalter" });
|
||||
@@ -247,10 +248,16 @@ async function rendereDetails(agentId) {
|
||||
{ method: "PUT" }
|
||||
);
|
||||
tool.enabled = aktiv;
|
||||
zeigeMeldung(`Werkzeug '${tool.name}' ${aktiv ? "aktiviert" : "deaktiviert"}.`, "info");
|
||||
zeigeMeldung(
|
||||
t("agents.toolToggled", {
|
||||
tool: tool.name,
|
||||
status: aktiv ? t("agents.enabled") : t("agents.disabled")
|
||||
}),
|
||||
"info"
|
||||
);
|
||||
} catch (err) {
|
||||
e.target.checked = !aktiv;
|
||||
zeigeMeldung(`Änderung fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("agents.saveFailed", { error: err.message }), "gefahr");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -263,7 +270,7 @@ async function rendereDetails(agentId) {
|
||||
}
|
||||
}
|
||||
|
||||
const werkzeugeKarte = erstelleKarte("Verfügbare Werkzeuge", [werkzeugeListe]);
|
||||
const werkzeugeKarte = erstelleKarte(t("agents.availableTools"), [werkzeugeListe]);
|
||||
|
||||
detailContainer.append(basisKarte, werkzeugeKarte);
|
||||
}
|
||||
@@ -277,14 +284,14 @@ function oeffneNeuerAgentDialog() {
|
||||
const idFeld = erstelleElement("input", {
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "z.B. scout",
|
||||
placeholder: t("agents.idPlaceholder"),
|
||||
id: "neu-agent-id"
|
||||
});
|
||||
|
||||
const nameFeld = erstelleElement("input", {
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "z.B. Scout Agent",
|
||||
placeholder: t("agents.namePlaceholder"),
|
||||
id: "neu-agent-name"
|
||||
});
|
||||
|
||||
@@ -298,18 +305,18 @@ function oeffneNeuerAgentDialog() {
|
||||
|
||||
const descFeld = erstelleElement("textarea", {
|
||||
rows: "3",
|
||||
placeholder: "Optionale Beschreibung des Agenten…",
|
||||
placeholder: t("agents.descPlaceholder"),
|
||||
id: "neu-agent-desc"
|
||||
});
|
||||
|
||||
formular.append(
|
||||
erstelleElement("label", { text: "Agent-ID (eindeutiger Schlüssel):" }),
|
||||
erstelleElement("label", { text: t("agents.idLabel") }),
|
||||
idFeld,
|
||||
erstelleElement("label", { text: "Anzeigename:" }),
|
||||
erstelleElement("label", { text: t("agents.nameLabel") }),
|
||||
nameFeld,
|
||||
erstelleElement("label", { text: "Modell (OpenRouter):" }),
|
||||
erstelleElement("label", { text: t("agents.modelLabel") }),
|
||||
modellFeld,
|
||||
erstelleElement("label", { text: "Beschreibung:" }),
|
||||
erstelleElement("label", { text: t("agents.descLabel") }),
|
||||
descFeld
|
||||
);
|
||||
|
||||
@@ -320,7 +327,7 @@ function oeffneNeuerAgentDialog() {
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: () => modal.schliessen()
|
||||
},
|
||||
erstelleElement("span", { text: "Abbrechen" })
|
||||
erstelleElement("span", { text: t("common.cancel") })
|
||||
);
|
||||
|
||||
const speichernBtn = erstelleElement(
|
||||
@@ -333,10 +340,10 @@ function oeffneNeuerAgentDialog() {
|
||||
formular.requestSubmit();
|
||||
}
|
||||
},
|
||||
erstelleElement("span", { text: "Agent anlegen" })
|
||||
erstelleElement("span", { text: t("agents.createButton") })
|
||||
);
|
||||
|
||||
const modal = erstelleModal("Neuen Agenten anlegen", formular, [abbrechenBtn, speichernBtn]);
|
||||
const modal = erstelleModal(t("agents.newAgentTitle"), formular, [abbrechenBtn, speichernBtn]);
|
||||
|
||||
formular.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -353,10 +360,10 @@ function oeffneNeuerAgentDialog() {
|
||||
}
|
||||
});
|
||||
modal.schliessen();
|
||||
zeigeMeldung(`Agent '${nameFeld.value.trim()}' wurde erfolgreich angelegt.`, "erfolg");
|
||||
zeigeMeldung(t("agents.createdSuccess", { name: nameFeld.value.trim() }), "erfolg");
|
||||
await ladeUndRendereListe(idFeld.value.trim());
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fehler beim Anlegen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("agents.createFailed", { error: err.message }), "gefahr");
|
||||
} finally {
|
||||
speichernBtn.disabled = false;
|
||||
}
|
||||
@@ -366,30 +373,30 @@ function oeffneNeuerAgentDialog() {
|
||||
}
|
||||
|
||||
async function loescheAgent(agentId, name) {
|
||||
if (!confirm(`Soll der Agent '${name}' (${agentId}) wirklich gelöscht werden?`)) return;
|
||||
if (!confirm(t("agents.deleteConfirm", { name, id: agentId }))) return;
|
||||
|
||||
try {
|
||||
await api(`/agents/${encodeURIComponent(agentId)}`, { method: "DELETE" });
|
||||
zeigeMeldung(`Agent '${name}' wurde gelöscht.`, "info");
|
||||
zeigeMeldung(t("agents.deleted", { name }), "info");
|
||||
await ladeUndRendereListe();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Löschen fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("agents.deleteFailed", { error: err.message }), "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
async function zeigeWerkzeugEinstellungen(agentId, toolName) {
|
||||
const box = erstelleElement("div", { klasse: "einstellungen-box" });
|
||||
box.append(erstelleElement("p", { klasse: "gedaempft", text: "Lade Konfiguration…" }));
|
||||
box.append(erstelleElement("p", { klasse: "gedaempft", text: t("agents.loadingSettings") }));
|
||||
|
||||
const schliessenBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: () => modal.schliessen(),
|
||||
text: "Schließen"
|
||||
text: t("common.close")
|
||||
});
|
||||
|
||||
const modal = erstelleModal(
|
||||
`Einstellungen: ${toolName} (${agentId})`,
|
||||
t("agents.settingsTitle", { tool: toolName, agent: agentId }),
|
||||
box,
|
||||
[schliessenBtn]
|
||||
);
|
||||
@@ -403,13 +410,13 @@ async function zeigeWerkzeugEinstellungen(agentId, toolName) {
|
||||
|
||||
const hinweis = erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Geheime Werte (Passwörter, Schlüssel) werden vom Server maskiert ausgegeben."
|
||||
text: t("agents.secretHint")
|
||||
});
|
||||
box.append(hinweis);
|
||||
|
||||
if (!daten || Object.keys(daten).length === 0) {
|
||||
box.append(
|
||||
erstelleElement("p", { klasse: "gedaempft", text: "Keine individuellen Einstellungen hinterlegt." })
|
||||
erstelleElement("p", { klasse: "gedaempft", text: t("agents.noSettings") })
|
||||
);
|
||||
} else {
|
||||
const dl = erstelleElement("dl", { klasse: "datenliste" });
|
||||
@@ -422,7 +429,7 @@ async function zeigeWerkzeugEinstellungen(agentId, toolName) {
|
||||
}
|
||||
} catch (err) {
|
||||
box.replaceChildren(
|
||||
erstelleElement("p", { klasse: "fehler", text: `Laden fehlgeschlagen: ${err.message}` })
|
||||
erstelleElement("p", { klasse: "fehler", text: t("agents.loadFailed", { error: err.message }) })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -431,11 +438,14 @@ async function zeigeWerkzeugEinstellungen(agentId, toolName) {
|
||||
await zeigeWerkzeugrechte(agentId, toolName, rechteBox);
|
||||
}
|
||||
|
||||
const WIRKUNG = {
|
||||
auto: ["läuft", "erfolg"],
|
||||
approve: ["einzeln freigeben", "warnung"],
|
||||
deny: ["gesperrt", "gefahr"]
|
||||
};
|
||||
function getWirkung(effective) {
|
||||
const map = {
|
||||
auto: [t("agents.effectAuto"), "erfolg"],
|
||||
approve: [t("agents.effectApprove"), "warnung"],
|
||||
deny: [t("agents.effectDeny"), "gefahr"]
|
||||
};
|
||||
return map[effective] ?? [effective, "neutral"];
|
||||
}
|
||||
|
||||
/**
|
||||
* #641: Rechte je Operation. "erlaubt" laeuft ohne Rueckfrage, "freigabe" wird einzeln
|
||||
@@ -448,17 +458,17 @@ async function zeigeWerkzeugrechte(agentId, toolName, box) {
|
||||
try {
|
||||
rechte = await api(pfad);
|
||||
} catch (err) {
|
||||
box.replaceChildren(erstelleElement("p", { klasse: "gedaempft", text: `Rechte nicht lesbar: ${err.message}` }));
|
||||
box.replaceChildren(erstelleElement("p", { klasse: "gedaempft", text: t("agents.loadFailed", { error: err.message }) }));
|
||||
return;
|
||||
}
|
||||
|
||||
box.replaceChildren(erstelleElement("h3", { text: "Rechte" }));
|
||||
box.replaceChildren(erstelleElement("h3", { text: t("agents.rights") }));
|
||||
|
||||
if (!rechte.operations || rechte.operations.length === 0) {
|
||||
box.append(
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Dieses Werkzeug kennt keine einzelnen Operationen. Es gilt die Standardregel."
|
||||
text: t("agents.noOperations")
|
||||
})
|
||||
);
|
||||
return;
|
||||
@@ -468,14 +478,14 @@ async function zeigeWerkzeugrechte(agentId, toolName, box) {
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: rechte.configured
|
||||
? "Erlaubte Operationen laufen ohne Rückfrage, alle anderen werden einzeln zur Freigabe vorgelegt, verbotene gar nicht erst."
|
||||
: "Noch keine Rechte gesetzt: Es gilt die Vorgabe des Werkzeugs. Mit dem Speichern werden die Einstellungen unten verbindlich."
|
||||
? t("agents.configuredHint")
|
||||
: t("agents.notConfiguredHint")
|
||||
})
|
||||
);
|
||||
|
||||
const tabelle = erstelleElement("table", { klasse: "daten-tabelle rechte-tabelle" });
|
||||
const kopf = erstelleElement("tr", {});
|
||||
for (const titel of ["Operation", "Einstellung", "Wirkung"]) {
|
||||
for (const titel of [t("agents.operation"), t("agents.setting"), t("agents.effect")]) {
|
||||
kopf.append(erstelleElement("th", { text: titel }));
|
||||
}
|
||||
tabelle.append(kopf);
|
||||
@@ -485,9 +495,9 @@ async function zeigeWerkzeugrechte(agentId, toolName, box) {
|
||||
const select = erstelleElement(
|
||||
"select",
|
||||
{ disabled: op.alwaysApproval },
|
||||
erstelleElement("option", { value: "erlaubt", text: "erlaubt" }),
|
||||
erstelleElement("option", { value: "freigabe", text: "einzeln freigeben" }),
|
||||
erstelleElement("option", { value: "verboten", text: "verboten" })
|
||||
erstelleElement("option", { value: "erlaubt", text: t("agents.allowed") }),
|
||||
erstelleElement("option", { value: "freigabe", text: t("agents.singleApproval") }),
|
||||
erstelleElement("option", { value: "verboten", text: t("agents.forbidden") })
|
||||
);
|
||||
// Ohne gesetzte Rechte die tatsaechliche Wirkung vorbelegen - sonst wuerde das erste
|
||||
// Speichern z.B. das freie Lesen still in eine Freigabepflicht verwandeln.
|
||||
@@ -496,14 +506,14 @@ async function zeigeWerkzeugrechte(agentId, toolName, box) {
|
||||
: op.effective === "auto" ? "erlaubt" : op.effective === "deny" ? "verboten" : "freigabe";
|
||||
auswahl.set(op.operation, select);
|
||||
|
||||
const [wirkungText, wirkungArt] = WIRKUNG[op.effective] ?? [op.effective, "neutral"];
|
||||
const [wirkungText, wirkungArt] = getWirkung(op.effective);
|
||||
const wirkung = erstelleElement("td", {}, erstellePlakette(wirkungText, wirkungArt));
|
||||
if (op.standingApprovalId) {
|
||||
wirkung.append(" ", erstellePlakette(`Dauerfreigabe #${op.standingApprovalId}`, "neutral"));
|
||||
wirkung.append(" ", erstellePlakette(t("agents.standingApproval", { id: op.standingApprovalId }), "neutral"));
|
||||
}
|
||||
wirkung.append(erstelleElement("div", {
|
||||
klasse: "grund",
|
||||
text: op.alwaysApproval ? "Änderungen an Agenten brauchen immer eine Freigabe." : op.reason
|
||||
text: op.alwaysApproval ? t("agents.alwaysApprovalReason") : op.reason
|
||||
}));
|
||||
|
||||
const zeile = erstelleElement("tr", {});
|
||||
@@ -515,7 +525,7 @@ async function zeigeWerkzeugrechte(agentId, toolName, box) {
|
||||
const speichern = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche primaer klein",
|
||||
text: "Rechte speichern",
|
||||
text: t("agents.saveRights"),
|
||||
onclick: async () => {
|
||||
const allowed = [];
|
||||
const denied = [];
|
||||
@@ -527,10 +537,10 @@ async function zeigeWerkzeugrechte(agentId, toolName, box) {
|
||||
speichern.disabled = true;
|
||||
try {
|
||||
const res = await api(pfad, { method: "PUT", body: { allowed, denied } });
|
||||
zeigeMeldung(res?.message ?? "Rechte gespeichert.", "erfolg");
|
||||
zeigeMeldung(res?.message ?? t("agents.rightsSaved"), "erfolg");
|
||||
await zeigeWerkzeugrechte(agentId, toolName, box);
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Speichern fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("agents.saveFailed", { error: err.message }), "gefahr");
|
||||
speichern.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// Strikt DOM API, kein innerHTML.
|
||||
|
||||
import { api, holeAktiveInstanz } from "../modules/api.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleKarte,
|
||||
@@ -29,11 +30,11 @@ function formatiereBytes(bytes) {
|
||||
|
||||
/** Was eine Sicherung schützt — als Plakette. */
|
||||
function schutzPlakette(b) {
|
||||
if (b.fremdeLizenz) return erstellePlakette("Fremde Lizenz", "warnung");
|
||||
if (b.secrets === "Unlesbar") return erstellePlakette("Unlesbar", "gefahr");
|
||||
if (b.verschluesselt) return erstellePlakette(`Lizenz (${b.secretCount} Zugangsdaten)`, "erfolg");
|
||||
if (b.secrets === "Passphrase") return erstellePlakette(`Passphrase (${b.secretCount})`, "neutral");
|
||||
return erstellePlakette("Unverschlüsselt, ohne Zugangsdaten", "neutral");
|
||||
if (b.fremdeLizenz) return erstellePlakette(t("backup.foreignLicense"), "warnung");
|
||||
if (b.secrets === "Unlesbar") return erstellePlakette(t("backup.unreadable"), "gefahr");
|
||||
if (b.verschluesselt) return erstellePlakette(t("backup.licenseProtection", { count: b.secretCount }), "erfolg");
|
||||
if (b.secrets === "Passphrase") return erstellePlakette(t("backup.passphraseProtection", { count: b.secretCount }), "neutral");
|
||||
return erstellePlakette(t("backup.unencryptedProtection"), "neutral");
|
||||
}
|
||||
|
||||
function checkbox(id, text, an) {
|
||||
@@ -63,20 +64,20 @@ export async function rendereSicherung() {
|
||||
const kopf = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
kopf.append(
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" },
|
||||
erstelleElement("h1", { text: "Datensicherung & Wiederherstellung" })),
|
||||
erstelleElement("h1", { text: t("backup.title") })),
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" },
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Sicherungen neu laden",
|
||||
title: t("backup.refreshTooltip"),
|
||||
disabled: !instanzId,
|
||||
onclick: () => ladeSicherungen()
|
||||
}, erstelleIcon(PFADE.aktualisieren, 16), erstelleElement("span", { text: " Aktualisieren" })),
|
||||
}, erstelleIcon(PFADE.aktualisieren, 16), erstelleElement("span", { text: ` ${t("backup.refresh")}` })),
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche primaer",
|
||||
title: "Neue Sicherung erstellen",
|
||||
title: t("backup.newBackupTooltip"),
|
||||
disabled: !instanzId,
|
||||
onclick: () => oeffneSicherungsDialog()
|
||||
}, erstelleIcon(PFADE.plus, 16), erstelleElement("span", { text: " Neue Sicherung" }))
|
||||
}, erstelleIcon(PFADE.plus, 16), erstelleElement("span", { text: ` ${t("backup.newBackup")}` }))
|
||||
)
|
||||
);
|
||||
layout.append(kopf);
|
||||
@@ -87,10 +88,10 @@ export async function rendereSicherung() {
|
||||
|
||||
if (!instanzId) {
|
||||
inhaltBereich.append(
|
||||
erstelleKarte("Keine Instanz ausgewählt", [
|
||||
erstelleKarte(t("backup.noInstance"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Bitte wähle oben im Kopfbereich eine Instanz aus, um deren Sicherungen zu verwalten."
|
||||
text: t("backup.selectInstancePrompt")
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -107,7 +108,7 @@ export async function rendereSicherung() {
|
||||
// ─── Liste ───
|
||||
async function ladeSicherungen() {
|
||||
inhaltBereich.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Sicherungen…" })
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("backup.loading") })
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -115,12 +116,10 @@ export async function rendereSicherung() {
|
||||
const backups = daten?.backups ?? [];
|
||||
|
||||
infoBereich.replaceChildren(
|
||||
erstelleKarte("Sicherungsverzeichnis", [
|
||||
erstelleKarte(t("backup.backupDir"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Sicherungen werden als Ganzes mit dem Lizenzschlüssel des Instanzmanagers "
|
||||
+ "verschlüsselt und enthalten die Zugangsdaten. Auf einer anderen Maschine lassen "
|
||||
+ "sie sich mit demselben Lizenzschlüssel zurückspielen."
|
||||
text: t("backup.backupDirHint")
|
||||
}),
|
||||
erstelleElement("div", { klasse: "ordner-pfad-anzeige" },
|
||||
erstelleIcon(PFADE.server, 14),
|
||||
@@ -130,10 +129,10 @@ export async function rendereSicherung() {
|
||||
|
||||
if (backups.length === 0) {
|
||||
inhaltBereich.replaceChildren(
|
||||
erstelleKarte("Vorhandene Sicherungen", [
|
||||
erstelleKarte(t("backup.existingBackups", { count: 0 }), [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Für diese Instanz gibt es noch keine Sicherung. Lege über die Schaltfläche oben eine an."
|
||||
text: t("backup.noBackups")
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -146,13 +145,13 @@ export async function rendereSicherung() {
|
||||
const aktionen = erstelleElement("td", { klasse: "spalte-aktionen" },
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche klein sekundär",
|
||||
title: "Diese Sicherung wiederherstellen",
|
||||
title: t("backup.restoreTooltip"),
|
||||
disabled: b.secrets === "Unlesbar",
|
||||
onclick: () => oeffneRestoreDialog(b)
|
||||
}, erstelleIcon(PFADE.abspielen, 14), erstelleElement("span", { text: " Wiederherstellen" })),
|
||||
}, erstelleIcon(PFADE.abspielen, 14), erstelleElement("span", { text: ` ${t("backup.restore")}` })),
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche klein gefahr flach",
|
||||
title: "Sicherungsdatei unwiderruflich löschen",
|
||||
title: t("backup.deleteTooltip"),
|
||||
onclick: () => oeffneLoeschenDialog(b.fileName)
|
||||
}, erstelleIcon(PFADE.muelleimer, 14))
|
||||
);
|
||||
@@ -163,7 +162,7 @@ export async function rendereSicherung() {
|
||||
erstelleElement("td", { text: formatiereDatumZeit(b.createdAt) }),
|
||||
erstelleElement("td", { text: formatiereBytes(b.sizeBytes) }),
|
||||
erstelleElement("td", {}, schutzPlakette(b)),
|
||||
erstelleElement("td", { text: b.fileCount > 0 ? `${b.fileCount} Dateien` : "—" }),
|
||||
erstelleElement("td", { text: b.fileCount > 0 ? t("backup.fileCount", { count: b.fileCount }) : "—" }),
|
||||
aktionen
|
||||
));
|
||||
}
|
||||
@@ -171,15 +170,15 @@ export async function rendereSicherung() {
|
||||
const tabelle = erstelleElement("table", { klasse: "tabelle sicherungen-tabelle" },
|
||||
erstelleElement("thead", {},
|
||||
erstelleElement("tr", {},
|
||||
erstelleElement("th", { text: "Archivdatei" }),
|
||||
erstelleElement("th", { text: "Erstellt am" }),
|
||||
erstelleElement("th", { text: "Größe" }),
|
||||
erstelleElement("th", { text: "Schutz" }),
|
||||
erstelleElement("th", { text: "Dateien" }),
|
||||
erstelleElement("th", { klasse: "spalte-aktionen", text: "Aktionen" }))),
|
||||
erstelleElement("th", { text: t("backup.archiveFile") }),
|
||||
erstelleElement("th", { text: t("backup.createdAt") }),
|
||||
erstelleElement("th", { text: t("backup.size") }),
|
||||
erstelleElement("th", { text: t("backup.protection") }),
|
||||
erstelleElement("th", { text: t("backup.files") }),
|
||||
erstelleElement("th", { klasse: "spalte-aktionen", text: t("backup.actions") }))),
|
||||
tbody);
|
||||
|
||||
inhaltBereich.replaceChildren(erstelleKarte(`Vorhandene Sicherungen (${backups.length})`, [tabelle]));
|
||||
inhaltBereich.replaceChildren(erstelleKarte(t("backup.existingBackups", { count: backups.length }), [tabelle]));
|
||||
} catch (err) {
|
||||
inhaltBereich.replaceChildren(
|
||||
erstelleKarte("Fehler", [
|
||||
@@ -192,14 +191,13 @@ export async function rendereSicherung() {
|
||||
// ─── Neue Sicherung ───
|
||||
function oeffneSicherungsDialog() {
|
||||
const form = erstelleElement("form", { klasse: "formular" });
|
||||
const chats = checkbox("backup-chk-chats", "Chatverläufe sichern", true);
|
||||
const logs = checkbox("backup-chk-logs", "Protokolldateien (Logs) einschließen", false);
|
||||
const chats = checkbox("backup-chk-chats", t("backup.includeChats"), true);
|
||||
const logs = checkbox("backup-chk-logs", t("backup.includeLogs"), false);
|
||||
|
||||
form.append(
|
||||
erstelleElement("p", {
|
||||
klasse: "form-hilfe",
|
||||
text: "Die Sicherung wird mit dem Lizenzschlüssel des Instanzmanagers verschlüsselt und "
|
||||
+ "enthält die Zugangsdaten. Ist ein FTP-Upload eingerichtet, wird sie anschließend hochgeladen."
|
||||
text: t("backup.dialogHint")
|
||||
}),
|
||||
chats.gruppe,
|
||||
logs.gruppe
|
||||
@@ -207,13 +205,13 @@ export async function rendereSicherung() {
|
||||
|
||||
let modal;
|
||||
const speichernBtn = erstelleElement("button", {
|
||||
type: "submit", klasse: "schaltflaeche primaer", text: "Sicherung erstellen"
|
||||
type: "submit", klasse: "schaltflaeche primaer", text: t("backup.createButton")
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
speichernBtn.disabled = true;
|
||||
speichernBtn.textContent = "Erstelle Sicherung…";
|
||||
speichernBtn.textContent = t("backup.creating");
|
||||
|
||||
try {
|
||||
const res = await api(`/verwaltung/instanzen/${pfadInstanz}/backups`, {
|
||||
@@ -222,19 +220,19 @@ export async function rendereSicherung() {
|
||||
});
|
||||
// Ein gescheiterter Upload steht in der Meldung; die Sicherung liegt trotzdem lokal.
|
||||
const warnt = (res.meldung || "").includes("gescheitert");
|
||||
zeigeMeldung(res.meldung || "Sicherung erstellt.", warnt ? "warnung" : "erfolg", warnt ? 8000 : undefined);
|
||||
zeigeMeldung(res.meldung || t("backup.created"), warnt ? "warnung" : "erfolg", warnt ? 8000 : undefined);
|
||||
modal.schliessen();
|
||||
ladeSicherungen();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fehler beim Sichern: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("backup.createFailed", { error: err.message }), "gefahr");
|
||||
speichernBtn.disabled = false;
|
||||
speichernBtn.textContent = "Sicherung erstellen";
|
||||
speichernBtn.textContent = t("backup.createButton");
|
||||
}
|
||||
});
|
||||
|
||||
modal = erstelleModal("Neue Sicherung erstellen", form, [
|
||||
modal = erstelleModal(t("backup.dialogTitle"), form, [
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "schaltflaeche", text: "Abbrechen", onclick: () => modal.schliessen()
|
||||
type: "button", klasse: "schaltflaeche", text: t("common.cancel"), onclick: () => modal.schliessen()
|
||||
}),
|
||||
speichernBtn
|
||||
]);
|
||||
@@ -255,26 +253,26 @@ export async function rendereSicherung() {
|
||||
form.append(erstelleElement("div", { klasse: "hinweis-box warnung" },
|
||||
erstelleIcon(PFADE.ausrufezeichen, 18),
|
||||
erstelleElement("div", {},
|
||||
erstelleElement("strong", { text: "Achtung: Überschreiben bestehender Daten" }),
|
||||
erstelleElement("strong", { text: t("backup.overwriteWarningTitle") }),
|
||||
erstelleElement("p", {
|
||||
text: `Die Wiederherstellung überschreibt die aktuellen Konfigurationen und Daten der Instanz mit dem Stand vom ${formatiereDatumZeit(backup.createdAt)}.`
|
||||
text: t("backup.overwriteWarningText", { date: formatiereDatumZeit(backup.createdAt) })
|
||||
}),
|
||||
erstelleElement("p", {
|
||||
text: "Die Instanz wird dafür geordnet angehalten und danach wieder gestartet."
|
||||
text: t("backup.instanceStopNotice")
|
||||
}))));
|
||||
|
||||
// Nur ältere, unverschlüsselte Sicherungen mit Passphrase brauchen sie noch.
|
||||
let passInput = null;
|
||||
if (backup.secrets === "Passphrase" && !backup.verschluesselt) {
|
||||
passInput = erstelleElement("input", {
|
||||
type: "password", klasse: "eingabe", placeholder: "Passphrase der Sicherung…", required: true
|
||||
type: "password", klasse: "eingabe", placeholder: t("backup.passphrasePlaceholder"), required: true
|
||||
});
|
||||
form.append(feld("Passphrase für Zugangsdaten", passInput));
|
||||
form.append(feld(t("backup.passphraseLabel"), passInput));
|
||||
}
|
||||
|
||||
let modal;
|
||||
const restoreBtn = erstelleElement("button", {
|
||||
type: "submit", klasse: "schaltflaeche warnung", text: "Jetzt wiederherstellen"
|
||||
type: "submit", klasse: "schaltflaeche warnung", text: t("backup.restoreNow")
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
@@ -282,36 +280,36 @@ export async function rendereSicherung() {
|
||||
const passphrase = passInput?.value?.trim() || null;
|
||||
|
||||
if (passInput && !passphrase) {
|
||||
zeigeMeldung("Bitte gib die Passphrase an.", "warnung");
|
||||
zeigeMeldung(t("backup.passphraseRequired"), "warnung");
|
||||
passInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
restoreBtn.disabled = true;
|
||||
restoreBtn.textContent = "Stelle wieder her…";
|
||||
restoreBtn.textContent = t("backup.restoring");
|
||||
|
||||
try {
|
||||
const res = await stelleWiederHer(backup, { passphrase });
|
||||
zeigeMeldung(res.meldung || "Wiederherstellung abgeschlossen.", "erfolg");
|
||||
zeigeMeldung(res.meldung || t("backup.restoreSuccess"), "erfolg");
|
||||
modal.schliessen();
|
||||
ladeSicherungen();
|
||||
} catch (err) {
|
||||
// Our own key does not match — the backup comes from another installation.
|
||||
// Nothing was stopped for this; the server checks that beforehand.
|
||||
if (err.code === "fremde_lizenz") {
|
||||
// Der eigene Schlüssel passt nicht - die Sicherung stammt von einer anderen
|
||||
// Installation. Angehalten wurde dafür nichts; das prüft der Server vorher.
|
||||
modal.schliessen();
|
||||
frageLizenzschluessel(backup, err.message);
|
||||
return;
|
||||
}
|
||||
zeigeMeldung(`Wiederherstellung fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("backup.restoreFailed", { error: err.message }), "gefahr");
|
||||
restoreBtn.disabled = false;
|
||||
restoreBtn.textContent = "Jetzt wiederherstellen";
|
||||
restoreBtn.textContent = t("backup.restoreNow");
|
||||
}
|
||||
});
|
||||
|
||||
modal = erstelleModal(`Sicherung wiederherstellen: ${backup.fileName}`, form, [
|
||||
modal = erstelleModal(t("backup.restoreTitle", { file: backup.fileName }), form, [
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "schaltflaeche", text: "Abbrechen", onclick: () => modal.schliessen()
|
||||
type: "button", klasse: "schaltflaeche", text: t("common.cancel"), onclick: () => modal.schliessen()
|
||||
}),
|
||||
restoreBtn
|
||||
]);
|
||||
@@ -327,20 +325,19 @@ export async function rendereSicherung() {
|
||||
const eingabe = erstelleElement("input", {
|
||||
type: "password",
|
||||
klasse: "eingabe",
|
||||
placeholder: "Lizenzschlüssel der Installation, von der die Sicherung stammt…",
|
||||
placeholder: t("backup.sourceLicensePlaceholder"),
|
||||
autocomplete: "off",
|
||||
required: true
|
||||
});
|
||||
|
||||
form.append(
|
||||
erstelleElement("p", { text: grund }),
|
||||
feld("Lizenzschlüssel der Quelle", eingabe,
|
||||
"Groß- und Kleinschreibung sowie Leerzeichen spielen keine Rolle.")
|
||||
feld(t("backup.sourceLicenseLabel"), eingabe, t("backup.sourceLicenseHint"))
|
||||
);
|
||||
|
||||
let modal;
|
||||
const okBtn = erstelleElement("button", {
|
||||
type: "submit", klasse: "schaltflaeche warnung", text: "Mit diesem Schlüssel wiederherstellen"
|
||||
type: "submit", klasse: "schaltflaeche warnung", text: t("backup.restoreWithKey")
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
@@ -349,26 +346,26 @@ export async function rendereSicherung() {
|
||||
if (!lizenzschluessel) { eingabe.focus(); return; }
|
||||
|
||||
okBtn.disabled = true;
|
||||
okBtn.textContent = "Stelle wieder her…";
|
||||
okBtn.textContent = t("backup.restoring");
|
||||
|
||||
try {
|
||||
const res = await stelleWiederHer(backup, { lizenzschluessel });
|
||||
zeigeMeldung(res.meldung || "Wiederherstellung abgeschlossen.", "erfolg");
|
||||
zeigeMeldung(res.meldung || t("backup.restoreSuccess"), "erfolg");
|
||||
modal.schliessen();
|
||||
ladeSicherungen();
|
||||
} catch (err) {
|
||||
zeigeMeldung(err.code === "fremde_lizenz"
|
||||
? "Dieser Lizenzschlüssel passt nicht zu der Sicherung."
|
||||
: `Wiederherstellung fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
? t("backup.keyMismatch")
|
||||
: t("backup.restoreFailed", { error: err.message }), "gefahr");
|
||||
okBtn.disabled = false;
|
||||
okBtn.textContent = "Mit diesem Schlüssel wiederherstellen";
|
||||
okBtn.textContent = t("backup.restoreWithKey");
|
||||
eingabe.select();
|
||||
}
|
||||
});
|
||||
|
||||
modal = erstelleModal("Lizenzschlüssel der Quelle erforderlich", form, [
|
||||
modal = erstelleModal(t("backup.foreignLicenseTitle"), form, [
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "schaltflaeche", text: "Abbrechen", onclick: () => modal.schliessen()
|
||||
type: "button", klasse: "schaltflaeche", text: t("common.cancel"), onclick: () => modal.schliessen()
|
||||
}),
|
||||
okBtn
|
||||
]);
|
||||
@@ -382,28 +379,28 @@ export async function rendereSicherung() {
|
||||
const loeschenBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche gefahr",
|
||||
text: "Löschen",
|
||||
text: t("common.delete"),
|
||||
onclick: async () => {
|
||||
loeschenBtn.disabled = true;
|
||||
try {
|
||||
const res = await api(`/verwaltung/instanzen/${pfadInstanz}/backups/${encodeURIComponent(fileName)}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
zeigeMeldung(res.meldung || "Sicherung gelöscht.", "erfolg");
|
||||
zeigeMeldung(res.meldung || t("backup.deleted"), "erfolg");
|
||||
modal.schliessen();
|
||||
ladeSicherungen();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Löschen fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("backup.deleteFailed", { error: err.message }), "gefahr");
|
||||
loeschenBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
modal = erstelleModal("Sicherung löschen",
|
||||
erstelleElement("p", { text: `Soll die Sicherungsdatei '${fileName}' wirklich unwiderruflich gelöscht werden?` }),
|
||||
modal = erstelleModal(t("backup.deleteTitle"),
|
||||
erstelleElement("p", { text: t("backup.deleteConfirm", { file: fileName }) }),
|
||||
[
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "schaltflaeche", text: "Abbrechen", onclick: () => modal.schliessen()
|
||||
type: "button", klasse: "schaltflaeche", text: t("common.cancel"), onclick: () => modal.schliessen()
|
||||
}),
|
||||
loeschenBtn
|
||||
]);
|
||||
@@ -416,13 +413,13 @@ export async function rendereSicherung() {
|
||||
// ─── FTP-Upload (für alle Instanzen) ───
|
||||
|
||||
async function rendereFtp(bereich) {
|
||||
bereich.replaceChildren(erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade FTP-Einstellungen…" }));
|
||||
bereich.replaceChildren(erstelleElement("div", { klasse: "hinweis-zentriert", text: t("backup.ftpLoading") }));
|
||||
|
||||
let stand;
|
||||
try {
|
||||
stand = await api("/verwaltung/sicherung/ftp");
|
||||
} catch (err) {
|
||||
bereich.replaceChildren(erstelleKarte("FTP-Upload", [
|
||||
bereich.replaceChildren(erstelleKarte(t("backup.ftpTitle"), [
|
||||
erstelleElement("p", { klasse: "fehler-meldung", text: `Einstellungen nicht ladbar: ${err.message}` })
|
||||
]));
|
||||
return;
|
||||
@@ -430,7 +427,7 @@ async function rendereFtp(bereich) {
|
||||
|
||||
const form = erstelleElement("form", { klasse: "formular" });
|
||||
|
||||
const an = checkbox("ftp-an", "Fertige Sicherungen automatisch hochladen", stand.enabled);
|
||||
const an = checkbox("ftp-an", t("backup.ftpAutoUpload"), stand.enabled);
|
||||
const host = erstelleElement("input", { type: "text", klasse: "eingabe", value: stand.host ?? "", placeholder: "ftp.example.org" });
|
||||
const port = erstelleElement("input", { type: "number", klasse: "eingabe", value: String(stand.port ?? 21), min: "1", max: "65535" });
|
||||
const benutzer = erstelleElement("input", { type: "text", klasse: "eingabe", value: stand.username ?? "", autocomplete: "off" });
|
||||
@@ -438,40 +435,39 @@ async function rendereFtp(bereich) {
|
||||
type: "password",
|
||||
klasse: "eingabe",
|
||||
autocomplete: "new-password",
|
||||
placeholder: stand.kennwortGesetzt ? "(gespeichert — leer lassen für unverändert)" : "Kennwort"
|
||||
placeholder: stand.kennwortGesetzt ? t("backup.ftpPasswordSaved") : t("backup.ftpPassword")
|
||||
});
|
||||
const ordner = erstelleElement("input", { type: "text", klasse: "eingabe", value: stand.remoteDirectory ?? "/clawd-backups" });
|
||||
const behalten = erstelleElement("input", { type: "number", klasse: "eingabe", value: String(stand.keepCount ?? 14), min: "1", max: "365" });
|
||||
const tls = checkbox("ftp-tls", "Nur mit TLS verbinden", stand.requireTls);
|
||||
const tls = checkbox("ftp-tls", t("backup.ftpRequireTls"), stand.requireTls);
|
||||
|
||||
form.append(
|
||||
erstelleElement("p", {
|
||||
klasse: "form-hilfe",
|
||||
text: "Die Sicherungen sind verschlüsselt — wer den Server liest, liest nichts Brauchbares. "
|
||||
+ "Das Kennwort des Servers geht ohne TLS aber im Klartext über die Leitung."
|
||||
text: t("backup.ftpHint")
|
||||
}),
|
||||
an.gruppe,
|
||||
feld("Server", host),
|
||||
feld("Port", port),
|
||||
feld("Benutzer", benutzer),
|
||||
feld("Kennwort", kennwort),
|
||||
feld("Zielordner", ordner, "Darunter bekommt jede Instanz einen eigenen Ordner."),
|
||||
feld("Sicherungen je Instanz behalten", behalten, "Ältere werden auf dem Server gelöscht."),
|
||||
feld(t("backup.ftpServer"), host),
|
||||
feld(t("backup.ftpPort"), port),
|
||||
feld(t("backup.ftpUser"), benutzer),
|
||||
feld(t("backup.ftpPassword"), kennwort),
|
||||
feld(t("backup.ftpDirectory"), ordner, t("backup.ftpDirectoryHint")),
|
||||
feld(t("backup.ftpKeepCount"), behalten, t("backup.ftpKeepCountHint")),
|
||||
tls.gruppe
|
||||
);
|
||||
|
||||
const speichernBtn = erstelleElement("button", { type: "submit", klasse: "schaltflaeche primaer", text: "Speichern" });
|
||||
const speichernBtn = erstelleElement("button", { type: "submit", klasse: "schaltflaeche primaer", text: t("common.save") });
|
||||
const testBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche",
|
||||
text: "Verbindung testen",
|
||||
text: t("backup.ftpTest"),
|
||||
onclick: async () => {
|
||||
testBtn.disabled = true;
|
||||
try {
|
||||
const res = await api("/verwaltung/sicherung/ftp/test", { method: "POST" });
|
||||
zeigeMeldung(res.meldung, res.ok ? "erfolg" : "warnung", 8000);
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Test fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("backup.ftpTestFailed", { error: err.message }), "gefahr");
|
||||
} finally {
|
||||
testBtn.disabled = false;
|
||||
}
|
||||
@@ -495,11 +491,11 @@ async function rendereFtp(bereich) {
|
||||
keepCount: Number(behalten.value) || 14
|
||||
}
|
||||
});
|
||||
zeigeMeldung(res.meldung || "Gespeichert.", "erfolg");
|
||||
zeigeMeldung(res.meldung || t("backup.ftpSaved"), "erfolg");
|
||||
kennwort.value = "";
|
||||
rendereFtp(bereich);
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Speichern fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("backup.ftpSaveFailed", { error: err.message }), "gefahr");
|
||||
} finally {
|
||||
speichernBtn.disabled = false;
|
||||
}
|
||||
@@ -507,5 +503,5 @@ async function rendereFtp(bereich) {
|
||||
|
||||
form.append(erstelleElement("div", { klasse: "gruppe-horizontal" }, testBtn, speichernBtn));
|
||||
|
||||
bereich.replaceChildren(erstelleKarte("FTP-Upload der Sicherungen", [form]));
|
||||
bereich.replaceChildren(erstelleKarte(t("backup.ftpTitle"), [form]));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { api, holeAktiveInstanz } from "../modules/api.js";
|
||||
import { abonnieren } from "../modules/events.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleIcon,
|
||||
@@ -55,8 +56,8 @@ export async function rendereChat(parameter = {}) {
|
||||
onchange: (e) => waehleAgent(e.target.value)
|
||||
});
|
||||
|
||||
statusPlakette = erstellePlakette("Lädt…", "neutral");
|
||||
agentenWahl.append(erstelleElement("label", { text: "Agent: " }), agentenSelector, statusPlakette);
|
||||
statusPlakette = erstellePlakette(t("common.loading"), "neutral");
|
||||
agentenWahl.append(erstelleElement("label", { text: t("chat.agentLabel") }), agentenSelector, statusPlakette);
|
||||
|
||||
const aktionen = erstelleElement("div", { klasse: "chat-aktionen" });
|
||||
|
||||
@@ -64,34 +65,34 @@ export async function rendereChat(parameter = {}) {
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Agenten manuell ausführen / wecken",
|
||||
title: t("chat.wakeTooltip"),
|
||||
onclick: weckeAktivenAgent
|
||||
},
|
||||
erstelleIcon(PFADE.abspielen, 14),
|
||||
erstelleElement("span", { text: " Wecken" })
|
||||
erstelleElement("span", { text: " " + t("chat.wake") })
|
||||
);
|
||||
|
||||
const leerenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Chatverlauf dieses Agenten leeren",
|
||||
title: t("chat.clearHistoryTooltip"),
|
||||
onclick: leereChatverlauf
|
||||
},
|
||||
erstelleIcon(PFADE.leeren, 14),
|
||||
erstelleElement("span", { text: " Verlauf leeren" })
|
||||
erstelleElement("span", { text: " " + t("chat.clearHistory") })
|
||||
);
|
||||
|
||||
abbrechenButton = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche gefahr flach",
|
||||
title: "Laufenden Lauf abbrechen",
|
||||
title: t("chat.cancelTooltip"),
|
||||
hidden: true,
|
||||
onclick: brecheChatAb
|
||||
},
|
||||
erstelleIcon(PFADE.abbrechen, 14),
|
||||
erstelleElement("span", { text: " Abbrechen" })
|
||||
erstelleElement("span", { text: " " + t("chat.cancel") })
|
||||
);
|
||||
|
||||
aktionen.append(weckenBtn, leerenBtn, abbrechenButton);
|
||||
@@ -110,7 +111,7 @@ export async function rendereChat(parameter = {}) {
|
||||
|
||||
eingabeTextarea = erstelleElement("textarea", {
|
||||
klasse: "chat-eingabe",
|
||||
placeholder: "Nachricht an den Agenten eingeben (Enter zum Senden, Umschalt+Enter für neue Zeile)…",
|
||||
placeholder: t("chat.placeholder"),
|
||||
rows: "3",
|
||||
onkeydown: (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
@@ -124,11 +125,11 @@ export async function rendereChat(parameter = {}) {
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche primaer",
|
||||
title: "Nachricht senden",
|
||||
title: t("chat.sendTooltip"),
|
||||
onclick: sendeNachricht
|
||||
},
|
||||
erstelleIcon(PFADE.senden, 16),
|
||||
erstelleElement("span", { text: "Senden" })
|
||||
erstelleElement("span", { text: t("chat.send") })
|
||||
);
|
||||
|
||||
eingabeBereich.append(eingabeTextarea, sendenButton);
|
||||
@@ -190,9 +191,9 @@ async function ladeAgenten(gewuenschteId = null) {
|
||||
agentenSelector.replaceChildren();
|
||||
|
||||
if (!agentenListe || agentenListe.length === 0) {
|
||||
const opt = erstelleElement("option", { text: "Keine Agenten vorhanden", value: "" });
|
||||
const opt = erstelleElement("option", { text: t("chat.noAgents"), value: "" });
|
||||
agentenSelector.append(opt);
|
||||
statusPlakette.replaceWith(erstellePlakette("Keine Agenten", "warnung"));
|
||||
statusPlakette.replaceWith(erstellePlakette(t("chat.noAgents"), "warnung"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -211,7 +212,7 @@ async function ladeAgenten(gewuenschteId = null) {
|
||||
agentenSelector.value = zielId;
|
||||
await waehleAgent(zielId);
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fehler beim Laden der Agenten: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(`Fehler: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,8 +234,8 @@ async function waehleAgent(agentId) {
|
||||
function aktualisiereLaufStatus(laeuft) {
|
||||
if (statusPlakette) {
|
||||
const neuePlakette = laeuft
|
||||
? erstellePlakette("Beschäftigt", "warnung")
|
||||
: erstellePlakette("Bereit", "erfolg");
|
||||
? erstellePlakette(t("status.busy"), "warnung")
|
||||
: erstellePlakette(t("status.ready"), "erfolg");
|
||||
statusPlakette.replaceWith(neuePlakette);
|
||||
statusPlakette = neuePlakette;
|
||||
}
|
||||
@@ -247,7 +248,7 @@ function aktualisiereLaufStatus(laeuft) {
|
||||
async function ladeChatVerlauf(agentId) {
|
||||
if (!nachrichtenContainer) return;
|
||||
nachrichtenContainer.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Verlauf…" })
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("chat.loadingHistory") })
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -258,7 +259,7 @@ async function ladeChatVerlauf(agentId) {
|
||||
nachrichtenContainer.append(
|
||||
erstelleElement("div", {
|
||||
klasse: "hinweis-zentriert",
|
||||
text: "Noch keine Nachrichten im Verlauf vorhanden."
|
||||
text: t("chat.emptyHistory")
|
||||
})
|
||||
);
|
||||
return;
|
||||
@@ -273,7 +274,7 @@ async function ladeChatVerlauf(agentId) {
|
||||
nachrichtenContainer.replaceChildren(
|
||||
erstelleElement("div", {
|
||||
klasse: "fehler-zentriert",
|
||||
text: `Verlauf konnte nicht geladen werden: ${err.message}`
|
||||
text: t("chat.historyLoadError", { error: err.message })
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -290,14 +291,14 @@ function fuegeNachrichtEin(eintrag) {
|
||||
const istTool = rolle === "tool";
|
||||
|
||||
let rollenKlasse = "nachricht-benutzer";
|
||||
let rollenTitel = "Benutzer";
|
||||
let rollenTitel = t("chat.role.user");
|
||||
|
||||
if (istSystem) {
|
||||
rollenKlasse = "nachricht-system";
|
||||
rollenTitel = "System";
|
||||
rollenTitel = t("chat.role.system");
|
||||
} else if (istTool) {
|
||||
rollenKlasse = "nachricht-werkzeug";
|
||||
rollenTitel = "Werkzeug";
|
||||
rollenTitel = t("chat.role.tool");
|
||||
} else if (!istUser) {
|
||||
rollenKlasse = "nachricht-agent";
|
||||
rollenTitel = agentenListe.find((a) => a.agentId === aktiverAgentId)?.displayName || "Agent";
|
||||
@@ -370,7 +371,7 @@ async function sendeNachricht() {
|
||||
body: { message: nachricht }
|
||||
});
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Nachricht konnte nicht gesendet werden: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("chat.sendError", { error: err.message }), "gefahr");
|
||||
aktualisiereLaufStatus(false);
|
||||
} finally {
|
||||
sendenButton.disabled = false;
|
||||
@@ -386,9 +387,9 @@ async function brecheChatAb() {
|
||||
});
|
||||
versteckeSchritt();
|
||||
aktualisiereLaufStatus(false);
|
||||
zeigeMeldung("Lauf wurde abgebrochen.", "warnung");
|
||||
zeigeMeldung(t("chat.cancelConfirm"), "warnung");
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Abbruch fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("chat.cancelError", { error: err.message }), "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,15 +400,15 @@ async function weckeAktivenAgent() {
|
||||
method: "POST"
|
||||
});
|
||||
aktualisiereLaufStatus(true);
|
||||
zeigeMeldung("Agent wurde manuell gestartet.", "info");
|
||||
zeigeMeldung(t("chat.wakeSuccess"), "info");
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Starten fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("chat.wakeError", { error: err.message }), "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
async function leereChatverlauf() {
|
||||
if (!aktiverAgentId) return;
|
||||
if (!confirm("Soll der gesamte Chatverlauf dieses Agenten unwiderruflich gelöscht werden?")) return;
|
||||
if (!confirm(t("chat.clearConfirmPrompt"))) return;
|
||||
|
||||
try {
|
||||
await api(`/agents/${encodeURIComponent(aktiverAgentId)}/chat`, {
|
||||
@@ -416,11 +417,11 @@ async function leereChatverlauf() {
|
||||
nachrichtenContainer.replaceChildren(
|
||||
erstelleElement("div", {
|
||||
klasse: "hinweis-zentriert",
|
||||
text: "Verlauf wurde geleert."
|
||||
text: t("chat.cleared")
|
||||
})
|
||||
);
|
||||
zeigeMeldung("Chatverlauf geleert.", "info");
|
||||
zeigeMeldung(t("chat.cleared"), "info");
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Leeren fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("chat.clearError", { error: err.message }), "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// ClawdDotNet — System-Info & Status
|
||||
//
|
||||
// Zeigt Anwendungsversion, Instanzkennung, Teilsysteme und OpenRouter-Guthaben an.
|
||||
// Shows application version, instance ID, subsystems, and OpenRouter credits.
|
||||
|
||||
import { api, holeAktiveInstanz } from "../modules/api.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleKarte,
|
||||
@@ -15,7 +16,7 @@ export async function rendereInfo() {
|
||||
container.replaceChildren();
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "info-ansicht" });
|
||||
layout.append(erstelleElement("h1", { text: "System & Status" }));
|
||||
layout.append(erstelleElement("h1", { text: t("info.title") }));
|
||||
|
||||
const gitter = erstelleElement("div", { klasse: "info-gitter" });
|
||||
layout.append(gitter);
|
||||
@@ -23,10 +24,10 @@ export async function rendereInfo() {
|
||||
|
||||
if (!holeAktiveInstanz()) {
|
||||
gitter.append(
|
||||
erstelleKarte("Keine Instanz ausgewählt", [
|
||||
erstelleKarte(t("info.noInstanceSelected"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Wähle oben im Kopfbereich eine aktive Instanz aus oder wechsle in den Bereich 'Instanzen', um eine Instanz zu starten."
|
||||
text: t("info.noInstanceHint")
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -34,7 +35,7 @@ export async function rendereInfo() {
|
||||
}
|
||||
|
||||
gitter.append(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Systemdaten…" })
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("info.loading") })
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -49,50 +50,50 @@ export async function rendereInfo() {
|
||||
if (info) {
|
||||
const dlInstanz = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlInstanz.append(
|
||||
erstelleElement("dt", { text: "Instanzname" }),
|
||||
erstelleElement("dt", { text: t("info.instanceName") }),
|
||||
erstelleElement("dd", {}, erstelleElement("strong", { text: info.instanceName })),
|
||||
erstelleElement("dt", { text: "Instanz-ID" }),
|
||||
erstelleElement("dt", { text: t("info.instanceId") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: info.instanceId })),
|
||||
erstelleElement("dt", { text: "Version" }),
|
||||
erstelleElement("dt", { text: t("info.version") }),
|
||||
erstelleElement("dd", { text: info.appVersion }),
|
||||
erstelleElement("dt", { text: "Build" }),
|
||||
erstelleElement("dt", { text: t("info.build") }),
|
||||
erstelleElement("dd", { text: info.buildSummary })
|
||||
);
|
||||
gitter.append(erstelleKarte("Instanz & Programm", [dlInstanz]));
|
||||
gitter.append(erstelleKarte(t("info.instanceAndVersion"), [dlInstanz]));
|
||||
|
||||
// 2. Teilsysteme
|
||||
const dlSysteme = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlSysteme.append(
|
||||
erstelleElement("dt", { text: "Agent Engine" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(info.engineActive ? "Aktiv" : "Aus", info.engineActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dd", {}, erstellePlakette(info.engineActive ? (t("status.running")) : (t("status.stopped")), info.engineActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: "Scanner / Scheduler" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(info.schedulerActive ? "Aktiv" : "Aus", info.schedulerActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: "Freigabestelle (Staging)" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(info.stagingActive ? "Aktiv" : "Aus", info.stagingActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dd", {}, erstellePlakette(info.schedulerActive ? (t("status.running")) : (t("status.stopped")), info.schedulerActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: t("nav.staging") }),
|
||||
erstelleElement("dd", {}, erstellePlakette(info.stagingActive ? (t("status.running")) : (t("status.stopped")), info.stagingActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: "Deploymentcenter" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(info.deploymentcenterActive ? "Verbunden" : "Nicht konfiguriert", info.deploymentcenterActive ? "erfolg" : "neutral"))
|
||||
erstelleElement("dd", {}, erstellePlakette(info.deploymentcenterActive ? "OK" : "—", info.deploymentcenterActive ? "erfolg" : "neutral"))
|
||||
);
|
||||
gitter.append(erstelleKarte("Dienste & Teilsysteme", [dlSysteme]));
|
||||
gitter.append(erstelleKarte(t("info.subsystems"), [dlSysteme]));
|
||||
}
|
||||
|
||||
// 3. OpenRouter / API-Status
|
||||
const dlStatus = erstelleElement("dl", { klasse: "datenliste" });
|
||||
if (status) {
|
||||
dlStatus.append(
|
||||
erstelleElement("dt", { text: "API-Erreichbarkeit" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(status.isApiReachable ? "Verbunden" : "Getrennt", status.isApiReachable ? "erfolg" : "gefahr")),
|
||||
erstelleElement("dt", { text: "Statusmeldung" }),
|
||||
erstelleElement("dt", { text: "API" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(status.isApiReachable ? "OK" : "Offline", status.isApiReachable ? "erfolg" : "gefahr")),
|
||||
erstelleElement("dt", { text: t("common.status") }),
|
||||
erstelleElement("dd", { text: status.statusText || "—" }),
|
||||
erstelleElement("dt", { text: "Guthaben / Credits" }),
|
||||
erstelleElement("dt", { text: "Credits" }),
|
||||
erstelleElement("dd", {}, erstelleElement("strong", { text: status.creditsText || "—" }))
|
||||
);
|
||||
} else {
|
||||
dlStatus.append(
|
||||
erstelleElement("dt", { text: "OpenRouter" }),
|
||||
erstelleElement("dd", { klasse: "gedaempft", text: "Kein API-Schlüssel oder Dienst nicht erreichbar." })
|
||||
erstelleElement("dd", { klasse: "gedaempft", text: "—" })
|
||||
);
|
||||
}
|
||||
gitter.append(erstelleKarte("Modellanbindung (OpenRouter)", [dlStatus]));
|
||||
gitter.append(erstelleKarte(t("info.openRouterStatus"), [dlStatus]));
|
||||
|
||||
} catch (err) {
|
||||
gitter.replaceChildren(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { setzeToken, loescheToken, api } from "../modules/api.js";
|
||||
import { erstelleElement, PFADE, erstelleIcon } from "../modules/dom.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
|
||||
let loginCallback = null;
|
||||
|
||||
@@ -37,8 +38,8 @@ export function initialisiereLogin(onErfolg) {
|
||||
loescheToken();
|
||||
if (fehlerBox) {
|
||||
fehlerBox.textContent = err.abgelehnt
|
||||
? "Token ungültig oder kürzer als 20 Zeichen."
|
||||
: `Anmeldefehler: ${err.message}`;
|
||||
? t("login.invalidToken")
|
||||
: t("login.error", { error: err.message });
|
||||
fehlerBox.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// ClawdDotNet — Live-Protokollansicht (Logs)
|
||||
//
|
||||
// Fortlaufender Stream über /events/logs, Modul- und Level-Filterung, Autoscroll, strikt textContent.
|
||||
// Continuous stream via /events/logs, module and level filtering, autoscroll.
|
||||
|
||||
import { api, holeToken } from "../modules/api.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleIcon,
|
||||
@@ -33,8 +34,8 @@ export async function rendereProtokoll() {
|
||||
// Kopfleiste mit Filtern und Steuerungen
|
||||
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
const titelGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
titelGruppe.append(erstelleElement("h1", { text: "Protokoll" }));
|
||||
streamStatusPlakette = erstellePlakette("Verbinde…", "neutral");
|
||||
titelGruppe.append(erstelleElement("h1", { text: t("logs.title") }));
|
||||
streamStatusPlakette = erstellePlakette(t("logs.connecting"), "neutral");
|
||||
titelGruppe.append(streamStatusPlakette);
|
||||
|
||||
const steuerungGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
@@ -42,12 +43,12 @@ export async function rendereProtokoll() {
|
||||
// Instanz-Auswahl
|
||||
const instanzSelect = erstelleElement("select", {
|
||||
klasse: "eingabe-select",
|
||||
title: "Nach Instanz filtern",
|
||||
title: t("logs.allInstances"),
|
||||
onchange: (e) => {
|
||||
instanzFilter = e.target.value;
|
||||
}
|
||||
});
|
||||
instanzSelect.append(erstelleElement("option", { value: "", text: "Alle Instanzen" }));
|
||||
instanzSelect.append(erstelleElement("option", { value: "", text: t("logs.allInstances") }));
|
||||
|
||||
// Modul-Auswahl
|
||||
const modulSelect = erstelleElement("select", {
|
||||
@@ -57,7 +58,7 @@ export async function rendereProtokoll() {
|
||||
starteLogStream();
|
||||
}
|
||||
});
|
||||
modulSelect.append(erstelleElement("option", { value: "", text: "Alle Module" }));
|
||||
modulSelect.append(erstelleElement("option", { value: "", text: t("logs.allModules") }));
|
||||
|
||||
// Level-Auswahl
|
||||
const levelSelect = erstelleElement("select", {
|
||||
@@ -68,12 +69,12 @@ export async function rendereProtokoll() {
|
||||
}
|
||||
});
|
||||
const levelOptionen = [
|
||||
["All", "Alle Stufen"],
|
||||
["All", t("common.all")],
|
||||
["Debug", "Debug"],
|
||||
["Information", "Info"],
|
||||
["Warning", "Warnung"],
|
||||
["Error", "Fehler"],
|
||||
["Critical", "Kritisch"]
|
||||
["Warning", "Warning"],
|
||||
["Error", "Error"],
|
||||
["Critical", "Critical"]
|
||||
];
|
||||
for (const [val, label] of levelOptionen) {
|
||||
levelSelect.append(erstelleElement("option", { value: val, text: label }));
|
||||
@@ -88,7 +89,7 @@ export async function rendereProtokoll() {
|
||||
autoScroll = e.target.checked;
|
||||
}
|
||||
});
|
||||
autoScrollLabel.append(autoScrollInput, erstelleElement("span", { text: " Auto-Scroll" }));
|
||||
autoScrollLabel.append(autoScrollInput, erstelleElement("span", { text: " " + t("logs.autoScroll") }));
|
||||
|
||||
// Pause / Fortsetzen
|
||||
pauseButton = erstelleElement(
|
||||
@@ -98,7 +99,7 @@ export async function rendereProtokoll() {
|
||||
onclick: togglePause
|
||||
},
|
||||
erstelleIcon(PFADE.abbrechen, 14),
|
||||
erstelleElement("span", { text: " Anhalten" })
|
||||
erstelleElement("span", { text: " " + t("logs.pause") })
|
||||
);
|
||||
|
||||
// Leeren
|
||||
@@ -106,11 +107,11 @@ export async function rendereProtokoll() {
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Anzeige leeren",
|
||||
title: t("logs.clear"),
|
||||
onclick: () => logContainer?.replaceChildren()
|
||||
},
|
||||
erstelleIcon(PFADE.leeren, 14),
|
||||
erstelleElement("span", { text: " Leeren" })
|
||||
erstelleElement("span", { text: " " + t("logs.clear") })
|
||||
);
|
||||
|
||||
steuerungGruppe.append(instanzSelect, modulSelect, levelSelect, autoScrollLabel, pauseButton, leerenBtn);
|
||||
@@ -120,7 +121,6 @@ export async function rendereProtokoll() {
|
||||
layout.append(kopfZeile, logContainer);
|
||||
container.append(layout);
|
||||
|
||||
// Lade verfügbare Instanzen für Filter
|
||||
try {
|
||||
const instanzen = await api("/verwaltung/instanzen");
|
||||
if (Array.isArray(instanzen)) {
|
||||
@@ -129,10 +129,9 @@ export async function rendereProtokoll() {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Nicht kritisch
|
||||
//
|
||||
}
|
||||
|
||||
// Lade verfügbare Module
|
||||
try {
|
||||
const module = await api("/logs/modules");
|
||||
if (Array.isArray(module)) {
|
||||
@@ -141,7 +140,7 @@ export async function rendereProtokoll() {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Nicht kritisch, Filter bleibt auf "Alle Module"
|
||||
//
|
||||
}
|
||||
|
||||
starteLogStream();
|
||||
@@ -182,9 +181,9 @@ function starteLogStream() {
|
||||
function fuegeLogZeileEin(roh) {
|
||||
if (!logContainer) return;
|
||||
|
||||
// Seit #591 kommen die Felder klein geschrieben (module/level/text). Instanzen einer
|
||||
// älteren Fassung senden sie noch groß — beide Schreibweisen annehmen, sonst zeigt
|
||||
// die Ansicht bei gemischten Fassungen wieder nur den Instanznamen.
|
||||
// Since #591 the fields arrive in lower case (module/level/text). Instances of an
|
||||
// older version still send them capitalised — accept both, otherwise the view shows
|
||||
// only the instance name again when versions are mixed.
|
||||
const eintrag = {
|
||||
...roh,
|
||||
module: roh.module ?? roh.Module,
|
||||
@@ -192,7 +191,6 @@ function fuegeLogZeileEin(roh) {
|
||||
text: roh.text ?? roh.Text
|
||||
};
|
||||
|
||||
// Filter nach Instanz falls gewählt
|
||||
if (instanzFilter && eintrag.instanz && eintrag.instanz !== instanzFilter && eintrag.instanzName !== instanzFilter) {
|
||||
return;
|
||||
}
|
||||
@@ -226,13 +224,11 @@ function fuegeLogZeileEin(roh) {
|
||||
zeile.append(erstelleElement("span", { klasse: "log-modul", text: `[${eintrag.module}] ` }));
|
||||
}
|
||||
|
||||
// Wichtig: textContent, kein innerHTML!
|
||||
const textSpan = erstelleElement("span", { klasse: "log-text", text: eintrag.text || "" });
|
||||
zeile.append(textSpan);
|
||||
|
||||
logContainer.append(zeile);
|
||||
|
||||
// Begrenze auf 300 Zeilen im DOM
|
||||
while (logContainer.children.length > 300) {
|
||||
logContainer.firstElementChild?.remove();
|
||||
}
|
||||
@@ -246,19 +242,19 @@ function togglePause() {
|
||||
istPausiert = !istPausiert;
|
||||
if (istPausiert) {
|
||||
trenneLogStream();
|
||||
pauseButton.replaceChildren(erstelleIcon(PFADE.abspielen, 14), erstelleElement("span", { text: " Fortsetzen" }));
|
||||
aktualisiereStatus(false, "Pausiert");
|
||||
pauseButton.replaceChildren(erstelleIcon(PFADE.abspielen, 14), erstelleElement("span", { text: " " + t("logs.resume") }));
|
||||
aktualisiereStatus(false, t("logs.paused"));
|
||||
} else {
|
||||
pauseButton.replaceChildren(erstelleIcon(PFADE.abbrechen, 14), erstelleElement("span", { text: " Anhalten" }));
|
||||
pauseButton.replaceChildren(erstelleIcon(PFADE.abbrechen, 14), erstelleElement("span", { text: " " + t("logs.pause") }));
|
||||
starteLogStream();
|
||||
}
|
||||
}
|
||||
|
||||
function aktualisiereStatus(offen, text = null) {
|
||||
if (!streamStatusPlakette) return;
|
||||
const t = text || (offen ? "Live" : "Getrennt");
|
||||
const statusT = text || (offen ? "Live" : "—");
|
||||
const art = offen ? "erfolg" : "neutral";
|
||||
const neuePlakette = erstellePlakette(t, art);
|
||||
const neuePlakette = erstellePlakette(statusT, art);
|
||||
streamStatusPlakette.replaceWith(neuePlakette);
|
||||
streamStatusPlakette = neuePlakette;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// ClawdDotNet — Hintergrunddienste
|
||||
//
|
||||
// Übersicht und Verwaltung der Instanz-Dienste (Port, Protokoll, Status, Ein-/Ausschalten).
|
||||
// Overview and management of instance services (port, protocol, status, toggle).
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import { abonnieren } from "../modules/events.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleIcon,
|
||||
@@ -28,7 +29,7 @@ export async function rendereDienste() {
|
||||
|
||||
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
kopfZeile.append(
|
||||
erstelleElement("h1", { text: "Dienste" }),
|
||||
erstelleElement("h1", { text: t("services.title") }),
|
||||
erstelleElement(
|
||||
"button",
|
||||
{
|
||||
@@ -36,7 +37,7 @@ export async function rendereDienste() {
|
||||
onclick: oeffneNeuerDienstDialog
|
||||
},
|
||||
erstelleIcon(PFADE.plus, 16),
|
||||
erstelleElement("span", { text: " Neuer Dienst" })
|
||||
erstelleElement("span", { text: " " + t("services.newService") })
|
||||
)
|
||||
);
|
||||
|
||||
@@ -60,7 +61,7 @@ async function ladeDienste() {
|
||||
if (!container) return;
|
||||
|
||||
container.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Dienste…" })
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("common.loading") })
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -69,10 +70,10 @@ async function ladeDienste() {
|
||||
|
||||
if (!services || services.length === 0) {
|
||||
container.append(
|
||||
erstelleKarte("Keine Dienste", [
|
||||
erstelleKarte(t("common.none"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Für diese Instanz sind noch keine Hintergrunddienste eingerichtet."
|
||||
text: t("services.empty")
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -83,12 +84,12 @@ async function ladeDienste() {
|
||||
const thead = erstelleElement("thead");
|
||||
thead.append(
|
||||
erstelleElement("tr", {},
|
||||
erstelleElement("th", { text: "Aktiv" }),
|
||||
erstelleElement("th", { text: "Dienstname" }),
|
||||
erstelleElement("th", { text: "Typ" }),
|
||||
erstelleElement("th", { text: "Port" }),
|
||||
erstelleElement("th", { text: "Beschreibung" }),
|
||||
erstelleElement("th", { text: "Aktionen", stil: "text-align: right;" })
|
||||
erstelleElement("th", { text: t("services.active") }),
|
||||
erstelleElement("th", { text: t("services.serviceName") }),
|
||||
erstelleElement("th", { text: t("services.serviceType") }),
|
||||
erstelleElement("th", { text: t("services.port") }),
|
||||
erstelleElement("th", { text: t("common.description") }),
|
||||
erstelleElement("th", { text: t("common.actions"), stil: "text-align: right;" })
|
||||
)
|
||||
);
|
||||
|
||||
@@ -110,10 +111,10 @@ async function ladeDienste() {
|
||||
method: "PUT"
|
||||
});
|
||||
s.enabled = aktiv;
|
||||
zeigeMeldung(`Dienst '${s.name}' ${aktiv ? "aktiviert" : "deaktiviert"}.`, "info");
|
||||
zeigeMeldung(`'${s.name}' ${aktiv ? "enabled" : "disabled"}.`, "info");
|
||||
} catch (err) {
|
||||
e.target.checked = !aktiv;
|
||||
zeigeMeldung(`Fehler: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(`Error: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -130,7 +131,7 @@ async function ladeDienste() {
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche gefahr flach klein",
|
||||
title: "Dienst löschen",
|
||||
title: t("common.delete"),
|
||||
onclick: () => loescheDienst(s)
|
||||
},
|
||||
erstelleIcon(PFADE.muelleimer, 14)
|
||||
@@ -151,14 +152,14 @@ async function ladeDienste() {
|
||||
}
|
||||
|
||||
async function loescheDienst(s) {
|
||||
if (!confirm(`Soll der Dienst '${s.name}' (${s.id}) gelöscht werden?`)) return;
|
||||
if (!confirm(`Delete service '${s.name}' (${s.id})?`)) return;
|
||||
|
||||
try {
|
||||
await api(`/services/${encodeURIComponent(s.id)}`, { method: "DELETE" });
|
||||
zeigeMeldung(`Dienst '${s.name}' wurde gelöscht.`, "info");
|
||||
zeigeMeldung(`Service '${s.name}' deleted.`, "info");
|
||||
await ladeDienste();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Löschen fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(`Failed: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,35 +169,35 @@ function oeffneNeuerDienstDialog() {
|
||||
const nameFeld = erstelleElement("input", {
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "z.B. Webhook-Empfänger"
|
||||
placeholder: "e.g. WebHookService"
|
||||
});
|
||||
|
||||
const typFeld = erstelleElement("input", {
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "z.B. HTTP, TCP, Socket"
|
||||
placeholder: "e.g. HTTP, TCP, Socket"
|
||||
});
|
||||
|
||||
const portFeld = erstelleElement("input", {
|
||||
type: "number",
|
||||
min: "1",
|
||||
max: "65535",
|
||||
placeholder: "z.B. 8081 (optional)"
|
||||
placeholder: "e.g. 8081 (optional)"
|
||||
});
|
||||
|
||||
const descFeld = erstelleElement("textarea", {
|
||||
rows: "2",
|
||||
placeholder: "Beschreibung des Dienstes…"
|
||||
placeholder: "..."
|
||||
});
|
||||
|
||||
formular.append(
|
||||
erstelleElement("label", { text: "Dienstname:" }),
|
||||
erstelleElement("label", { text: t("services.serviceName") + ":" }),
|
||||
nameFeld,
|
||||
erstelleElement("label", { text: "Diensttyp:" }),
|
||||
erstelleElement("label", { text: t("services.serviceType") + ":" }),
|
||||
typFeld,
|
||||
erstelleElement("label", { text: "Port (optional):" }),
|
||||
erstelleElement("label", { text: t("services.port") + " (optional):" }),
|
||||
portFeld,
|
||||
erstelleElement("label", { text: "Beschreibung:" }),
|
||||
erstelleElement("label", { text: t("common.description") + ":" }),
|
||||
descFeld
|
||||
);
|
||||
|
||||
@@ -204,16 +205,16 @@ function oeffneNeuerDienstDialog() {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: () => modal.schliessen(),
|
||||
text: "Abbrechen"
|
||||
text: t("common.cancel")
|
||||
});
|
||||
|
||||
const anlegenBtn = erstelleElement("button", {
|
||||
type: "submit",
|
||||
klasse: "schaltflaeche primaer",
|
||||
text: "Dienst anlegen"
|
||||
text: t("common.save")
|
||||
});
|
||||
|
||||
const modal = erstelleModal("Neuen Hintergrunddienst anlegen", formular, [abbrechenBtn, anlegenBtn]);
|
||||
const modal = erstelleModal(t("services.newService"), formular, [abbrechenBtn, anlegenBtn]);
|
||||
|
||||
formular.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -230,10 +231,10 @@ function oeffneNeuerDienstDialog() {
|
||||
}
|
||||
});
|
||||
modal.schliessen();
|
||||
zeigeMeldung(`Dienst '${nameFeld.value.trim()}' angelegt.`, "erfolg");
|
||||
zeigeMeldung(`Service '${nameFeld.value.trim()}' created.`, "erfolg");
|
||||
await ladeDienste();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fehler beim Anlegen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(`Error: ${err.message}`, "gefahr");
|
||||
} finally {
|
||||
anlegenBtn.disabled = false;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// ClawdDotNet — Einstellungen & Systemarchitektur
|
||||
// ClawdDotNet — Einstellungen & Systemarchitektur
|
||||
//
|
||||
// Strikte Trennung zwischen globaler Manager-Ebene (Installation) und isolierter Instanz-Ebene.
|
||||
|
||||
import { api, holeAktiveInstanz } from "../modules/api.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleKarte,
|
||||
@@ -23,10 +24,10 @@ export async function rendereEinstellungen() {
|
||||
const kopf = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
const kopfText = erstelleElement("div");
|
||||
kopfText.append(
|
||||
erstelleElement("h1", { text: "Einstellungen & Systemarchitektur" }),
|
||||
erstelleElement("h1", { text: t("settings.title") }),
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Übersicht der Konfiguration nach dem Instanzmanager-Konzept: Strikte Trennung zwischen globaler Manager-Ebene (Installation) und isolierter Instanz-Ebene."
|
||||
text: t("settings.intro")
|
||||
})
|
||||
);
|
||||
kopf.append(kopfText);
|
||||
@@ -42,14 +43,14 @@ export async function rendereEinstellungen() {
|
||||
const managerTitelLinks = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
managerTitelLinks.append(
|
||||
erstelleIcon(PFADE.server, 20),
|
||||
erstelleElement("h2", { text: "Instanzmanager" }),
|
||||
erstellePlakette("Manager-Ebene · Global", "warnung")
|
||||
erstelleElement("h2", { text: t("settings.managerLevel") }),
|
||||
erstellePlakette(t("settings.managerBadge"), "warnung")
|
||||
);
|
||||
managerKopf.append(managerTitelLinks);
|
||||
|
||||
const managerIntro = erstelleElement("p", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: "Gilt für alle Instanzen und die Weboberfläche. Gespeichert in manager.json im Anwendungs-Hauptverzeichnis."
|
||||
text: t("settings.managerIntro")
|
||||
});
|
||||
|
||||
const managerGitter = erstelleElement("div", { klasse: "info-gitter" });
|
||||
@@ -57,44 +58,44 @@ export async function rendereEinstellungen() {
|
||||
// Karte 1: Weboberfläche & Netzwerk
|
||||
const dlNetz = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlNetz.append(
|
||||
erstelleElement("dt", { text: "Konfigurationsdatei" }),
|
||||
erstelleElement("dt", { text: t("settings.configFile") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: "manager.json" })),
|
||||
erstelleElement("dt", { text: "WebUI-Port" }),
|
||||
erstelleElement("dd", { text: "7000 (Vorgabe, webUiPort)" }),
|
||||
erstelleElement("dt", { text: "Netzwerk-Bindung" }),
|
||||
erstelleElement("dd", { text: "loopback (127.0.0.1, webUiBinding)" }),
|
||||
erstelleElement("dt", { text: "Zugangsschutz" }),
|
||||
erstelleElement("dd", {}, erstellePlakette("Token aktiv (≥ 20 Zeichen)", "erfolg"))
|
||||
erstelleElement("dt", { text: t("settings.webUiPort") }),
|
||||
erstelleElement("dd", { text: t("settings.webUiPortDefault") }),
|
||||
erstelleElement("dt", { text: t("settings.networkBinding") }),
|
||||
erstelleElement("dd", { text: t("settings.networkBindingValue") }),
|
||||
erstelleElement("dt", { text: t("settings.accessProtection") }),
|
||||
erstelleElement("dd", {}, erstellePlakette(t("settings.tokenActive"), "erfolg"))
|
||||
);
|
||||
managerGitter.append(erstelleKarte("Weboberfläche & Netzwerk", [dlNetz]));
|
||||
managerGitter.append(erstelleKarte(t("settings.webUiAndNet"), [dlNetz]));
|
||||
|
||||
// Karte 2: Lizenz & Programm-Updates
|
||||
const dlLizenz = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlLizenz.append(
|
||||
erstelleElement("dt", { text: "Lizenz-Geltung" }),
|
||||
erstelleElement("dd", { text: "Installationsebene (E-M3: Beliebig viele Instanzen)" }),
|
||||
erstelleElement("dt", { text: "Lizenzschlüssel" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: "●●●●-●●●●-●●●●-●●●● (verschlüsselt)" })),
|
||||
erstelleElement("dt", { text: "Update-Kanal" }),
|
||||
erstelleElement("dd", { text: "prod (Offizielle Release-Versionen)" }),
|
||||
erstelleElement("dt", { text: "Update-Prüfung" }),
|
||||
erstelleElement("dd", { text: "Zentral auf Manager-Ebene" })
|
||||
erstelleElement("dt", { text: t("settings.licenseScope") }),
|
||||
erstelleElement("dd", { text: t("settings.licenseScopeValue") }),
|
||||
erstelleElement("dt", { text: t("settings.licenseKey") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: t("settings.licenseEncrypted") })),
|
||||
erstelleElement("dt", { text: t("settings.updateChannel") }),
|
||||
erstelleElement("dd", { text: t("settings.updateChannelValue") }),
|
||||
erstelleElement("dt", { text: t("settings.updateCheck") }),
|
||||
erstelleElement("dd", { text: t("settings.updateCheckValue") })
|
||||
);
|
||||
managerGitter.append(erstelleKarte("Lizenz & Updates", [dlLizenz]));
|
||||
managerGitter.append(erstelleKarte(t("settings.licenseAndUpdates"), [dlLizenz]));
|
||||
|
||||
// Karte 3: Deploymentcenter & Aufsicht
|
||||
const dlAufsicht = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlAufsicht.append(
|
||||
erstelleElement("dt", { text: "Deploymentcenter" }),
|
||||
erstelleElement("dd", { text: "https://dc.mhdf.de" }),
|
||||
erstelleElement("dt", { text: "Master-Token" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: "●●●●●●●● (erzeugt Sub-Token je Instanz)" })),
|
||||
erstelleElement("dt", { text: "Desktop-Fenster" }),
|
||||
erstelleElement("dd", { text: "Aktiv (jede Instanz hat ein eigenes Fenster)" }),
|
||||
erstelleElement("dt", { text: "Absturz-Schutz" }),
|
||||
erstelleElement("dd", { text: "Max. 5 Neustarts / 10 Minuten" })
|
||||
erstelleElement("dt", { text: t("settings.dcMasterToken") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: t("settings.dcMasterTokenValue") })),
|
||||
erstelleElement("dt", { text: t("settings.desktopWindows") }),
|
||||
erstelleElement("dd", { text: t("settings.desktopWindowsValue") }),
|
||||
erstelleElement("dt", { text: t("settings.crashProtection") }),
|
||||
erstelleElement("dd", { text: t("settings.crashProtectionValue") })
|
||||
);
|
||||
managerGitter.append(erstelleKarte("Deploymentcenter & Aufsicht", [dlAufsicht]));
|
||||
managerGitter.append(erstelleKarte(t("settings.dcAndOversight"), [dlAufsicht]));
|
||||
|
||||
managerSektion.append(managerKopf, managerIntro, managerGitter);
|
||||
layout.append(managerSektion);
|
||||
@@ -114,35 +115,39 @@ export async function rendereEinstellungen() {
|
||||
}
|
||||
|
||||
const aktiveInstanz = instanzenListe.find((i) => i.id === aktiveId);
|
||||
const zInfo = aktiveInstanz ? instanzZustandInfo(aktiveInstanz.zustand) : { text: "Keine gewählt", art: "neutral" };
|
||||
const zInfo = aktiveInstanz ? instanzZustandInfo(aktiveInstanz.zustand) : { text: t("common.none"), art: "neutral" };
|
||||
|
||||
const instanzKopf = erstelleElement("div", { klasse: "sektion-titel-zeile" });
|
||||
const instanzTitelLinks = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
instanzTitelLinks.append(
|
||||
erstelleIcon(PFADE.fenster, 20),
|
||||
erstelleElement("h2", { text: aktiveInstanz ? `Aktive Instanz: ${aktiveInstanz.name}` : "Instanzbezogene Einstellungen" }),
|
||||
erstellePlakette("Instanz-Ebene · Isoliert", "erfolg"),
|
||||
aktiveInstanz ? erstellePlakette(zInfo.text, zInfo.art) : erstellePlakette("Keine Instanz aktiv", "neutral")
|
||||
erstelleElement("h2", {
|
||||
text: aktiveInstanz
|
||||
? t("settings.instanceSectionTitle", { name: aktiveInstanz.name })
|
||||
: t("settings.instanceSectionTitleNone")
|
||||
}),
|
||||
erstellePlakette(t("settings.instanceBadge"), "erfolg"),
|
||||
aktiveInstanz ? erstellePlakette(zInfo.text, zInfo.art) : erstellePlakette(t("settings.noInstanceActive"), "neutral")
|
||||
);
|
||||
instanzKopf.append(instanzTitelLinks);
|
||||
|
||||
const instanzIntro = erstelleElement("p", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: "Gilt ausschließlich für die aktuell ausgewählte Instanz. Alle Pfade, Protokolle, Agenten und Budgets sind strikt von anderen Instanzen isoliert."
|
||||
text: t("settings.instanceIntro")
|
||||
});
|
||||
|
||||
instanzSektion.append(instanzKopf, instanzIntro);
|
||||
|
||||
if (!aktiveInstanz) {
|
||||
const leerKarte = erstelleKarte("Keine Instanz ausgewählt", [
|
||||
const leerKarte = erstelleKarte(t("settings.noInstanceSelected"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Bitte wähle oben im Kopfbereich eine aktive Instanz aus oder wechsle in den Bereich 'Instanzen', um eine Instanz zu starten oder auszuwählen."
|
||||
text: t("settings.noInstancePrompt")
|
||||
}),
|
||||
erstelleElement("a", {
|
||||
href: "#/instanzen",
|
||||
klasse: "schaltflaeche primaer klein",
|
||||
text: "Zur Instanzen-Übersicht"
|
||||
text: t("settings.toInstancesOverview")
|
||||
})
|
||||
]);
|
||||
instanzSektion.append(leerKarte);
|
||||
@@ -158,45 +163,51 @@ export async function rendereEinstellungen() {
|
||||
// Karte 1: Instanz-Identität & Autostart
|
||||
const dlIdent = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlIdent.append(
|
||||
erstelleElement("dt", { text: "Instanzname" }),
|
||||
erstelleElement("dt", { text: t("settings.instanceName") }),
|
||||
erstelleElement("dd", {}, erstelleElement("strong", { text: aktiveInstanz.name })),
|
||||
erstelleElement("dt", { text: "Instanz-ID" }),
|
||||
erstelleElement("dt", { text: t("settings.instanceId") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: aktiveInstanz.id })),
|
||||
erstelleElement("dt", { text: "Konfigurationsdatei" }),
|
||||
erstelleElement("dt", { text: t("settings.configFile") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/InstanceSettings.json` })),
|
||||
erstelleElement("dt", { text: "Autostart durch Manager" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(aktiveInstanz.autostart ? "Aktiv (Am Laufen halten)" : "Inaktiv (Manueller Start)", aktiveInstanz.autostart ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: "Kommunikation" }),
|
||||
erstelleElement("dd", { text: "HTTP über Named Pipe (Windows) / Socket (Linux)" })
|
||||
erstelleElement("dt", { text: t("settings.autostartByManager") }),
|
||||
erstelleElement("dd", {}, erstellePlakette(
|
||||
aktiveInstanz.autostart ? t("settings.autostartActive") : t("settings.autostartInactive"),
|
||||
aktiveInstanz.autostart ? "erfolg" : "neutral"
|
||||
)),
|
||||
erstelleElement("dt", { text: t("settings.communication") }),
|
||||
erstelleElement("dd", { text: t("settings.communicationValue") })
|
||||
);
|
||||
instanzGitter.append(erstelleKarte("Identität & Prozess", [dlIdent]));
|
||||
instanzGitter.append(erstelleKarte(t("settings.identityAndProcess"), [dlIdent]));
|
||||
|
||||
// Karte 2: Isolierte Verzeichnisse & Speicher
|
||||
const dlPfade = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlPfade.append(
|
||||
erstelleElement("dt", { text: "Protokolle (Logs)" }),
|
||||
erstelleElement("dt", { text: t("settings.logsPath") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/Logs/` })),
|
||||
erstelleElement("dt", { text: "Datensicherungen" }),
|
||||
erstelleElement("dt", { text: t("settings.backupsPath") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/Backups/` })),
|
||||
erstelleElement("dt", { text: "Zustandsdatenbank" }),
|
||||
erstelleElement("dt", { text: t("settings.stateDbPath") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/state.db (exklusiv)` })),
|
||||
erstelleElement("dt", { text: "Arbeitsbereich (Shared)" }),
|
||||
erstelleElement("dt", { text: t("settings.workspacePath") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/Agents/SharedWorkspace/` }))
|
||||
);
|
||||
instanzGitter.append(erstelleKarte("Isolierte Verzeichnisse", [dlPfade]));
|
||||
instanzGitter.append(erstelleKarte(t("settings.isolatedPaths"), [dlPfade]));
|
||||
|
||||
// Karte 3: Modellanbindung & Budget (OpenRouter)
|
||||
const dlModell = erstelleElement("dl", { klasse: "datenliste" });
|
||||
if (instanzStatus) {
|
||||
dlModell.append(
|
||||
erstelleElement("dt", { text: "OpenRouter-Status" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(instanzStatus.isApiReachable ? "Verbunden" : "Offline", instanzStatus.isApiReachable ? "erfolg" : "gefahr")),
|
||||
erstelleElement("dt", { text: "Statusmeldung" }),
|
||||
erstelleElement("dt", { text: t("settings.openRouterStatus") }),
|
||||
erstelleElement("dd", {}, erstellePlakette(
|
||||
instanzStatus.isApiReachable ? t("settings.connected") : t("settings.offline"),
|
||||
instanzStatus.isApiReachable ? "erfolg" : "gefahr"
|
||||
)),
|
||||
erstelleElement("dt", { text: t("settings.statusMessage") }),
|
||||
erstelleElement("dd", { text: instanzStatus.statusText || "—" }),
|
||||
erstelleElement("dt", { text: "Guthaben / Credits" }),
|
||||
erstelleElement("dt", { text: t("settings.credits") }),
|
||||
erstelleElement("dd", {}, erstelleElement("strong", { text: instanzStatus.creditsText || "—" })),
|
||||
erstelleElement("dt", { text: "API-Schlüssel" }),
|
||||
erstelleElement("dd", { text: "In InstanceSettings.json dieser Instanz maskiert hinterlegt" })
|
||||
erstelleElement("dt", { text: t("settings.apiKey") }),
|
||||
erstelleElement("dd", { text: t("settings.apiKeyMasked") })
|
||||
);
|
||||
} else {
|
||||
dlModell.append(
|
||||
@@ -204,38 +215,38 @@ export async function rendereEinstellungen() {
|
||||
erstelleElement("dd", {
|
||||
klasse: "gedaempft",
|
||||
text: aktiveInstanz.zustand === "Running"
|
||||
? "Kein Schlüssel hinterlegt oder Dienst nicht erreichbar."
|
||||
: "Instanz läuft nicht. Daten werden nach dem Start geladen."
|
||||
? t("settings.openRouterNoKey")
|
||||
: t("settings.openRouterNotRunning")
|
||||
})
|
||||
);
|
||||
}
|
||||
instanzGitter.append(erstelleKarte("Modellanbindung (OpenRouter)", [dlModell]));
|
||||
instanzGitter.append(erstelleKarte(t("settings.openRouterCard"), [dlModell]));
|
||||
|
||||
// Karte 4: Teilsysteme der Instanz
|
||||
const dlSysteme = erstelleElement("dl", { klasse: "datenliste" });
|
||||
if (instanzInfo) {
|
||||
dlSysteme.append(
|
||||
erstelleElement("dt", { text: "Agent Engine" }),
|
||||
erstelleElement("dt", { text: t("settings.agentEngine") }),
|
||||
erstelleElement("dd", {}, erstellePlakette(instanzInfo.engineActive ? "Aktiv" : "Aus", instanzInfo.engineActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: "Scanner / Scheduler" }),
|
||||
erstelleElement("dt", { text: t("settings.scheduler") }),
|
||||
erstelleElement("dd", {}, erstellePlakette(instanzInfo.schedulerActive ? "Aktiv" : "Aus", instanzInfo.schedulerActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: "Freigabestelle (Staging)" }),
|
||||
erstelleElement("dt", { text: t("settings.stagingService") }),
|
||||
erstelleElement("dd", {}, erstellePlakette(instanzInfo.stagingActive ? "Aktiv" : "Aus", instanzInfo.stagingActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: "Deploymentcenter Sub-Token" }),
|
||||
erstelleElement("dt", { text: t("settings.dcSubToken") }),
|
||||
erstelleElement("dd", {}, erstellePlakette(instanzInfo.deploymentcenterActive ? "Aktiv" : "Aus", instanzInfo.deploymentcenterActive ? "erfolg" : "neutral"))
|
||||
);
|
||||
} else {
|
||||
dlSysteme.append(
|
||||
erstelleElement("dt", { text: "Subsysteme" }),
|
||||
erstelleElement("dt", { text: t("info.subsystems") }),
|
||||
erstelleElement("dd", {
|
||||
klasse: "gedaempft",
|
||||
text: aktiveInstanz.zustand === "Running"
|
||||
? "Laufzeitdaten werden abgefragt…"
|
||||
: "Instanz ist beendet — Teilsysteme ruhen."
|
||||
? t("settings.subsystemsLoading")
|
||||
: t("settings.subsystemsStopped")
|
||||
})
|
||||
);
|
||||
}
|
||||
instanzGitter.append(erstelleKarte("Laufzeit & Teilsysteme", [dlSysteme]));
|
||||
instanzGitter.append(erstelleKarte(t("settings.runtimeAndSubsystems"), [dlSysteme]));
|
||||
|
||||
instanzSektion.append(instanzGitter);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// Überprüfung und Genehmigung / Ablehnung von sensiblen Werkzeug-Aufrufen.
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleIcon,
|
||||
@@ -33,7 +34,7 @@ export async function rendereStaging() {
|
||||
|
||||
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
kopfZeile.append(
|
||||
erstelleElement("h1", { text: "Freigabestelle" }),
|
||||
erstelleElement("h1", { text: t("staging.title") }),
|
||||
erstelleElement(
|
||||
"button",
|
||||
{
|
||||
@@ -41,7 +42,7 @@ export async function rendereStaging() {
|
||||
onclick: ladeFreigaben
|
||||
},
|
||||
erstelleIcon(PFADE.aktualisieren, 16),
|
||||
erstelleElement("span", { text: " Aktualisieren" })
|
||||
erstelleElement("span", { text: ` ${t("staging.refresh")}` })
|
||||
)
|
||||
);
|
||||
|
||||
@@ -58,7 +59,7 @@ async function ladeFreigaben() {
|
||||
if (!container) return;
|
||||
|
||||
container.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Freigaben…" })
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("staging.loading") })
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -71,10 +72,10 @@ async function ladeFreigaben() {
|
||||
|
||||
if (!wartendeAufrufe || wartendeAufrufe.length === 0) {
|
||||
container.append(
|
||||
erstelleKarte("Keine offenen Freigaben", [
|
||||
erstelleKarte(t("staging.empty"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Aktuell warten keine Werkzeug-Aufrufe auf eine manuelle Bestätigung."
|
||||
text: t("staging.emptyHint")
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -95,7 +96,7 @@ async function ladeFreigaben() {
|
||||
onclick: () => bestaetigeFreigabe(anfrage.id, gleichartig.checked)
|
||||
},
|
||||
erstelleIcon(PFADE.haken, 14),
|
||||
erstelleElement("span", { text: " Freigeben" })
|
||||
erstelleElement("span", { text: ` ${t("staging.approve")}` })
|
||||
);
|
||||
|
||||
const ablehnenBtn = erstelleElement(
|
||||
@@ -105,25 +106,25 @@ async function ladeFreigaben() {
|
||||
onclick: () => oeffneAblehnenDialog(anfrage.id)
|
||||
},
|
||||
erstelleIcon(PFADE.abbrechen, 14),
|
||||
erstelleElement("span", { text: " Ablehnen" })
|
||||
erstelleElement("span", { text: ` ${t("staging.reject")}` })
|
||||
);
|
||||
|
||||
kopfAktionen.append(freigebenBtn, ablehnenBtn);
|
||||
|
||||
const datenliste = erstelleElement("dl", { klasse: "datenliste" });
|
||||
datenliste.append(
|
||||
erstelleElement("dt", { text: "Aufruf-ID" }),
|
||||
erstelleElement("dt", { text: t("staging.callId") }),
|
||||
erstelleElement("dd", { text: String(anfrage.id) }),
|
||||
erstelleElement("dt", { text: "Agent" }),
|
||||
erstelleElement("dt", { text: t("staging.agent") }),
|
||||
erstelleElement("dd", { text: anfrage.agentId || "—" }),
|
||||
erstelleElement("dt", { text: "Werkzeug" }),
|
||||
erstelleElement("dt", { text: t("staging.tool") }),
|
||||
erstelleElement("dd", { text: `${anfrage.tool} → ${anfrage.action || "—"}` }),
|
||||
erstelleElement("dt", { text: "Zeitpunkt" }),
|
||||
erstelleElement("dt", { text: t("staging.time") }),
|
||||
erstelleElement("dd", { text: formatiereDatumZeit(anfrage.createdAt) })
|
||||
);
|
||||
|
||||
const argBox = erstelleElement("div", { klasse: "staging-argumente" });
|
||||
argBox.append(erstelleElement("strong", { text: "Eingefrorene Argumente:" }));
|
||||
argBox.append(erstelleElement("strong", { text: t("staging.frozenArgs") }));
|
||||
|
||||
// Strikt textContent! Argumente können Fremdtext sein.
|
||||
// Die API liefert sie als rohe Zeichenkette (argumentsJson) — genau das, was bei
|
||||
@@ -140,10 +141,12 @@ async function ladeFreigaben() {
|
||||
teile.push(
|
||||
erstelleElement("div", {
|
||||
klasse: "hinweis-box vorschlag",
|
||||
text:
|
||||
`Diese Operation von ${anfrage.agentId} wurde in den letzten 14 Tagen schon ` +
|
||||
`${anfrage.previousApprovals}-mal freigegeben. Soll ${anfrage.tool}.${anfrage.action} ` +
|
||||
`für diesen Agenten künftig ohne Rückfrage laufen?`
|
||||
text: t("staging.suggestHint", {
|
||||
agent: anfrage.agentId,
|
||||
approvals: anfrage.previousApprovals,
|
||||
tool: anfrage.tool,
|
||||
action: anfrage.action
|
||||
})
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -155,9 +158,11 @@ async function ladeFreigaben() {
|
||||
{ klasse: "gleichartig-wahl", for: `gleichartig-${anfrage.id}` },
|
||||
gleichartig,
|
||||
erstelleElement("span", {
|
||||
text:
|
||||
`Gleichartiges künftig erlauben: ${anfrage.agentId} darf ${anfrage.tool}.${anfrage.action} ` +
|
||||
`ab jetzt ohne Rückfrage (widerrufbar unten unter „Dauerfreigaben“).`
|
||||
text: t("staging.allowSimilar", {
|
||||
agent: anfrage.agentId,
|
||||
tool: anfrage.tool,
|
||||
action: anfrage.action
|
||||
})
|
||||
})
|
||||
)
|
||||
);
|
||||
@@ -171,10 +176,10 @@ async function ladeFreigaben() {
|
||||
}
|
||||
} catch (err) {
|
||||
container.replaceChildren(
|
||||
erstelleKarte("Freigabestelle nicht aktiv", [
|
||||
erstelleKarte(t("staging.notActive"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: err.message || "In dieser Instanz ist keine Freigabestelle eingerichtet."
|
||||
text: err.message || t("staging.notActiveHint")
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -186,7 +191,7 @@ async function ladeFreigaben() {
|
||||
* gültiges JSON ist, sonst genau die Zeichenkette, die ausgeführt würde.
|
||||
*/
|
||||
function formatiereArgumente(argumentsJson) {
|
||||
if (!argumentsJson) return "(keine)";
|
||||
if (!argumentsJson) return "(none)";
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(argumentsJson), null, 2);
|
||||
} catch {
|
||||
@@ -209,12 +214,15 @@ async function bestaetigeFreigabe(id, gleichartig = false) {
|
||||
try {
|
||||
const res = await api(`/staging/${id}/approve${gleichartig ? "?similar=true" : ""}`, { method: "POST" });
|
||||
zeigeMeldung(
|
||||
`Aufruf freigegeben${gleichartig ? " und künftig erlaubt" : ""}: ${res?.message ?? "Erfolgreich ausgeführt"}`,
|
||||
t("staging.approved", {
|
||||
similar: gleichartig ? t("staging.similarSuffix") : "",
|
||||
message: res?.message ?? t("staging.executedSuccess")
|
||||
}),
|
||||
"erfolg"
|
||||
);
|
||||
await ladeFreigaben();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Freigabe fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("staging.approveFailed", { error: err.message }), "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,15 +246,13 @@ async function ladeDauerfreigaben() {
|
||||
inhalt.push(
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text:
|
||||
"Keine. Beim Freigeben lässt sich „Gleichartiges künftig erlauben“ wählen; " +
|
||||
"dauerhafte Rechte stehen sonst in der Werkzeugkonfiguration des Agenten."
|
||||
text: t("staging.standingEmpty")
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const tabelle = erstelleElement("table", { klasse: "daten-tabelle" });
|
||||
const kopf = erstelleElement("tr", {});
|
||||
for (const titel of ["Agent", "Operation", "Erteilt", "Anlass", ""]) {
|
||||
for (const titel of [t("staging.agent"), t("staging.tool"), t("staging.granted"), t("staging.occasion"), ""]) {
|
||||
kopf.append(erstelleElement("th", { text: titel }));
|
||||
}
|
||||
tabelle.append(kopf);
|
||||
@@ -256,7 +262,7 @@ async function ladeDauerfreigaben() {
|
||||
const widerrufen = erstelleElement(
|
||||
"button",
|
||||
{ klasse: "schaltflaeche gefahr flach klein", onclick: () => widerrufeDauerfreigabe(d) },
|
||||
erstelleElement("span", { text: "Widerrufen" })
|
||||
erstelleElement("span", { text: t("staging.revoke") })
|
||||
);
|
||||
zeile.append(
|
||||
erstelleElement("td", { text: d.agentId }),
|
||||
@@ -270,19 +276,19 @@ async function ladeDauerfreigaben() {
|
||||
inhalt.push(tabelle);
|
||||
}
|
||||
|
||||
bereich.replaceChildren(erstelleKarte("Dauerfreigaben", inhalt));
|
||||
bereich.replaceChildren(erstelleKarte(t("staging.standingTitle"), inhalt));
|
||||
}
|
||||
|
||||
async function widerrufeDauerfreigabe(d) {
|
||||
if (!confirm(`Dauerfreigabe widerrufen? ${d.agentId} muss ${d.tool}.${d.operation} dann wieder einzeln vorlegen.`)) {
|
||||
if (!confirm(t("staging.revokeConfirm", { agent: d.agentId, tool: d.tool, action: d.operation }))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api(`/staging/standing/${d.id}/revoke`, { method: "POST" });
|
||||
zeigeMeldung(res?.message ?? "Widerrufen.", res?.success === false ? "gefahr" : "info");
|
||||
zeigeMeldung(res?.message ?? t("staging.revoked"), res?.success === false ? "gefahr" : "info");
|
||||
await ladeDauerfreigaben();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Widerruf fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("staging.revokeFailed", { error: err.message }), "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,11 +296,11 @@ function oeffneAblehnenDialog(id) {
|
||||
const formular = erstelleElement("form", { klasse: "formular-raster" });
|
||||
const grundInput = erstelleElement("input", {
|
||||
type: "text",
|
||||
placeholder: "Begründung für die Ablehnung an den Agenten…"
|
||||
placeholder: t("staging.rejectReasonPlaceholder")
|
||||
});
|
||||
|
||||
formular.append(
|
||||
erstelleElement("label", { text: "Begründung (optional):" }),
|
||||
erstelleElement("label", { text: t("staging.rejectReasonLabel") }),
|
||||
grundInput
|
||||
);
|
||||
|
||||
@@ -302,16 +308,16 @@ function oeffneAblehnenDialog(id) {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: () => modal.schliessen(),
|
||||
text: "Abbrechen"
|
||||
text: t("common.cancel")
|
||||
});
|
||||
|
||||
const ablehnenBtn = erstelleElement("button", {
|
||||
type: "submit",
|
||||
klasse: "schaltflaeche gefahr",
|
||||
text: "Aufruf ablehnen"
|
||||
text: t("staging.rejectButton")
|
||||
});
|
||||
|
||||
const modal = erstelleModal("Werkzeug-Aufruf ablehnen", formular, [abbrechenBtn, ablehnenBtn]);
|
||||
const modal = erstelleModal(t("staging.rejectTitle"), formular, [abbrechenBtn, ablehnenBtn]);
|
||||
|
||||
formular.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -323,10 +329,10 @@ function oeffneAblehnenDialog(id) {
|
||||
body: { reason: grundInput.value.trim() }
|
||||
});
|
||||
modal.schliessen();
|
||||
zeigeMeldung("Aufruf wurde abgelehnt.", "info");
|
||||
zeigeMeldung(t("staging.rejected"), "info");
|
||||
await ladeFreigaben();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Ablehnen fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("staging.rejectFailed", { error: err.message }), "gefahr");
|
||||
} finally {
|
||||
ablehnenBtn.disabled = false;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// ClawdDotNet — Aufgaben (Taskboard)
|
||||
//
|
||||
// Seit #594 die einzige Stelle für geplante Arbeit. Der frühere Reiter „Geplante Läufe (Jobs)“
|
||||
// bearbeitete die Alt-Konfiguration scheduler/toolJobs; nach der ersten Übernahme ins Taskboard
|
||||
// wirkte dort keine Änderung mehr. Termin, nächster und letzter Lauf, Pausieren und Sofortlauf
|
||||
// stehen jetzt an der Aufgabe selbst.
|
||||
// Central taskboard view for scheduled and manual tasks.
|
||||
//
|
||||
// Since #594 the only place for scheduled work. The former tab "Scheduled runs (Jobs)"
|
||||
// edited the legacy config scheduler/toolJobs; after the first migration into the taskboard
|
||||
// no change made there had any effect. Schedule, next and last run, pause and run-now
|
||||
// now live on the task itself.
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleIcon,
|
||||
@@ -18,16 +21,19 @@ import {
|
||||
zeigeMeldung
|
||||
} from "../modules/dom.js";
|
||||
|
||||
const STATUS_TEXT = {
|
||||
Backlog: "Pausiert",
|
||||
Todo: "Bereit",
|
||||
InProgress: "Läuft",
|
||||
InReview: "Im Review",
|
||||
Done: "Erledigt",
|
||||
Canceled: "Verworfen",
|
||||
Blocked: "Blockiert",
|
||||
Archived: "Archiviert"
|
||||
};
|
||||
function holeStatusText(status) {
|
||||
switch (status) {
|
||||
case "Backlog": return t("status.paused");
|
||||
case "Todo": return t("status.ready");
|
||||
case "InProgress": return t("status.running");
|
||||
case "InReview": return t("status.inReview");
|
||||
case "Done": return t("status.done");
|
||||
case "Canceled": return t("status.canceled");
|
||||
case "Blocked": return t("status.blocked");
|
||||
case "Archived": return t("status.archived");
|
||||
default: return status || t("common.unknown");
|
||||
}
|
||||
}
|
||||
|
||||
const STATUS_ART = {
|
||||
Backlog: "warnung",
|
||||
@@ -37,7 +43,6 @@ const STATUS_ART = {
|
||||
Blocked: "warnung"
|
||||
};
|
||||
|
||||
// Laufstatus aus AgentRunStatus (#577/#581 brachten EmptyResponse, Incomplete, RepeatedFailure).
|
||||
const LAUF_ART = {
|
||||
Completed: "erfolg",
|
||||
Truncated: "warnung",
|
||||
@@ -49,7 +54,6 @@ const LAUF_ART = {
|
||||
Failed: "gefahr"
|
||||
};
|
||||
|
||||
const TERMIN_ART = { cron: "Cron", every: "Intervall", at: "Zeitpunkt" };
|
||||
const ABGESCHLOSSEN = ["Done", "Canceled", "Archived"];
|
||||
|
||||
let zeigeAbgeschlossene = false;
|
||||
@@ -73,12 +77,12 @@ export async function rendereAufgaben() {
|
||||
ladeTaskboard();
|
||||
}
|
||||
}),
|
||||
erstelleElement("span", { text: "Erledigte zeigen" })
|
||||
erstelleElement("span", { text: t("tasks.showCompleted") })
|
||||
);
|
||||
|
||||
const neuladen = erstelleElement(
|
||||
"button",
|
||||
{ klasse: "schaltflaeche flach", title: "Neu laden", onclick: () => ladeTaskboard() },
|
||||
{ klasse: "schaltflaeche flach", title: t("common.refresh"), onclick: () => ladeTaskboard() },
|
||||
erstelleIcon(PFADE.aktualisieren, 16)
|
||||
);
|
||||
|
||||
@@ -86,11 +90,11 @@ export async function rendereAufgaben() {
|
||||
"button",
|
||||
{ klasse: "schaltflaeche primaer", onclick: () => oeffneAufgabenDialog(null) },
|
||||
erstelleIcon(PFADE.plus, 16),
|
||||
erstelleElement("span", { text: " Neue Aufgabe" })
|
||||
erstelleElement("span", { text: " " + t("tasks.newTask") })
|
||||
);
|
||||
|
||||
kopf.append(
|
||||
erstelleElement("h1", { text: "Aufgaben" }),
|
||||
erstelleElement("h1", { text: t("tasks.title") }),
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" }, abgeschlossenSchalter, neuladen, neu)
|
||||
);
|
||||
|
||||
@@ -105,7 +109,7 @@ async function ladeTaskboard() {
|
||||
if (!container) return;
|
||||
|
||||
container.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Taskboard…" })
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("common.loading") })
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -115,10 +119,10 @@ async function ladeTaskboard() {
|
||||
|
||||
if (tasks.length === 0) {
|
||||
container.append(
|
||||
erstelleKarte("Keine Aufgaben", [
|
||||
erstelleKarte(t("tasks.empty"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Keine offenen Aufgaben. Wiederkehrende Arbeit legst du über „Neue Aufgabe“ mit einem Termin an."
|
||||
text: "..."
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -130,7 +134,7 @@ async function ladeTaskboard() {
|
||||
container.append(raster);
|
||||
} catch (err) {
|
||||
container.replaceChildren(
|
||||
erstelleKarte("Taskboard nicht verfügbar", [
|
||||
erstelleKarte("Taskboard Error", [
|
||||
erstelleElement("p", { klasse: "gedaempft", text: err.message })
|
||||
])
|
||||
);
|
||||
@@ -138,8 +142,8 @@ async function ladeTaskboard() {
|
||||
}
|
||||
|
||||
function terminText(task) {
|
||||
if (!task.when) return "einmalig";
|
||||
const art = TERMIN_ART[task.when.kind] || task.when.kind;
|
||||
if (!task.when) return t("tasks.scheduleNone");
|
||||
const art = task.when.kind;
|
||||
return `${art} ${task.when.value} (${task.when.timeZone || "UTC"})`;
|
||||
}
|
||||
|
||||
@@ -150,25 +154,25 @@ function erstelleAufgabenKarte(task) {
|
||||
erstelleElement(
|
||||
"div",
|
||||
{ klasse: "task-status-zeile gruppe-horizontal" },
|
||||
erstellePlakette(STATUS_TEXT[task.status] || task.status, STATUS_ART[task.status] || "neutral"),
|
||||
erstellePlakette(holeStatusText(task.status), STATUS_ART[task.status] || "neutral"),
|
||||
erstelleElement("span", { klasse: "gedaempft text-klein", text: `${task.id} · ${task.assignee || "—"}` })
|
||||
)
|
||||
);
|
||||
|
||||
zeilen.push(erstelleElement("p", { klasse: "gedaempft text-klein", text: `Termin: ${terminText(task)}` }));
|
||||
zeilen.push(erstelleElement("p", { klasse: "gedaempft text-klein", text: `${t("tasks.schedule")}: ${terminText(task)}` }));
|
||||
zeilen.push(
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: `Nächster Lauf: ${task.nextRunUtc ? formatiereDatumZeit(task.nextRunUtc) : "—"}`
|
||||
text: `Next: ${task.nextRunUtc ? formatiereDatumZeit(task.nextRunUtc) : "—"}`
|
||||
})
|
||||
);
|
||||
|
||||
const letzter = erstelleElement("p", { klasse: "gedaempft text-klein" }, "Letzter Lauf: ");
|
||||
const letzter = erstelleElement("p", { klasse: "gedaempft text-klein" }, "Last: ");
|
||||
if (task.lastRun) {
|
||||
letzter.append(
|
||||
`${formatiereDatumZeit(task.lastRun.occurredAt)} `,
|
||||
erstellePlakette(task.lastRun.status, LAUF_ART[task.lastRun.status] || "neutral"),
|
||||
` ${task.lastRun.costIsKnown ? formatiereKosten(task.lastRun.costUsd) : "Kosten unbekannt"}`
|
||||
` ${task.lastRun.costIsKnown ? formatiereKosten(task.lastRun.costUsd) : "—"}`
|
||||
);
|
||||
if (task.lastRun.resultRef) letzter.title = task.lastRun.resultRef;
|
||||
} else {
|
||||
@@ -189,9 +193,9 @@ function erstelleAufgabenKarte(task) {
|
||||
aktionen.append(
|
||||
erstelleElement(
|
||||
"button",
|
||||
{ klasse: "schaltflaeche flach klein", title: "Jetzt ausführen", onclick: () => fuehreAus(task) },
|
||||
{ klasse: "schaltflaeche flach klein", title: t("tasks.runNow"), onclick: () => fuehreAus(task) },
|
||||
erstelleIcon(PFADE.abspielen, 14),
|
||||
erstelleElement("span", { text: " Jetzt" })
|
||||
erstelleElement("span", { text: " " + t("tasks.runNow") })
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -200,7 +204,7 @@ function erstelleAufgabenKarte(task) {
|
||||
aktionen.append(
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche flach klein",
|
||||
text: "Pausieren",
|
||||
text: t("tasks.pause"),
|
||||
onclick: () => setzeAktiv(task, false)
|
||||
})
|
||||
);
|
||||
@@ -208,7 +212,7 @@ function erstelleAufgabenKarte(task) {
|
||||
aktionen.append(
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche flach klein",
|
||||
text: "Aktivieren",
|
||||
text: t("tasks.resume"),
|
||||
onclick: () => setzeAktiv(task, true)
|
||||
})
|
||||
);
|
||||
@@ -217,32 +221,32 @@ function erstelleAufgabenKarte(task) {
|
||||
aktionen.append(
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche flach klein",
|
||||
text: "Bearbeiten",
|
||||
text: t("common.edit"),
|
||||
onclick: () => oeffneAufgabenDialog(task)
|
||||
})
|
||||
);
|
||||
zeilen.push(aktionen);
|
||||
|
||||
return erstelleKarte(task.title || `Aufgabe ${task.id}`, zeilen);
|
||||
return erstelleKarte(task.title || `Task ${task.id}`, zeilen);
|
||||
}
|
||||
|
||||
async function fuehreAus(task) {
|
||||
try {
|
||||
const res = await api(`/tasks/${encodeURIComponent(task.id)}/run`, { method: "POST" });
|
||||
zeigeMeldung(res?.message || "Lauf angestoßen.", "erfolg");
|
||||
zeigeMeldung(res?.message || t("tasks.runTriggered"), "erfolg");
|
||||
setTimeout(ladeTaskboard, 1500);
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Nicht gestartet: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(`Error: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
async function setzeAktiv(task, aktiv) {
|
||||
try {
|
||||
const res = await api(`/tasks/${encodeURIComponent(task.id)}/enabled?enabled=${aktiv}`, { method: "PUT" });
|
||||
zeigeMeldung(res?.message || (aktiv ? "Aktiviert." : "Pausiert."), "info");
|
||||
zeigeMeldung(res?.message || (aktiv ? t("tasks.resume") : t("tasks.pause")), "info");
|
||||
await ladeTaskboard();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fehler: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(`Error: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,25 +255,24 @@ async function oeffneAufgabenDialog(task) {
|
||||
try {
|
||||
agenten = (await api("/agents")) || [];
|
||||
} catch {
|
||||
// Ohne Agentenliste bleibt nur „Mensch“ und eine vorhandene Zuordnung.
|
||||
//
|
||||
}
|
||||
|
||||
const formular = erstelleElement("form", { klasse: "formular-raster" });
|
||||
|
||||
const titel = erstelleElement("input", { type: "text", required: true, value: task?.title || "" });
|
||||
|
||||
const beschreibung = erstelleElement("textarea", { rows: "5", placeholder: "Auftrag an den Agenten…" });
|
||||
const beschreibung = erstelleElement("textarea", { rows: "5", placeholder: "..." });
|
||||
beschreibung.value = task?.body || "";
|
||||
|
||||
// #598: Frischer Lauf zuerst und als Vorgabe - im Chatverlauf wächst der Kontext mit jedem Lauf.
|
||||
const zuordnung = erstelleElement("select", { required: true });
|
||||
for (const a of agenten) {
|
||||
zuordnung.append(
|
||||
erstelleElement("option", { value: `@new:${a.agentId}`, text: `${a.displayName} – frischer Lauf` }),
|
||||
erstelleElement("option", { value: `@${a.agentId}`, text: `${a.displayName} – im Chatverlauf` })
|
||||
erstelleElement("option", { value: `@new:${a.agentId}`, text: `${a.displayName} (New Run)` }),
|
||||
erstelleElement("option", { value: `@${a.agentId}`, text: `${a.displayName} (Chat History)` })
|
||||
);
|
||||
}
|
||||
zuordnung.append(erstelleElement("option", { value: "@human", text: "Mensch (kein Agentenlauf)" }));
|
||||
zuordnung.append(erstelleElement("option", { value: "@human", text: "Human (No Agent)" }));
|
||||
if (task?.assignee && ![...zuordnung.options].some((o) => o.value === task.assignee)) {
|
||||
zuordnung.append(erstelleElement("option", { value: task.assignee, text: task.assignee }));
|
||||
}
|
||||
@@ -277,10 +280,10 @@ async function oeffneAufgabenDialog(task) {
|
||||
|
||||
const terminArt = erstelleElement("select");
|
||||
terminArt.append(
|
||||
erstelleElement("option", { value: "", text: "Einmalig" }),
|
||||
erstelleElement("option", { value: "cron", text: "Cron (z.B. stündlich)" }),
|
||||
erstelleElement("option", { value: "every", text: "Intervall" }),
|
||||
erstelleElement("option", { value: "at", text: "Zeitpunkt" })
|
||||
erstelleElement("option", { value: "", text: t("tasks.scheduleNone") }),
|
||||
erstelleElement("option", { value: "cron", text: "Cron" }),
|
||||
erstelleElement("option", { value: "every", text: t("tasks.scheduleInterval") }),
|
||||
erstelleElement("option", { value: "at", text: t("tasks.scheduleAt") })
|
||||
);
|
||||
terminArt.value = task?.when?.kind || "";
|
||||
|
||||
@@ -289,7 +292,7 @@ async function oeffneAufgabenDialog(task) {
|
||||
const standardZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "Europe/Berlin";
|
||||
const zeitzone = erstelleElement("input", { type: "text", value: task?.when?.timeZone || standardZone });
|
||||
|
||||
const abnahme = erstelleElement("textarea", { rows: "2", placeholder: "Woran das Ergebnis gemessen wird (optional)" });
|
||||
const abnahme = erstelleElement("textarea", { rows: "2", placeholder: "..." });
|
||||
abnahme.value = task?.acceptance || "";
|
||||
|
||||
const hinweis = erstelleElement("p", { klasse: "gedaempft text-klein" });
|
||||
@@ -298,45 +301,43 @@ async function oeffneAufgabenDialog(task) {
|
||||
const wiederkehrend = terminArt.value === "cron" || terminArt.value === "every";
|
||||
const imVerlauf = zuordnung.value.startsWith("@") && !zuordnung.value.startsWith("@new") && zuordnung.value !== "@human";
|
||||
hinweis.hidden = !(wiederkehrend && imVerlauf);
|
||||
hinweis.textContent =
|
||||
"⚠ Im Chatverlauf wächst der Kontext mit jedem Lauf – und mit ihm die Kosten je Lauf. " +
|
||||
"Für wiederkehrende Aufgaben ist „frischer Lauf“ meist die bessere Wahl.";
|
||||
hinweis.textContent = "⚠ Notice: Running recurring tasks in chat history increases context and costs.";
|
||||
|
||||
const ohneTermin = terminArt.value === "";
|
||||
terminWert.disabled = ohneTermin;
|
||||
zeitzone.disabled = ohneTermin;
|
||||
terminWert.placeholder =
|
||||
{ cron: "z.B. 0 7-22/3 * * *", every: "z.B. 30m, 2h, 1d", at: "z.B. 2026-09-15T08:00" }[terminArt.value] || "";
|
||||
{ cron: "e.g. 0 7-22/3 * * *", every: "e.g. 30m, 2h, 1d", at: "e.g. 2026-09-15T08:00" }[terminArt.value] || "";
|
||||
};
|
||||
zuordnung.addEventListener("change", aktualisiere);
|
||||
terminArt.addEventListener("change", aktualisiere);
|
||||
aktualisiere();
|
||||
|
||||
formular.append(
|
||||
erstelleElement("label", { text: "Titel:" }), titel,
|
||||
erstelleElement("label", { text: "Auftrag:" }), beschreibung,
|
||||
erstelleElement("label", { text: "Ausführen durch:" }), zuordnung,
|
||||
erstelleElement("label", { text: "Termin:" }), terminArt,
|
||||
erstelleElement("label", { text: "Wert:" }), terminWert,
|
||||
erstelleElement("label", { text: "Zeitzone:" }), zeitzone,
|
||||
erstelleElement("label", { text: "Abnahme:" }), abnahme,
|
||||
erstelleElement("label", { text: t("common.name") + ":" }), titel,
|
||||
erstelleElement("label", { text: t("tasks.taskMessage") + ":" }), beschreibung,
|
||||
erstelleElement("label", { text: t("tasks.agent") + ":" }), zuordnung,
|
||||
erstelleElement("label", { text: t("tasks.scheduleType") + ":" }), terminArt,
|
||||
erstelleElement("label", { text: "Value:" }), terminWert,
|
||||
erstelleElement("label", { text: "Timezone:" }), zeitzone,
|
||||
erstelleElement("label", { text: "Acceptance Criteria:" }), abnahme,
|
||||
erstelleElement("span"), hinweis
|
||||
);
|
||||
|
||||
const abbrechen = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
text: "Abbrechen",
|
||||
text: t("common.cancel"),
|
||||
onclick: () => modal.schliessen()
|
||||
});
|
||||
|
||||
const speichern = erstelleElement("button", {
|
||||
type: "submit",
|
||||
klasse: "schaltflaeche primaer",
|
||||
text: task ? "Speichern" : "Anlegen"
|
||||
text: task ? t("common.save") : t("tasks.newTask")
|
||||
});
|
||||
|
||||
const modal = erstelleModal(task ? "Aufgabe bearbeiten" : "Neue Aufgabe", formular, [abbrechen, speichern]);
|
||||
const modal = erstelleModal(task ? t("common.edit") : t("tasks.createTitle"), formular, [abbrechen, speichern]);
|
||||
|
||||
formular.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -359,11 +360,11 @@ async function oeffneAufgabenDialog(task) {
|
||||
body
|
||||
});
|
||||
modal.schliessen();
|
||||
const text = res?.message || "Gespeichert.";
|
||||
zeigeMeldung(text, text.includes("Hinweis:") ? "warnung" : "erfolg", text.includes("Hinweis:") ? 9000 : 4000);
|
||||
const text = res?.message || t("tasks.saved");
|
||||
zeigeMeldung(text, "erfolg");
|
||||
await ladeTaskboard();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Nicht gespeichert: ${err.message}`, "gefahr", 8000);
|
||||
zeigeMeldung(`Error: ${err.message}`, "gefahr");
|
||||
} finally {
|
||||
speichern.disabled = false;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// totalTokens, cost und timestamp und zeigte überall 0 bzw. "—".
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleKarte,
|
||||
@@ -21,7 +22,7 @@ export async function rendereVerbrauch() {
|
||||
container.replaceChildren();
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "verbrauch-ansicht" });
|
||||
layout.append(erstelleElement("h1", { text: "Token-Verbrauch & Analyse" }));
|
||||
layout.append(erstelleElement("h1", { text: t("usage.title") }));
|
||||
|
||||
const inhaltBereich = erstelleElement("div", { id: "verbrauch-inhalt-bereich" });
|
||||
layout.append(inhaltBereich);
|
||||
@@ -39,7 +40,7 @@ async function ladeVerbrauch() {
|
||||
if (!container) return;
|
||||
|
||||
container.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Verbrauchsdaten…" })
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("usage.loading") })
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -49,10 +50,10 @@ async function ladeVerbrauch() {
|
||||
|
||||
if (!laeufe || laeufe.length === 0) {
|
||||
container.append(
|
||||
erstelleKarte("Keine Daten", [
|
||||
erstelleKarte(t("usage.empty"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Noch keine Token-Verbrauchsdaten aufgezeichnet."
|
||||
text: t("usage.emptyHint")
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -76,10 +77,10 @@ async function ladeVerbrauch() {
|
||||
summeKosten += Number(l.costUsd || 0);
|
||||
if (!l.costIsKnown) unbekannteKosten++;
|
||||
|
||||
const agentKey = l.agentId || "Unbekannt";
|
||||
const agentKey = l.agentId || t("common.unknown");
|
||||
nachAgent.set(agentKey, (nachAgent.get(agentKey) || 0) + tokens);
|
||||
|
||||
const modellKey = l.model || "Unbekannt";
|
||||
const modellKey = l.model || t("common.unknown");
|
||||
nachModell.set(modellKey, (nachModell.get(modellKey) || 0) + tokens);
|
||||
}
|
||||
|
||||
@@ -87,10 +88,10 @@ async function ladeVerbrauch() {
|
||||
const kachelGitter = erstelleElement("div", { klasse: "kachel-gitter" });
|
||||
|
||||
kachelGitter.append(
|
||||
erstelleStatKachel(`Gesamtkosten (${laeufe.length} Läufe)`, formatiereKosten(summeKosten)),
|
||||
erstelleStatKachel("Tokens gesamt", formatiereTokens(summeTokens)),
|
||||
erstelleStatKachel("Eingabe-Tokens (Prompt)", formatiereTokens(summePrompt)),
|
||||
erstelleStatKachel("Ausgabe-Tokens (Completion)", formatiereTokens(summeCompletion))
|
||||
erstelleStatKachel(t("usage.totalCost", { runs: laeufe.length }), formatiereKosten(summeKosten)),
|
||||
erstelleStatKachel(t("usage.totalTokens"), formatiereTokens(summeTokens)),
|
||||
erstelleStatKachel(t("usage.inputTokens"), formatiereTokens(summePrompt)),
|
||||
erstelleStatKachel(t("usage.outputTokens"), formatiereTokens(summeCompletion))
|
||||
);
|
||||
|
||||
const hinweise = [];
|
||||
@@ -98,7 +99,7 @@ async function ladeVerbrauch() {
|
||||
hinweise.push(
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: `${unbekannteKosten} Lauf/Läufe ohne bekannten Preis — deren Kosten sind in der Summe nicht enthalten.`
|
||||
text: t("usage.unknownCostRuns", { count: unbekannteKosten })
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -106,26 +107,26 @@ async function ladeVerbrauch() {
|
||||
// 2. Balkendiagramme (rein CSS/DOM)
|
||||
const diagrammGitter = erstelleElement("div", { klasse: "diagramm-gitter" });
|
||||
|
||||
const agentBalken = erstelleBalkenDiagramm("Tokens nach Agent", nachAgent, summeTokens);
|
||||
const modellBalken = erstelleBalkenDiagramm("Tokens nach Modell", nachModell, summeTokens);
|
||||
const agentBalken = erstelleBalkenDiagramm(t("usage.tokensByAgent"), nachAgent, summeTokens);
|
||||
const modellBalken = erstelleBalkenDiagramm(t("usage.tokensByModel"), nachModell, summeTokens);
|
||||
diagrammGitter.append(agentBalken, modellBalken);
|
||||
|
||||
// 3. Läufe-Tabelle
|
||||
const tabelleKarte = erstelleElement("div", { klasse: "karte" });
|
||||
tabelleKarte.append(erstelleElement("h2", { text: "Letzte Agenten-Läufe" }));
|
||||
tabelleKarte.append(erstelleElement("h2", { text: t("usage.recentRuns") }));
|
||||
|
||||
const tabelle = erstelleElement("table", { klasse: "daten-tabelle" });
|
||||
const thead = erstelleElement("thead");
|
||||
thead.append(
|
||||
erstelleElement("tr", {},
|
||||
erstelleElement("th", { text: "Zeitpunkt" }),
|
||||
erstelleElement("th", { text: "Agent" }),
|
||||
erstelleElement("th", { text: "Modell" }),
|
||||
erstelleElement("th", { text: "Status" }),
|
||||
erstelleElement("th", { text: "Prompt", stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: "Completion", stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: "Gesamt", stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: "Kosten", stil: "text-align: right;" })
|
||||
erstelleElement("th", { text: t("usage.time") }),
|
||||
erstelleElement("th", { text: t("usage.agent") }),
|
||||
erstelleElement("th", { text: t("usage.model") }),
|
||||
erstelleElement("th", { text: t("usage.status") }),
|
||||
erstelleElement("th", { text: t("usage.prompt"), stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: t("usage.completion"), stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: t("usage.total"), stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: t("usage.cost"), stil: "text-align: right;" })
|
||||
)
|
||||
);
|
||||
|
||||
@@ -134,7 +135,7 @@ async function ladeVerbrauch() {
|
||||
for (const lauf of laeufe) {
|
||||
const kosten = lauf.costIsKnown
|
||||
? formatiereKosten(lauf.costUsd)
|
||||
: `${formatiereKosten(lauf.costUsd)} (Preis unbekannt)`;
|
||||
: `${formatiereKosten(lauf.costUsd)} ${t("usage.unknownPrice")}`;
|
||||
|
||||
const tr = erstelleElement("tr");
|
||||
tr.append(
|
||||
@@ -156,7 +157,7 @@ async function ladeVerbrauch() {
|
||||
container.append(kachelGitter, ...hinweise, diagrammGitter, tabelleKarte);
|
||||
} catch (err) {
|
||||
container.replaceChildren(
|
||||
erstelleKarte("Verbrauchsdaten nicht verfügbar", [
|
||||
erstelleKarte(t("usage.notAvailable"), [
|
||||
erstelleElement("p", { klasse: "gedaempft", text: err.message })
|
||||
])
|
||||
);
|
||||
@@ -165,12 +166,14 @@ async function ladeVerbrauch() {
|
||||
|
||||
// ─── Kostenprognose (#595) ───
|
||||
|
||||
const GRUNDLAGE_TEXT = {
|
||||
Laeufe: (n) => `Ø letzte ${n} Läufe`,
|
||||
Agent: (n) => `Ø des Agenten (${n} Läufe)`,
|
||||
Schaetzung: () => "Schätzung – noch kein Lauf",
|
||||
Unbekannt: () => "keine Grundlage"
|
||||
};
|
||||
function getGrundlageText(basis, sample) {
|
||||
switch (basis) {
|
||||
case "Laeufe": return t("usage.basisRuns", { n: sample });
|
||||
case "Agent": return t("usage.basisAgent", { n: sample });
|
||||
case "Schaetzung": return t("usage.basisEstimate");
|
||||
default: return t("usage.basisUnknown");
|
||||
}
|
||||
}
|
||||
|
||||
async function erstellePrognoseKarte() {
|
||||
let p;
|
||||
@@ -183,54 +186,57 @@ async function erstellePrognoseKarte() {
|
||||
|
||||
const kacheln = erstelleElement("div", { klasse: "kachel-gitter" });
|
||||
kacheln.append(
|
||||
erstelleStatKachel("Prognose je Tag", formatiereKosten(p.totalPerDay)),
|
||||
erstelleStatKachel("Prognose je Monat (30 Tage)", formatiereKosten(p.totalPerMonth)),
|
||||
erstelleStatKachel("davon Aufgaben je Tag", formatiereKosten(p.tasksPerDay)),
|
||||
erstelleStatKachel("sonstige Läufe je Tag (Ø 7 Tage)", formatiereKosten(p.otherPerDay))
|
||||
erstelleStatKachel(t("usage.forecastDay"), formatiereKosten(p.totalPerDay)),
|
||||
erstelleStatKachel(t("usage.forecastMonth"), formatiereKosten(p.totalPerMonth)),
|
||||
erstelleStatKachel(t("usage.tasksDay"), formatiereKosten(p.tasksPerDay)),
|
||||
erstelleStatKachel(t("usage.otherDay"), formatiereKosten(p.otherPerDay))
|
||||
);
|
||||
|
||||
const hinweise = [];
|
||||
for (const w of p.budgetWarnings || []) {
|
||||
hinweise.push(erstelleElement("div", {
|
||||
klasse: "hinweis-box warnung",
|
||||
text: `${w.agentId}: Die geplanten Aufgaben kosten laut Prognose ${formatiereKosten(w.costPerDay)} am Tag, `
|
||||
+ `das Tagesbudget erlaubt ${formatiereKosten(w.budgetPerDay)}. Die späten Läufe eines Tages werden abgelehnt.`
|
||||
text: t("usage.budgetWarning", {
|
||||
agent: w.agentId,
|
||||
cost: formatiereKosten(w.costPerDay),
|
||||
budget: formatiereKosten(w.budgetPerDay)
|
||||
})
|
||||
}));
|
||||
}
|
||||
if (p.withoutBasis > 0) {
|
||||
hinweise.push(erstelleElement("p", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: `${p.withoutBasis} Aufgabe(n) ohne Grundlage (kein Lauf, kein Modellpreis) – nicht in der Summe enthalten.`
|
||||
text: t("usage.withoutBasis", { count: p.withoutBasis })
|
||||
}));
|
||||
}
|
||||
if (Number(p.oneOffOpen) > 0) {
|
||||
hinweise.push(erstelleElement("p", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: `Offene einmalige Aufgaben kosten zusätzlich etwa ${formatiereKosten(p.oneOffOpen)}.`
|
||||
text: t("usage.oneOffOpen", { cost: formatiereKosten(p.oneOffOpen) })
|
||||
}));
|
||||
}
|
||||
hinweise.push(erstelleElement("p", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: "Grundlage je Aufgabe: Mittelwert ihrer letzten Läufe, sonst der des Agenten, sonst eine Schätzung aus Promptlänge und Listenpreis (eher zu hoch)."
|
||||
text: t("usage.basisHint")
|
||||
}));
|
||||
|
||||
const tabelle = erstelleElement("table", { klasse: "daten-tabelle" });
|
||||
tabelle.append(
|
||||
erstelleElement("thead", {},
|
||||
erstelleElement("tr", {},
|
||||
erstelleElement("th", { text: "Aufgabe" }),
|
||||
erstelleElement("th", { text: "Agent" }),
|
||||
erstelleElement("th", { text: "Läufe/Tag", stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: "je Lauf", stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: "Grundlage" }),
|
||||
erstelleElement("th", { text: "je Tag", stil: "text-align: right;" })
|
||||
erstelleElement("th", { text: t("usage.task") }),
|
||||
erstelleElement("th", { text: t("usage.agent") }),
|
||||
erstelleElement("th", { text: t("usage.runsDay"), stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: t("usage.perRun"), stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: t("usage.basis") }),
|
||||
erstelleElement("th", { text: t("usage.perDay"), stil: "text-align: right;" })
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const tbody = erstelleElement("tbody");
|
||||
for (const a of p.tasks || []) {
|
||||
const grundlage = (GRUNDLAGE_TEXT[a.basis] || (() => a.basis))(a.sample);
|
||||
const grundlage = getGrundlageText(a.basis, a.sample);
|
||||
tbody.append(
|
||||
erstelleElement("tr", {},
|
||||
erstelleElement("td", { text: a.oneOffOpen ? `${a.title} (einmalig)` : a.title }),
|
||||
@@ -244,7 +250,7 @@ async function erstellePrognoseKarte() {
|
||||
}
|
||||
tabelle.append(tbody);
|
||||
|
||||
return erstelleKarte("Kostenprognose", [kacheln, ...hinweise, tabelle]);
|
||||
return erstelleKarte(t("usage.forecastTitle"), [kacheln, ...hinweise, tabelle]);
|
||||
}
|
||||
|
||||
function erstelleStatKachel(titel, wert) {
|
||||
|
||||
Reference in New Issue
Block a user