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:
Richard
2026-09-24 09:43:51 +02:00
co-authored by Claude Opus 5.5
parent 69718122f3
commit ad71682c2d
97 changed files with 4103 additions and 1928 deletions
+23 -10
View File
@@ -30,7 +30,7 @@ import { rendereInstanzen, stoppePolling as stoppeInstanzenPolling } from "./vie
import { rendereEinstellungen } from "./views/settings.js";
import { rendereSicherung } from "./views/backup.js";
import { rendereAgentenChats, trenneAgentenChatsStream } from "./views/agent-chats.js";
import { loadLanguage, translateDocument } from "./modules/i18n.js";
import { loadLanguage, translateDocument, setLanguage, currentLanguage, t } from "./modules/i18n.js";
import { renderUsageRuns } from "./views/usage-runs.js";
import { renderUsageTimeline } from "./views/usage-timeline.js";
import { renderRunDetail } from "./views/run-detail.js";
@@ -112,6 +112,18 @@ document.querySelectorAll(".nav-link").forEach((link) => {
});
});
// Sprache umschalten
const spracheSchalter = document.getElementById("sprache-schalter");
spracheSchalter?.addEventListener("change", async (e) => {
await setLanguage(e.target.value);
});
window.addEventListener("clawd:language-changed", () => {
if (spracheSchalter) spracheSchalter.value = currentLanguage();
const hash = window.location.hash || "#/chat";
navigiere(hash);
});
// Farbschema umschalten
themeSchalter?.addEventListener("click", () => {
wechsleTheme();
@@ -145,10 +157,10 @@ beiVerbindungsStatus((istOffen) => {
if (!verbindungPunkt) return;
if (istOffen) {
verbindungPunkt.classList.add("offen");
verbindungPunkt.title = "Ereignisstrom: verbunden";
verbindungPunkt.title = t("header.eventStreamConnected");
} else {
verbindungPunkt.classList.remove("offen");
verbindungPunkt.title = "Ereignisstrom: getrennt";
verbindungPunkt.title = t("header.eventStreamDisconnected");
}
});
@@ -190,7 +202,7 @@ function aktualisiereInstanzPlakette() {
kopfInstanzStatus.replaceWith(neuePlakette);
kopfInstanzStatus = neuePlakette;
} else {
const text = laufende.length > 1 ? `${laufende.length} laufen` : "Keine aktiv";
const text = laufende.length > 1 ? t("header.multipleRunning", { count: laufende.length }) : t("header.noneActive");
const art = laufende.length > 1 ? "warnung" : "neutral";
const neuePlakette = erstellePlakette(text, art);
neuePlakette.id = "kopf-instanz-status";
@@ -203,10 +215,10 @@ function aktualisiereInstanzPlakette() {
if (aktive && aktive.zustand !== "Running") {
startBtn.hidden = false;
startBtn.disabled = false;
startBtn.textContent = "Starten";
startBtn.textContent = t("header.startInstance");
startBtn.onclick = async () => {
startBtn.disabled = true;
startBtn.textContent = "Startet…";
startBtn.textContent = t("header.starting");
try {
const res = await api(`/verwaltung/instanzen/${encodeURIComponent(aktive.id)}/start`, {
method: "POST"
@@ -223,7 +235,7 @@ function aktualisiereInstanzPlakette() {
zeigeMeldung(`Start fehlgeschlagen: ${err.message}`, "gefahr");
}
startBtn.disabled = false;
startBtn.textContent = "Starten";
startBtn.textContent = t("header.startInstance");
}
};
} else {
@@ -266,7 +278,7 @@ async function aktualisiereInstanzenListe(vorhandene = null) {
kopfInstanzSelect.replaceChildren();
const placeholderOpt = erstelleElement("option", {
value: "",
text: bekannteInstanzen.length === 0 ? "Keine Instanzen" : "— Instanz wählen —"
text: bekannteInstanzen.length === 0 ? t("header.noInstances") : t("header.selectInstancePlaceholder")
});
kopfInstanzSelect.append(placeholderOpt);
@@ -304,9 +316,9 @@ async function aktualisiereKopfStatus() {
} catch (err) {
if (kopfOpenRouter) {
if (err.instanzUnerreichbar) {
kopfOpenRouter.textContent = "Instanz offline (502)";
kopfOpenRouter.textContent = t("header.instanceOffline");
} else if (err.instanzKonflikt) {
kopfOpenRouter.textContent = "Instanz wählen (409)";
kopfOpenRouter.textContent = t("header.selectInstancePrompt");
} else {
kopfOpenRouter.textContent = "";
}
@@ -366,6 +378,7 @@ async function starteHauptAnwendung(instanzen) {
// #661: language packs before the first render
await loadLanguage();
translateDocument();
if (spracheSchalter) spracheSchalter.value = currentLanguage();
// Router initialisieren
initialisiereRouter();
+568 -26
View File
@@ -6,6 +6,496 @@
"common.loading": "Lade…",
"common.previous": "Zurück",
"common.next": "Weiter",
"common.save": "Speichern",
"common.cancel": "Abbrechen",
"common.delete": "Löschen",
"common.edit": "Bearbeiten",
"common.refresh": "Aktualisieren",
"common.close": "Schließen",
"common.discard": "Verwerfen",
"common.apply": "Anwenden",
"common.reset": "Zurücksetzen",
"common.search": "Suche",
"common.actions": "Aktionen",
"common.status": "Status",
"common.name": "Name",
"common.description": "Beschreibung",
"common.ready": "Bereit",
"common.none": "Keine",
"common.unknown": "Unbekannt",
"common.all": "Alle",
"common.details": "Details",
"common.language": "Sprache",
"common.logout": "Abmelden",
"common.confirm": "Bestätigen",
"common.yes": "Ja",
"common.no": "Nein",
"status.running": "Läuft",
"status.stopped": "Gestoppt",
"status.silent": "Verstummt",
"status.crashed": "Abgestürzt",
"status.unknown": "Unbekannt",
"status.ready": "Bereit",
"status.busy": "Beschäftigt",
"status.paused": "Pausiert",
"status.done": "Erledigt",
"status.failed": "Fehlgeschlagen",
"status.blocked": "Blockiert",
"status.inReview": "Im Review",
"status.canceled": "Verworfen",
"status.archived": "Archiviert",
"header.instanceLabel": "Instanz:",
"header.noInstance": "(Keine Instanz)",
"header.switchInstance": "Aktive Instanz wechseln",
"header.startInstance": "Starten",
"header.startInstanceTooltip": "Ausgewählte Instanz jetzt starten",
"header.toggleTheme": "Farbschema umschalten",
"header.eventStreamConnected": "Ereignisstrom: verbunden",
"header.eventStreamDisconnected": "Ereignisstrom: getrennt",
"header.multipleRunning": "{count} laufen",
"header.noneActive": "Keine aktiv",
"header.starting": "Startet…",
"header.noInstances": "Keine Instanzen",
"header.selectInstancePlaceholder": "— Instanz wählen —",
"header.instanceOffline": "Instanz offline (502)",
"header.selectInstancePrompt": "Instanz wählen (409)",
"nav.toggleSidebar": "Seitenleiste ein-/ausklappen",
"nav.group.work": "Arbeiten",
"nav.chat": "Chat",
"nav.agents": "Agenten",
"nav.tasks": "Aufgaben",
"nav.services": "Dienste",
"nav.group.analytics": "Analyse",
"nav.usage": "Tokennutzung",
"nav.agentChats": "Agenten-Chats",
"nav.group.system": "System",
"nav.instances": "Instanzen",
"nav.staging": "Freigaben",
"nav.logs": "Protokoll",
"nav.backup": "Sicherung",
"nav.settings": "Einstellungen",
"nav.info": "Info",
"login.prompt": "Bitte gib das Zugangstoken des Instanzmanagers ein (webUiToken in manager.json).",
"login.tokenPlaceholder": "Zugangstoken (mind. 20 Zeichen)",
"login.submit": "Anmelden",
"login.invalidToken": "Token ungültig oder kürzer als 20 Zeichen.",
"login.error": "Anmeldefehler: {error}",
"chat.agentLabel": "Agent: ",
"chat.noAgents": "Keine Agenten vorhanden",
"chat.wake": "Wecken",
"chat.wakeTooltip": "Agenten manuell ausführen / wecken",
"chat.clearHistory": "Verlauf leeren",
"chat.clearHistoryTooltip": "Chatverlauf dieses Agenten leeren",
"chat.cancel": "Abbrechen",
"chat.cancelTooltip": "Laufenden Lauf abbrechen",
"chat.placeholder": "Nachricht an den Agenten eingeben (Enter zum Senden, Umschalt+Enter für neue Zeile)…",
"chat.send": "Senden",
"chat.sendTooltip": "Nachricht senden",
"chat.loadingHistory": "Lade Verlauf…",
"chat.emptyHistory": "Noch keine Nachrichten im Verlauf vorhanden.",
"chat.historyLoadError": "Verlauf konnte nicht geladen werden: {error}",
"chat.sendError": "Nachricht konnte nicht gesendet werden: {error}",
"chat.cancelConfirm": "Lauf wurde abgebrochen.",
"chat.cancelError": "Abbruch fehlgeschlagen: {error}",
"chat.wakeSuccess": "Agent wurde manuell gestartet.",
"chat.wakeError": "Starten fehlgeschlagen: {error}",
"chat.clearConfirmPrompt": "Soll der gesamte Chatverlauf dieses Agenten unwiderruflich gelöscht werden?",
"chat.cleared": "Chatverlauf geleert.",
"chat.clearError": "Leeren fehlgeschlagen: {error}",
"chat.role.user": "Benutzer",
"chat.role.system": "System",
"chat.role.tool": "Werkzeug",
"agents.title": "Agenten",
"agents.newAgent": "Neuer Agent",
"agents.loading": "Lade Agenten…",
"agents.empty": "In dieser Instanz sind noch keine Agenten eingerichtet.",
"agents.createFirst": "Ersten Agenten anlegen",
"agents.id": "Kennung (ID)",
"agents.model": "Modell",
"agents.toolsActive": "{count} Werkzeuge aktiv",
"agents.details": "Agent-Details",
"agents.selectPrompt": "Wähle links einen Agenten aus.",
"agents.chat": "Chatten",
"agents.chatTooltip": "Mit diesem Agenten chatten",
"agents.delete": "Löschen",
"agents.deleteTooltip": "Diesen Agenten löschen",
"agents.name": "Name",
"agents.description": "Beschreibung",
"agents.availableTools": "Verfügbare Werkzeuge",
"agents.noTools": "Keine Werkzeuge für diesen Agenten konfiguriert.",
"agents.configuration": "Konfiguration",
"agents.configTooltip": "Werkzeug-Einstellungen einsehen",
"agents.toolToggled": "Werkzeug '{tool}' {status}.",
"agents.enabled": "aktiviert",
"agents.disabled": "deaktiviert",
"agents.saveFailed": "Änderung fehlgeschlagen: {error}",
"agents.newAgentTitle": "Neuen Agenten anlegen",
"agents.idLabel": "Agent-ID (eindeutiger Schlüssel):",
"agents.idPlaceholder": "z.B. scout",
"agents.nameLabel": "Anzeigename:",
"agents.namePlaceholder": "z.B. Scout Agent",
"agents.modelLabel": "Modell (OpenRouter):",
"agents.descLabel": "Beschreibung:",
"agents.descPlaceholder": "Optionale Beschreibung des Agenten…",
"agents.createButton": "Agent anlegen",
"agents.createdSuccess": "Agent '{name}' wurde erfolgreich angelegt.",
"agents.createFailed": "Fehler beim Anlegen: {error}",
"agents.deleteConfirm": "Soll der Agent '{name}' ({id}) wirklich gelöscht werden?",
"agents.deleted": "Agent '{name}' wurde gelöscht.",
"agents.deleteFailed": "Löschen fehlgeschlagen: {error}",
"agents.settingsTitle": "Einstellungen: {tool} ({agent})",
"agents.loadingSettings": "Lade Konfiguration…",
"agents.secretHint": "Geheime Werte (Passwörter, Schlüssel) werden vom Server maskiert ausgegeben.",
"agents.noSettings": "Keine individuellen Einstellungen hinterlegt.",
"agents.loadFailed": "Laden fehlgeschlagen: {error}",
"agents.rights": "Rechte",
"agents.noOperations": "Dieses Werkzeug kennt keine einzelnen Operationen. Es gilt die Standardregel.",
"agents.configuredHint": "Erlaubte Operationen laufen ohne Rückfrage, alle anderen werden einzeln zur Freigabe vorgelegt, verbotene gar nicht erst.",
"agents.notConfiguredHint": "Noch keine Rechte gesetzt: Es gilt die Vorgabe des Werkzeugs. Mit dem Speichern werden die Einstellungen unten verbindlich.",
"agents.operation": "Operation",
"agents.setting": "Einstellung",
"agents.effect": "Wirkung",
"agents.allowed": "erlaubt",
"agents.singleApproval": "einzeln freigeben",
"agents.forbidden": "verboten",
"agents.alwaysApprovalReason": "Änderungen an Agenten brauchen immer eine Freigabe.",
"agents.saveRights": "Rechte speichern",
"agents.rightsSaved": "Rechte gespeichert.",
"agents.effectAuto": "läuft",
"agents.effectApprove": "einzeln freigeben",
"agents.effectDeny": "gesperrt",
"agents.standingApproval": "Dauerfreigabe #{id}",
"tasks.title": "Aufgaben",
"tasks.newTask": "Neue Aufgabe",
"tasks.showCompleted": "Abgeschlossene anzeigen",
"tasks.empty": "Keine Aufgaben vorhanden",
"tasks.createTitle": "Aufgabe erstellen",
"tasks.agent": "Agent",
"tasks.taskMessage": "Aufgabenbeschreibung / Nachricht",
"tasks.schedule": "Zeitplan",
"tasks.scheduleType": "Planungsart",
"tasks.scheduleNone": "Keine (Nur manuell)",
"tasks.scheduleCron": "Cron-Ausdruck",
"tasks.scheduleInterval": "Intervall",
"tasks.scheduleAt": "Fester Zeitpunkt",
"tasks.runNow": "Jetzt ausführen",
"tasks.pause": "Pausieren",
"tasks.resume": "Fortsetzen",
"tasks.delete": "Löschen",
"tasks.saved": "Aufgabe gespeichert.",
"tasks.deleted": "Aufgabe gelöscht.",
"tasks.runTriggered": "Lauf ausgelöst.",
"services.title": "Dienste",
"services.newService": "Neuer Dienst",
"services.empty": "Keine Hintergrunddienste konfiguriert",
"services.serviceName": "Dienstname",
"services.serviceType": "Typ",
"services.port": "Port",
"services.active": "Aktiv",
"services.start": "Starten",
"services.stop": "Beenden",
"services.restart": "Neustarten",
"backup.title": "Datensicherung & Wiederherstellung",
"backup.refresh": "Aktualisieren",
"backup.refreshTooltip": "Sicherungen neu laden",
"backup.newBackup": "Neue Sicherung",
"backup.newBackupTooltip": "Neue Sicherung erstellen",
"backup.noInstance": "Keine Instanz ausgewählt",
"backup.selectInstancePrompt": "Bitte wähle oben im Kopfbereich eine Instanz aus, um deren Sicherungen zu verwalten.",
"backup.loading": "Lade Sicherungen…",
"backup.backupDir": "Sicherungsverzeichnis",
"backup.backupDirHint": "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.",
"backup.existingBackups": "Vorhandene Sicherungen ({count})",
"backup.noBackups": "Für diese Instanz gibt es noch keine Sicherung. Lege über die Schaltfläche oben eine an.",
"backup.restore": "Wiederherstellen",
"backup.restoreTooltip": "Diese Sicherung wiederherstellen",
"backup.deleteTooltip": "Sicherungsdatei unwiderruflich löschen",
"backup.archiveFile": "Archivdatei",
"backup.createdAt": "Erstellt am",
"backup.size": "Größe",
"backup.protection": "Schutz",
"backup.files": "Dateien",
"backup.actions": "Aktionen",
"backup.fileCount": "{count} Dateien",
"backup.foreignLicense": "Fremde Lizenz",
"backup.unreadable": "Unlesbar",
"backup.licenseProtection": "Lizenz ({count} Zugangsdaten)",
"backup.passphraseProtection": "Passphrase ({count})",
"backup.unencryptedProtection": "Unverschlüsselt, ohne Zugangsdaten",
"backup.dialogTitle": "Neue Sicherung erstellen",
"backup.dialogHint": "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.",
"backup.includeChats": "Chatverläufe sichern",
"backup.includeLogs": "Protokolldateien (Logs) einschließen",
"backup.createButton": "Sicherung erstellen",
"backup.creating": "Erstelle Sicherung…",
"backup.created": "Sicherung erstellt.",
"backup.createFailed": "Fehler beim Sichern: {error}",
"backup.restoreTitle": "Sicherung wiederherstellen: {file}",
"backup.overwriteWarningTitle": "Achtung: Überschreiben bestehender Daten",
"backup.overwriteWarningText": "Die Wiederherstellung überschreibt die aktuellen Konfigurationen und Daten der Instanz mit dem Stand vom {date}.",
"backup.instanceStopNotice": "Die Instanz wird dafür geordnet angehalten und danach wieder gestartet.",
"backup.passphraseLabel": "Passphrase für Zugangsdaten",
"backup.passphrasePlaceholder": "Passphrase der Sicherung…",
"backup.passphraseRequired": "Bitte gib die Passphrase an.",
"backup.restoreNow": "Jetzt wiederherstellen",
"backup.restoring": "Stelle wieder her…",
"backup.restoreSuccess": "Wiederherstellung abgeschlossen.",
"backup.restoreFailed": "Wiederherstellung fehlgeschlagen: {error}",
"backup.foreignLicenseTitle": "Lizenzschlüssel der Quelle erforderlich",
"backup.sourceLicenseLabel": "Lizenzschlüssel der Quelle",
"backup.sourceLicenseHint": "Groß- und Kleinschreibung sowie Leerzeichen spielen keine Rolle.",
"backup.sourceLicensePlaceholder": "Lizenzschlüssel der Installation, von der die Sicherung stammt…",
"backup.restoreWithKey": "Mit diesem Schlüssel wiederherstellen",
"backup.keyMismatch": "Dieser Lizenzschlüssel passt nicht zu der Sicherung.",
"backup.deleteTitle": "Sicherung löschen",
"backup.deleteConfirm": "Soll die Sicherungsdatei '{file}' wirklich unwiderruflich gelöscht werden?",
"backup.deleted": "Sicherung gelöscht.",
"backup.deleteFailed": "Löschen fehlgeschlagen: {error}",
"backup.ftpTitle": "FTP-Upload der Sicherungen",
"backup.ftpLoading": "Lade FTP-Einstellungen…",
"backup.ftpHint": "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.",
"backup.ftpAutoUpload": "Fertige Sicherungen automatisch hochladen",
"backup.ftpServer": "Server",
"backup.ftpPort": "Port",
"backup.ftpUser": "Benutzer",
"backup.ftpPassword": "Kennwort",
"backup.ftpPasswordSaved": "(gespeichert — leer lassen für unverändert)",
"backup.ftpDirectory": "Zielordner",
"backup.ftpDirectoryHint": "Darunter bekommt jede Instanz einen eigenen Ordner.",
"backup.ftpKeepCount": "Sicherungen je Instanz behalten",
"backup.ftpKeepCountHint": "Ältere werden auf dem Server gelöscht.",
"backup.ftpRequireTls": "Nur mit TLS verbinden",
"backup.ftpTest": "Verbindung testen",
"backup.ftpTestFailed": "Test fehlgeschlagen: {error}",
"backup.ftpSaved": "Gespeichert.",
"backup.ftpSaveFailed": "Speichern fehlgeschlagen: {error}",
"logs.title": "Protokoll",
"logs.connecting": "Verbinde…",
"logs.allInstances": "Alle Instanzen",
"logs.allModules": "Alle Module",
"logs.clear": "Leeren",
"logs.pause": "Pausieren",
"logs.resume": "Fortsetzen",
"logs.paused": "Pausiert",
"logs.autoScroll": "Autoscroll",
"logs.empty": "Noch keine Protokolleinträge vorhanden.",
"settings.title": "Einstellungen & Systemarchitektur",
"settings.intro": "Übersicht der Konfiguration nach dem Instanzmanager-Konzept: Strikte Trennung zwischen globaler Manager-Ebene (Installation) und isolierter Instanz-Ebene.",
"settings.managerLevel": "Instanzmanager",
"settings.managerBadge": "Manager-Ebene · Global",
"settings.managerIntro": "Gilt für alle Instanzen und die Weboberfläche. Gespeichert in manager.json im Anwendungs-Hauptverzeichnis.",
"settings.webUiAndNet": "Weboberfläche & Netzwerk",
"settings.configFile": "Konfigurationsdatei",
"settings.webUiPort": "WebUI-Port",
"settings.webUiPortDefault": "7000 (Vorgabe, webUiPort)",
"settings.networkBinding": "Netzwerk-Bindung",
"settings.networkBindingValue": "loopback (127.0.0.1, webUiBinding)",
"settings.accessProtection": "Zugangsschutz",
"settings.tokenActive": "Token aktiv (≥ 20 Zeichen)",
"settings.licenseAndUpdates": "Lizenz & Updates",
"settings.licenseScope": "Lizenz-Geltung",
"settings.licenseScopeValue": "Installationsebene (E-M3: Beliebig viele Instanzen)",
"settings.licenseKey": "Lizenzschlüssel",
"settings.licenseEncrypted": "●●●●-●●●●-●●●●-●●●● (verschlüsselt)",
"settings.updateChannel": "Update-Kanal",
"settings.updateChannelValue": "prod (Offizielle Release-Versionen)",
"settings.updateCheck": "Update-Prüfung",
"settings.updateCheckValue": "Zentral auf Manager-Ebene",
"settings.dcAndOversight": "Deploymentcenter & Aufsicht",
"settings.dcMasterToken": "Master-Token",
"settings.dcMasterTokenValue": "●●●●●●●● (erzeugt Sub-Token je Instanz)",
"settings.desktopWindows": "Desktop-Fenster",
"settings.desktopWindowsValue": "Aktiv (jede Instanz hat ein eigenes Fenster)",
"settings.crashProtection": "Absturz-Schutz",
"settings.crashProtectionValue": "Max. 5 Neustarts / 10 Minuten",
"settings.instanceSectionTitle": "Aktive Instanz: {name}",
"settings.instanceSectionTitleNone": "Instanzbezogene Einstellungen",
"settings.instanceBadge": "Instanz-Ebene · Isoliert",
"settings.noInstanceActive": "Keine Instanz aktiv",
"settings.instanceIntro": "Gilt ausschließlich für die aktuell ausgewählte Instanz. Alle Pfade, Protokolle, Agenten und Budgets sind strikt von anderen Instanzen isoliert.",
"settings.noInstanceSelected": "Keine Instanz ausgewählt",
"settings.noInstancePrompt": "Bitte wähle oben im Kopfbereich eine aktive Instanz aus oder wechsle in den Bereich 'Instanzen', um eine Instanz zu starten oder auszuwählen.",
"settings.toInstancesOverview": "Zur Instanzen-Übersicht",
"settings.identityAndProcess": "Identität & Prozess",
"settings.instanceName": "Instanzname",
"settings.instanceId": "Instanz-ID",
"settings.autostartByManager": "Autostart durch Manager",
"settings.autostartActive": "Aktiv (Am Laufen halten)",
"settings.autostartInactive": "Inaktiv (Manueller Start)",
"settings.communication": "Kommunikation",
"settings.communicationValue": "HTTP über Named Pipe (Windows) / Socket (Linux)",
"settings.isolatedPaths": "Isolierte Verzeichnisse",
"settings.logsPath": "Protokolle (Logs)",
"settings.backupsPath": "Datensicherungen",
"settings.stateDbPath": "Zustandsdatenbank",
"settings.stateDbValue": "Instances/{name}/state.db (exklusiv)",
"settings.workspacePath": "Arbeitsbereich (Shared)",
"settings.openRouterCard": "Modellanbindung (OpenRouter)",
"settings.openRouterStatus": "OpenRouter-Status",
"settings.connected": "Verbunden",
"settings.offline": "Offline",
"settings.statusMessage": "Statusmeldung",
"settings.credits": "Guthaben / Credits",
"settings.apiKey": "API-Schlüssel",
"settings.apiKeyMasked": "In InstanceSettings.json dieser Instanz maskiert hinterlegt",
"settings.openRouterNotRunning": "Instanz läuft nicht. Daten werden nach dem Start geladen.",
"settings.openRouterNoKey": "Kein Schlüssel hinterlegt oder Dienst nicht erreichbar.",
"settings.runtimeAndSubsystems": "Laufzeit & Teilsysteme",
"settings.agentEngine": "Agent Engine",
"settings.scheduler": "Scanner / Scheduler",
"settings.stagingService": "Freigabestelle (Staging)",
"settings.dcSubToken": "Deploymentcenter Sub-Token",
"settings.subsystemsLoading": "Laufzeitdaten werden abgefragt…",
"settings.subsystemsStopped": "Instanz ist beendet — Teilsysteme ruhen.",
"settings.save": "Einstellungen speichern",
"settings.discard": "Verwerfen",
"settings.saved": "Einstellungen erfolgreich gespeichert.",
"settings.saveError": "Fehler beim Speichern der Einstellungen: {error}",
"info.title": "System & Status",
"info.noInstanceSelected": "Keine Instanz ausgewählt",
"info.noInstanceHint": "Wähle oben im Kopfbereich eine aktive Instanz aus oder wechsle in den Bereich 'Instanzen', um eine Instanz zu starten.",
"info.loading": "Lade Systemdaten…",
"info.instanceAndVersion": "Instanz & Version",
"info.instanceName": "Instanzname",
"info.instanceId": "Instanz-ID",
"info.version": "Version",
"info.build": "Build",
"info.environment": "Umgebung",
"info.dataDirectory": "Datenverzeichnis",
"info.subsystems": "Teilsysteme",
"info.openRouterStatus": "OpenRouter & Guthaben",
"instances.title": "Instanzverwaltung",
"instances.refresh": "Aktualisieren",
"instances.newInstance": "Neue Instanz",
"instances.activeBadge": "Aktiv im WebUI",
"instances.selectActive": "Als aktiv wählen",
"instances.start": "Starten",
"instances.stop": "Beenden",
"instances.bringToFront": "Fenster nach vorn",
"instances.createTitle": "Neue Instanz anlegen",
"instances.instanceOfflineError": "Die Instanz ist nicht erreichbar.",
"instances.noInstanceActiveError": "Keine Instanz aktiv. Unter 'Instanzen' starten oder auswählen.",
"instances.instanceNameLabel": "Instanz-Name",
"instances.instanceNamePlaceholder": "z.B. production-1",
"instances.dailyBudgetUsd": "Tagesbudget ($ USD)",
"instances.dailyBudgetTokens": "Tagesbudget (Tokens)",
"instances.autostart": "Autostart durch Manager",
"instances.created": "Instanz erfolgreich angelegt.",
"instances.createError": "Fehler beim Anlegen der Instanz: {error}",
"staging.title": "Freigabestelle",
"staging.refresh": "Aktualisieren",
"staging.loading": "Lade Freigaben…",
"staging.empty": "Keine offenen Freigaben",
"staging.emptyHint": "Aktuell warten keine Werkzeug-Aufrufe auf eine manuelle Bestätigung.",
"staging.approve": "Freigeben",
"staging.reject": "Ablehnen",
"staging.callId": "Aufruf-ID",
"staging.agent": "Agent",
"staging.tool": "Werkzeug",
"staging.time": "Zeitpunkt",
"staging.frozenArgs": "Eingefrorene Argumente:",
"staging.suggestHint": "Diese Operation von {agent} wurde in den letzten 14 Tagen schon {approvals}-mal freigegeben. Soll {tool}.{action} für diesen Agenten künftig ohne Rückfrage laufen?",
"staging.allowSimilar": "Gleichartiges künftig erlauben: {agent} darf {tool}.{action} ab jetzt ohne Rückfrage (widerrufbar unten unter „Dauerfreigaben“).",
"staging.notActive": "Freigabestelle nicht aktiv",
"staging.notActiveHint": "In dieser Instanz ist keine Freigabestelle eingerichtet.",
"staging.approved": "Aufruf freigegeben{similar}: {message}",
"staging.similarSuffix": " und künftig erlaubt",
"staging.executedSuccess": "Erfolgreich ausgeführt",
"staging.approveFailed": "Freigabe fehlgeschlagen: {error}",
"staging.standingTitle": "Dauerfreigaben",
"staging.standingEmpty": "Keine. Beim Freigeben lässt sich „Gleichartiges künftig erlauben“ wählen; dauerhafte Rechte stehen sonst in der Werkzeugkonfiguration des Agenten.",
"staging.granted": "Erteilt",
"staging.occasion": "Anlass",
"staging.revoke": "Widerrufen",
"staging.revokeConfirm": "Dauerfreigabe widerrufen? {agent} muss {tool}.{action} dann wieder einzeln vorlegen.",
"staging.revoked": "Widerrufen.",
"staging.revokeFailed": "Widerruf fehlgeschlagen: {error}",
"staging.rejectTitle": "Werkzeug-Aufruf ablehnen",
"staging.rejectReasonLabel": "Begründung (optional):",
"staging.rejectReasonPlaceholder": "Begründung für die Ablehnung an den Agenten…",
"staging.rejectButton": "Aufruf ablehnen",
"staging.rejected": "Aufruf wurde abgelehnt.",
"staging.rejectFailed": "Ablehnen fehlgeschlagen: {error}",
"agentChats.title": "Agenten-Kommunikation (A2A)",
"agentChats.allAgents": "Alle Agenten",
"agentChats.filterTooltip": "Nach beteiligtem Agenten filtern (Absender oder Empfänger)",
"agentChats.fromTooltip": "Nachrichten ab diesem Tag",
"agentChats.toTooltip": "Nachrichten bis einschließlich diesem Tag",
"agentChats.searchPlaceholder": "Volltext…",
"agentChats.searchTooltip": "Suche im Nachrichtentext und in den Agentennamen",
"agentChats.refresh": "Aktualisieren",
"agentChats.refreshTooltip": "Kommunikation neu laden",
"agentChats.from": "von",
"agentChats.to": "bis",
"agentChats.noInstance": "Keine Instanz ausgewählt",
"agentChats.selectInstancePrompt": "Bitte wähle oben im Kopfbereich eine Instanz aus, um deren Agenten-Kommunikation anzuzeigen.",
"agentChats.infoTitle": "Interne Kommunikation",
"agentChats.infoHint": "Hier wird der direkte Nachrichtenaustausch zwischen Agenten protokolliert, wenn Agenten über das Werkzeug AgentComm (Aktion send_message) miteinander sprechen.",
"agentChats.loading": "Lade Kommunikationsverlauf…",
"agentChats.matchCount": "{filtered} von {total} Nachrichten",
"agentChats.totalCount": "{total} Nachrichten",
"agentChats.empty": "Keine Nachrichten",
"agentChats.emptyFilter": "Für diese Filter liegt keine interne Kommunikation vor.",
"agentChats.emptyInstance": "In dieser Instanz wurde bisher keine interne Agent-zu-Agent-Kommunikation protokolliert.",
"agentChats.replyTo": "➔ Antwort an",
"agentChats.arrow": "➔",
"agentChats.loadFailed": "Kommunikation konnte nicht geladen werden: {error}",
"usage.title": "Token-Verbrauch & Analyse",
"usage.loading": "Lade Verbrauchsdaten…",
"usage.empty": "Keine Daten",
"usage.emptyHint": "Noch keine Token-Verbrauchsdaten aufgezeichnet.",
"usage.totalCost": "Gesamtkosten ({runs} Läufe)",
"usage.totalTokens": "Tokens gesamt",
"usage.inputTokens": "Eingabe-Tokens (Prompt)",
"usage.outputTokens": "Ausgabe-Tokens (Completion)",
"usage.unknownCostRuns": "{count} Lauf/Läufe ohne bekannten Preis — deren Kosten sind in der Summe nicht enthalten.",
"usage.tokensByAgent": "Tokens nach Agent",
"usage.tokensByModel": "Tokens nach Modell",
"usage.recentRuns": "Letzte Agenten-Läufe",
"usage.time": "Zeitpunkt",
"usage.agent": "Agent",
"usage.model": "Modell",
"usage.status": "Status",
"usage.prompt": "Prompt",
"usage.completion": "Completion",
"usage.total": "Gesamt",
"usage.cost": "Kosten",
"usage.unknownPrice": "(Preis unbekannt)",
"usage.notAvailable": "Verbrauchsdaten nicht verfügbar",
"usage.forecastTitle": "Kostenprognose",
"usage.forecastDay": "Prognose je Tag",
"usage.forecastMonth": "Prognose je Monat (30 Tage)",
"usage.tasksDay": "davon Aufgaben je Tag",
"usage.otherDay": "sonstige Läufe je Tag (Ø 7 Tage)",
"usage.budgetWarning": "{agent}: Die geplanten Aufgaben kosten laut Prognose {cost} am Tag, das Tagesbudget erlaubt {budget}. Die späten Läufe eines Tages werden abgelehnt.",
"usage.withoutBasis": "{count} Aufgabe(n) ohne Grundlage (kein Lauf, kein Modellpreis) – nicht in der Summe enthalten.",
"usage.oneOffOpen": "Offene einmalige Aufgaben kosten zusätzlich etwa {cost}.",
"usage.basisHint": "Grundlage je Aufgabe: Mittelwert ihrer letzten Läufe, sonst der des Agenten, sonst eine Schätzung aus Promptlänge und Listenpreis (eher zu hoch).",
"usage.task": "Aufgabe",
"usage.runsDay": "Läufe/Tag",
"usage.perRun": "je Lauf",
"usage.basis": "Grundlage",
"usage.perDay": "je Tag",
"usage.basisRuns": "Ø letzte {n} Läufe",
"usage.basisAgent": "Ø des Agenten ({n} Läufe)",
"usage.basisEstimate": "Schätzung – noch kein Lauf",
"usage.basisUnknown": "keine Grundlage",
"filter.range": "Zeitraum",
"filter.range.24h": "Letzte 24 Stunden",
@@ -42,15 +532,15 @@
"runs.total.runs": "Läufe",
"runs.total.prompt": "Prompt-Tokens",
"runs.total.completion": "Antwort-Tokens",
"runs.total.cached": "Aus dem Cache",
"runs.total.cached": "Aus Cache",
"runs.total.cost": "Kosten",
"runs.total.notRun": "Abgewiesen (Budget, Herunterfahren)",
"runs.unknownCost": "{n} Lauf/Läufe ohne bekannten Preis sind mit 0 $ gezählt.",
"runs.total.notRun": "Abgelehnt (Budget, Shutdown)",
"runs.unknownCost": "{n} Lauf/Läufe ohne bekannten Preis sind mit 0 $ gerechnet.",
"runs.empty.title": "Keine Läufe",
"runs.empty.text": "Kein Lauf passt zu diesem Filter.",
"runs.empty.text": "Kein Lauf passt auf diesen Filter.",
"runs.list": "Läufe",
"runs.page": "{from}–{to} von {total}",
"runs.col.time": "Zeit",
"runs.col.time": "Zeitpunkt",
"runs.col.agent": "Agent",
"runs.col.model": "Modell",
"runs.col.event": "Ereignis",
@@ -66,33 +556,33 @@
"detail.notFound": "Lauf nicht gefunden",
"detail.runId": "Lauf-ID",
"detail.tokens": "Tokens",
"detail.tokensValue": "{prompt} Prompt · {completion} Antwort · {cached} aus dem Cache",
"detail.tokensValue": "{prompt} Prompt · {completion} Antwort · {cached} aus Cache",
"detail.result": "Ergebnis",
"detail.receipt": "Beleg",
"detail.receipt": "Quittung",
"detail.transcript": "Prompt und Antwort",
"detail.transcript.disabled": "Für diesen Agenten ist der Prompt-Verlauf abgeschaltet (Aufbewahrung: nie).",
"detail.transcript.disabled": "Prompt-Verlauf ist für diesen Agenten abgeschaltet (Aufbewahrung: nie).",
"detail.transcript.unavailable": "Diese Instanz zeichnet keinen Prompt-Verlauf auf.",
"detail.transcript.missing": "Kein Transkript zu diesem Lauf – er lief vor Beginn der Aufzeichnung oder ist über seine Aufbewahrungsfrist hinaus.",
"detail.transcript.truncated": "Dieser Lauf hat die Größengrenze des Prompt-Verlaufs überschritten; spätere Einträge sind gekürzt.",
"detail.transcript.missing": "Kein Verlauf für diesen Lauf — er lief vor Beginn der Aufzeichnung oder ist älter als die Aufbewahrungsfrist.",
"detail.transcript.truncated": "Dieser Lauf hat die Größengrenze des Verlaufs überschritten; spätere Einträge sind gekürzt.",
"detail.transcript.size": "{n} Einträge, {chars} Zeichen",
"detail.systemPrompt": "Systemprompt ({n} Zeichen)",
"detail.entry.user": "Auftrag / Nachricht",
"detail.entry.engine": "Nachfrage der Engine",
"detail.systemPrompt": "System-Prompt ({n} Zeichen)",
"detail.entry.user": "Aufgabe / Nachricht",
"detail.entry.engine": "Nachfasslauf der Engine",
"detail.entry.event": "Ereignis",
"detail.entry.assistant": "Modellschritt {n}",
"detail.entry.usage": "{prompt} rein · {completion} raus",
"detail.entry.tool_result": "Ergebnis: {tool}",
"detail.entry.call": "Ruft {tool} auf",
"detail.entry.call": "Ruft {tool}",
"detail.entry.clipped": "gekürzt",
"detail.tool": "Werkzeug",
"detail.arguments": "Argumente",
"detail.toolCalls": "Werkzeugaufrufe",
"detail.toolCalls.hint": "Aus dem Audit-Log – auch ohne Transkript vorhanden.",
"detail.toolCalls.hint": "Aus dem Audit-Log — auch ohne Verlauf vorhanden.",
"detail.approvals": "Zur Freigabe vorgelegt",
"timeline.title": "Tokennutzung › Zeitverlauf",
"timeline.chart": "Zeitverlauf",
"timeline.metric": "Maß",
"timeline.metric": "Messwert",
"timeline.metric.cost": "Kosten",
"timeline.metric.tokens": "Tokens",
"timeline.metric.runs": "Läufe",
@@ -106,21 +596,21 @@
"timeline.tooltipRuns": "{n} Lauf/Läufe",
"timeline.event.budget_exceeded": "Budget erschöpft",
"timeline.event.model_changed": "Modell gewechselt",
"timeline.hint": "Ein Klick auf einen Balken zeigt dessen Läufe.",
"timeline.hint": "Klick auf einen Balken zeigt dessen Läufe.",
"history.title": "Prompt-Verlauf",
"history.intro": "Prompt und Antwort jedes Laufs, durchsuchbar über das ganze Gespräch samt Werkzeugaufrufen und Ergebnissen.",
"history.intro": "Prompt und Antwort jedes Laufs, durchsuchbar über die gesamte Unterhaltung samt Werkzeugaufrufen und Ergebnissen.",
"history.unavailable": "Prompt-Verlauf nicht verfügbar",
"history.empty.title": "Nichts aufgezeichnet",
"history.empty.text": "Keine Transkripte in diesem Zeitraum. Die Aufzeichnung beginnt mit Version 0.1.12 und folgt der Aufbewahrung unten.",
"history.empty.search": "Kein Transkript enthält diesen Text.",
"history.list": "Transkripte ({n})",
"history.empty.text": "Keine Verläufe in diesem Zeitraum. Die Aufzeichnung beginnt mit Version 0.1.12 und folgt den unten stehenden Fristen.",
"history.empty.search": "Kein Verlauf enthält diesen Text.",
"history.list": "Verläufe ({n})",
"history.prompt": "Prompt",
"history.response": "Antwort",
"history.truncated": "Größengrenze erreicht ({chars} Zeichen gespeichert).",
"history.settings.title": "Aufbewahrung",
"history.settings.intro": "Wie lange Prompts und Antworten aufbewahrt werden. Die Vorgabe gilt für jeden Agenten ohne eigene Einstellung; „nie“ löscht auch, was vorher aufgezeichnet wurde.",
"history.settings.intro": "Wie lange Prompts und Antworten aufgehoben werden. Die Vorgabe gilt für jeden Agenten ohne eigene Angabe; „nie“ löscht auch, was vorher aufgezeichnet war.",
"history.settings.default": "Vorgabe für alle Agenten:",
"history.settings.agent": "Agent",
"history.settings.retention": "Aufbewahrung",
@@ -129,13 +619,65 @@
"history.settings.mode.inherit": "Vorgabe",
"history.settings.mode.never": "Nie aufzeichnen",
"history.settings.mode.days": "Tage",
"history.settings.mode.forever": "Unbegrenzt",
"history.settings.mode.forever": "Für immer aufheben",
"history.settings.days": "Tage",
"history.settings.daysUnit": "Tage",
"history.settings.effective.never": "wird nicht aufgezeichnet",
"history.settings.effective.forever": "unbegrenzt",
"history.settings.effective.days": "{n} Tage",
"history.settings.effective.forever": "bleibt für immer",
"history.settings.effective.days": "bleibt {n} Tage",
"history.settings.save": "Aufbewahrung speichern",
"history.settings.saved": "Aufbewahrung gespeichert.",
"history.settings.failed": "Speichern fehlgeschlagen: {error}"
"history.settings.failed": "Speichern fehlgeschlagen: {error}",
"desktop.mainWindow.title": "ClawdDotNet",
"desktop.mainWindow.sidebarTooltip": "Seitenleiste ein-/ausklappen",
"desktop.mainWindow.switchInstance": "Instanz wechseln",
"desktop.mainWindow.instance": "INSTANZ",
"desktop.mainWindow.themeLight": "Hellmodus",
"desktop.mainWindow.themeDark": "Dunkelmodus",
"desktop.mainWindow.openInstanceFolder": "Instanz-Ordner öffnen",
"desktop.mainWindow.instanceManager": "Instanzmanager",
"desktop.mainWindow.exit": "Beenden",
"desktop.nav.work": "Arbeiten",
"desktop.nav.analytics": "Analyse",
"desktop.nav.system": "System",
"desktop.status.unknown": "unbekannt",
"desktop.status.design": "Entwurf",
"desktop.status.noApiKey": "kein API-Key",
"desktop.status.noUsageData": "keine Nutzungsdaten",
"desktop.settings.discard": "Verwerfen",
"desktop.settings.save": "Speichern",
"desktop.settings.instance": "INSTANZ",
"desktop.settings.instanceName": "Instanz-Name",
"desktop.settings.apiKey": "OpenRouter API-Key",
"desktop.settings.autostart": "Autostart durch Manager",
"desktop.settings.autostartHint": "Der Instanzmanager startet diese Instanz, wenn er hochkommt, und wieder, wenn sie abstürzt.",
"desktop.settings.logDir": "Protokoll-Verzeichnis",
"desktop.settings.logDirHint": "Gilt nur für diese Instanz. Leer heißt: Unterordner Logs im Instanzverzeichnis.",
"desktop.settings.dailyCost": "Tagesbudget ($ USD)",
"desktop.settings.dailyTokens": "Tagesbudget (Tokens)",
"desktop.settings.general": "ALLGEMEIN",
"desktop.settings.instancesDir": "Instanzen-Verzeichnis",
"desktop.settings.minLogLevel": "Minimaler Log-Level",
"desktop.settings.openRouterUrl": "OpenRouter Base-URL",
"desktop.settings.language": "Sprache",
"desktop.settings.dcAndLicense": "DEPLOYMENTCENTER & LIZENZ",
"desktop.settings.dcUrl": "Server-URL",
"desktop.settings.dcToken": "Master-Token",
"desktop.settings.dcEnv": "Umgebung",
"desktop.settings.errorReporting": "Fehler automatisch melden",
"desktop.settings.updateCheck": "Beim Start auf Updates prüfen",
"desktop.settings.updateChannel": "Update-Kanal",
"desktop.settings.licenseKey": "Lizenzschlüssel",
"desktop.chat.inputPlaceholder": "Nachricht eingeben (Enter zum Senden, Shift+Enter für neue Zeile)...",
"desktop.chat.send": "Senden",
"desktop.chat.statusReady": "Bereit",
"desktop.chat.statusSending": "Sende…",
"desktop.chat.statusDesign": "Entwurfsmodus",
"desktop.dialog.addAgent": "Agent hinzufügen",
"desktop.dialog.addJob": "Job hinzufügen",
"desktop.dialog.addService": "Dienst hinzufügen",
"desktop.dialog.restoreBackup": "Sicherung wiederherstellen",
"desktop.dialog.license": "Lizenz-Aktivierung",
"desktop.dialog.instancePicker": "Instanz auswählen"
}
@@ -6,6 +6,496 @@
"common.loading": "Loading…",
"common.previous": "Previous",
"common.next": "Next",
"common.save": "Save",
"common.cancel": "Cancel",
"common.delete": "Delete",
"common.edit": "Edit",
"common.refresh": "Refresh",
"common.close": "Close",
"common.discard": "Discard",
"common.apply": "Apply",
"common.reset": "Reset",
"common.search": "Search",
"common.actions": "Actions",
"common.status": "Status",
"common.name": "Name",
"common.description": "Description",
"common.ready": "Ready",
"common.none": "None",
"common.unknown": "Unknown",
"common.all": "All",
"common.details": "Details",
"common.language": "Language",
"common.logout": "Log Out",
"common.confirm": "Confirm",
"common.yes": "Yes",
"common.no": "No",
"status.running": "Running",
"status.stopped": "Stopped",
"status.silent": "Silent",
"status.crashed": "Crashed",
"status.unknown": "Unknown",
"status.ready": "Ready",
"status.busy": "Busy",
"status.paused": "Paused",
"status.done": "Done",
"status.failed": "Failed",
"status.blocked": "Blocked",
"status.inReview": "In Review",
"status.canceled": "Canceled",
"status.archived": "Archived",
"header.instanceLabel": "Instance:",
"header.noInstance": "(No instance)",
"header.switchInstance": "Switch active instance",
"header.startInstance": "Start",
"header.startInstanceTooltip": "Start selected instance now",
"header.toggleTheme": "Toggle color theme",
"header.eventStreamConnected": "Event stream: connected",
"header.eventStreamDisconnected": "Event stream: disconnected",
"header.multipleRunning": "{count} running",
"header.noneActive": "None active",
"header.starting": "Starting…",
"header.noInstances": "No instances",
"header.selectInstancePlaceholder": "— Select instance —",
"header.instanceOffline": "Instance offline (502)",
"header.selectInstancePrompt": "Select instance (409)",
"nav.toggleSidebar": "Toggle sidebar",
"nav.group.work": "Work",
"nav.chat": "Chat",
"nav.agents": "Agents",
"nav.tasks": "Tasks",
"nav.services": "Services",
"nav.group.analytics": "Analytics",
"nav.usage": "Token Usage",
"nav.agentChats": "Agent Chats",
"nav.group.system": "System",
"nav.instances": "Instances",
"nav.staging": "Approvals",
"nav.logs": "Logs",
"nav.backup": "Backup",
"nav.settings": "Settings",
"nav.info": "Info",
"login.prompt": "Please enter the instance manager access token (webUiToken in manager.json).",
"login.tokenPlaceholder": "Access token (at least 20 characters)",
"login.submit": "Sign In",
"login.invalidToken": "Token is invalid or shorter than 20 characters.",
"login.error": "Sign in error: {error}",
"chat.agentLabel": "Agent: ",
"chat.noAgents": "No agents available",
"chat.wake": "Wake",
"chat.wakeTooltip": "Manually trigger / wake agent",
"chat.clearHistory": "Clear history",
"chat.clearHistoryTooltip": "Clear chat history for this agent",
"chat.cancel": "Cancel",
"chat.cancelTooltip": "Cancel active run",
"chat.placeholder": "Type message to agent (Enter to send, Shift+Enter for new line)…",
"chat.send": "Send",
"chat.sendTooltip": "Send message",
"chat.loadingHistory": "Loading history…",
"chat.emptyHistory": "No messages in history yet.",
"chat.historyLoadError": "Failed to load history: {error}",
"chat.sendError": "Failed to send message: {error}",
"chat.cancelConfirm": "Run was canceled.",
"chat.cancelError": "Cancellation failed: {error}",
"chat.wakeSuccess": "Agent started manually.",
"chat.wakeError": "Failed to start agent: {error}",
"chat.clearConfirmPrompt": "Permanently clear the entire chat history for this agent?",
"chat.cleared": "Chat history cleared.",
"chat.clearError": "Failed to clear history: {error}",
"chat.role.user": "User",
"chat.role.system": "System",
"chat.role.tool": "Tool",
"agents.title": "Agents",
"agents.newAgent": "New Agent",
"agents.loading": "Loading agents…",
"agents.empty": "No agents configured in this instance yet.",
"agents.createFirst": "Create first agent",
"agents.id": "ID",
"agents.model": "Model",
"agents.toolsActive": "{count} tools active",
"agents.details": "Agent Details",
"agents.selectPrompt": "Select an agent on the left.",
"agents.chat": "Chat",
"agents.chatTooltip": "Chat with this agent",
"agents.delete": "Delete",
"agents.deleteTooltip": "Delete this agent",
"agents.name": "Name",
"agents.description": "Description",
"agents.availableTools": "Available Tools",
"agents.noTools": "No tools configured for this agent.",
"agents.configuration": "Configuration",
"agents.configTooltip": "View tool settings",
"agents.toolToggled": "Tool '{tool}' {status}.",
"agents.enabled": "enabled",
"agents.disabled": "disabled",
"agents.saveFailed": "Modification failed: {error}",
"agents.newAgentTitle": "Create New Agent",
"agents.idLabel": "Agent ID (unique key):",
"agents.idPlaceholder": "e.g. scout",
"agents.nameLabel": "Display Name:",
"agents.namePlaceholder": "e.g. Scout Agent",
"agents.modelLabel": "Model (OpenRouter):",
"agents.descLabel": "Description:",
"agents.descPlaceholder": "Optional description of the agent…",
"agents.createButton": "Create Agent",
"agents.createdSuccess": "Agent '{name}' created successfully.",
"agents.createFailed": "Failed to create agent: {error}",
"agents.deleteConfirm": "Really delete agent '{name}' ({id})?",
"agents.deleted": "Agent '{name}' deleted.",
"agents.deleteFailed": "Deletion failed: {error}",
"agents.settingsTitle": "Settings: {tool} ({agent})",
"agents.loadingSettings": "Loading configuration…",
"agents.secretHint": "Secret values (passwords, keys) are masked by the server.",
"agents.noSettings": "No custom settings configured.",
"agents.loadFailed": "Loading failed: {error}",
"agents.rights": "Permissions",
"agents.noOperations": "This tool has no individual operations. Default rule applies.",
"agents.configuredHint": "Allowed operations run without confirmation, others require single approval, forbidden are denied immediately.",
"agents.notConfiguredHint": "No permissions configured: Tool defaults apply. Saving below will make these settings binding.",
"agents.operation": "Operation",
"agents.setting": "Setting",
"agents.effect": "Effect",
"agents.allowed": "allowed",
"agents.singleApproval": "single approval",
"agents.forbidden": "forbidden",
"agents.alwaysApprovalReason": "Modifications to agents always require approval.",
"agents.saveRights": "Save Permissions",
"agents.rightsSaved": "Permissions saved.",
"agents.effectAuto": "runs",
"agents.effectApprove": "single approval",
"agents.effectDeny": "denied",
"agents.standingApproval": "Standing approval #{id}",
"tasks.title": "Tasks",
"tasks.newTask": "New Task",
"tasks.showCompleted": "Show completed",
"tasks.empty": "No tasks found",
"tasks.createTitle": "Create Task",
"tasks.agent": "Agent",
"tasks.taskMessage": "Task description / instruction",
"tasks.schedule": "Schedule",
"tasks.scheduleType": "Schedule Type",
"tasks.scheduleNone": "None (Manual run only)",
"tasks.scheduleCron": "Cron Expression",
"tasks.scheduleInterval": "Interval",
"tasks.scheduleAt": "Specific time",
"tasks.runNow": "Run Now",
"tasks.pause": "Pause",
"tasks.resume": "Resume",
"tasks.delete": "Delete",
"tasks.saved": "Task saved.",
"tasks.deleted": "Task deleted.",
"tasks.runTriggered": "Run triggered.",
"services.title": "Services",
"services.newService": "New Service",
"services.empty": "No background services configured",
"services.serviceName": "Service Name",
"services.serviceType": "Type",
"services.port": "Port",
"services.active": "Active",
"services.start": "Start",
"services.stop": "Stop",
"services.restart": "Restart",
"backup.title": "Backup & Restore",
"backup.refresh": "Refresh",
"backup.refreshTooltip": "Reload backups",
"backup.newBackup": "New Backup",
"backup.newBackupTooltip": "Create new backup",
"backup.noInstance": "No instance selected",
"backup.selectInstancePrompt": "Please select an instance in the header above to manage its backups.",
"backup.loading": "Loading backups…",
"backup.backupDir": "Backup Directory",
"backup.backupDirHint": "Backups are fully encrypted with the instance manager's license key and contain credentials. They can be restored on another machine using the same license key.",
"backup.existingBackups": "Existing Backups ({count})",
"backup.noBackups": "No backups exist for this instance yet. Create one using the button above.",
"backup.restore": "Restore",
"backup.restoreTooltip": "Restore this backup",
"backup.deleteTooltip": "Permanently delete backup file",
"backup.archiveFile": "Archive File",
"backup.createdAt": "Created At",
"backup.size": "Size",
"backup.protection": "Protection",
"backup.files": "Files",
"backup.actions": "Actions",
"backup.fileCount": "{count} files",
"backup.foreignLicense": "Foreign License",
"backup.unreadable": "Unreadable",
"backup.licenseProtection": "License ({count} credentials)",
"backup.passphraseProtection": "Passphrase ({count})",
"backup.unencryptedProtection": "Unencrypted, no credentials",
"backup.dialogTitle": "Create New Backup",
"backup.dialogHint": "The backup will be encrypted with the instance manager's license key and contains credentials. If FTP upload is configured, it will be uploaded afterwards.",
"backup.includeChats": "Backup chat history",
"backup.includeLogs": "Include log files",
"backup.createButton": "Create Backup",
"backup.creating": "Creating backup…",
"backup.created": "Backup created.",
"backup.createFailed": "Backup failed: {error}",
"backup.restoreTitle": "Restore Backup: {file}",
"backup.overwriteWarningTitle": "Caution: Overwriting Existing Data",
"backup.overwriteWarningText": "Restoring will overwrite the instance's current configuration and data with the state from {date}.",
"backup.instanceStopNotice": "The instance will be gracefully stopped and restarted afterwards.",
"backup.passphraseLabel": "Passphrase for credentials",
"backup.passphrasePlaceholder": "Backup passphrase…",
"backup.passphraseRequired": "Please enter the passphrase.",
"backup.restoreNow": "Restore Now",
"backup.restoring": "Restoring…",
"backup.restoreSuccess": "Restore completed.",
"backup.restoreFailed": "Restore failed: {error}",
"backup.foreignLicenseTitle": "Source License Key Required",
"backup.sourceLicenseLabel": "Source License Key",
"backup.sourceLicenseHint": "Case and spaces do not matter.",
"backup.sourceLicensePlaceholder": "License key of the installation where the backup originated…",
"backup.restoreWithKey": "Restore with this Key",
"backup.keyMismatch": "This license key does not match the backup.",
"backup.deleteTitle": "Delete Backup",
"backup.deleteConfirm": "Really permanently delete backup file '{file}'?",
"backup.deleted": "Backup deleted.",
"backup.deleteFailed": "Deletion failed: {error}",
"backup.ftpTitle": "Backup FTP Upload",
"backup.ftpLoading": "Loading FTP settings…",
"backup.ftpHint": "Backups are encrypted — anyone reading the server sees nothing usable. However, without TLS, the server password is sent in plain text.",
"backup.ftpAutoUpload": "Automatically upload completed backups",
"backup.ftpServer": "Server",
"backup.ftpPort": "Port",
"backup.ftpUser": "Username",
"backup.ftpPassword": "Password",
"backup.ftpPasswordSaved": "(saved — leave empty to keep unchanged)",
"backup.ftpDirectory": "Target Directory",
"backup.ftpDirectoryHint": "Each instance gets its own subfolder.",
"backup.ftpKeepCount": "Backups to keep per instance",
"backup.ftpKeepCountHint": "Older backups on the server will be deleted.",
"backup.ftpRequireTls": "Connect with TLS only",
"backup.ftpTest": "Test Connection",
"backup.ftpTestFailed": "Test failed: {error}",
"backup.ftpSaved": "Saved.",
"backup.ftpSaveFailed": "Save failed: {error}",
"logs.title": "Logs",
"logs.connecting": "Connecting…",
"logs.allInstances": "All Instances",
"logs.allModules": "All Modules",
"logs.clear": "Clear",
"logs.pause": "Pause",
"logs.resume": "Resume",
"logs.paused": "Paused",
"logs.autoScroll": "Autoscroll",
"logs.empty": "No log messages yet.",
"settings.title": "Settings & System Architecture",
"settings.intro": "Configuration overview following the instance manager concept: Strict separation between global manager level (installation) and isolated instance level.",
"settings.managerLevel": "Instance Manager",
"settings.managerBadge": "Manager Level · Global",
"settings.managerIntro": "Applies to all instances and the WebUI. Stored in manager.json in the application root directory.",
"settings.webUiAndNet": "WebUI & Network",
"settings.configFile": "Configuration File",
"settings.webUiPort": "WebUI Port",
"settings.webUiPortDefault": "7000 (default, webUiPort)",
"settings.networkBinding": "Network Binding",
"settings.networkBindingValue": "loopback (127.0.0.1, webUiBinding)",
"settings.accessProtection": "Access Protection",
"settings.tokenActive": "Token active (≥ 20 characters)",
"settings.licenseAndUpdates": "License & Updates",
"settings.licenseScope": "License Scope",
"settings.licenseScopeValue": "Installation level (E-M3: Unlimited instances)",
"settings.licenseKey": "License Key",
"settings.licenseEncrypted": "●●●●-●●●●-●●●●-●●●● (encrypted)",
"settings.updateChannel": "Update Channel",
"settings.updateChannelValue": "prod (Official release versions)",
"settings.updateCheck": "Update Check",
"settings.updateCheckValue": "Central at manager level",
"settings.dcAndOversight": "Deploymentcenter & Oversight",
"settings.dcMasterToken": "Master Token",
"settings.dcMasterTokenValue": "●●●●●●●● (generates sub-tokens per instance)",
"settings.desktopWindows": "Desktop Windows",
"settings.desktopWindowsValue": "Active (each instance has its own window)",
"settings.crashProtection": "Crash Protection",
"settings.crashProtectionValue": "Max 5 restarts / 10 minutes",
"settings.instanceSectionTitle": "Active Instance: {name}",
"settings.instanceSectionTitleNone": "Instance-Related Settings",
"settings.instanceBadge": "Instance Level · Isolated",
"settings.noInstanceActive": "No instance active",
"settings.instanceIntro": "Applies exclusively to the currently selected instance. All paths, logs, agents and budgets are strictly isolated from other instances.",
"settings.noInstanceSelected": "No instance selected",
"settings.noInstancePrompt": "Please select an active instance in the header above or switch to 'Instances' to start or select an instance.",
"settings.toInstancesOverview": "To Instances Overview",
"settings.identityAndProcess": "Identity & Process",
"settings.instanceName": "Instance Name",
"settings.instanceId": "Instance ID",
"settings.autostartByManager": "Autostart by Manager",
"settings.autostartActive": "Active (keep running)",
"settings.autostartInactive": "Inactive (manual start)",
"settings.communication": "Communication",
"settings.communicationValue": "HTTP over Named Pipe (Windows) / Socket (Linux)",
"settings.isolatedPaths": "Isolated Directories",
"settings.logsPath": "Logs",
"settings.backupsPath": "Backups",
"settings.stateDbPath": "State Database",
"settings.stateDbValue": "Instances/{name}/state.db (exclusive)",
"settings.workspacePath": "Shared Workspace",
"settings.openRouterCard": "Model Integration (OpenRouter)",
"settings.openRouterStatus": "OpenRouter Status",
"settings.connected": "Connected",
"settings.offline": "Offline",
"settings.statusMessage": "Status Message",
"settings.credits": "Credits",
"settings.apiKey": "API Key",
"settings.apiKeyMasked": "Masked in InstanceSettings.json of this instance",
"settings.openRouterNotRunning": "Instance is not running. Data will be loaded after start.",
"settings.openRouterNoKey": "No key configured or service unreachable.",
"settings.runtimeAndSubsystems": "Runtime & Subsystems",
"settings.agentEngine": "Agent Engine",
"settings.scheduler": "Scanner / Scheduler",
"settings.stagingService": "Approvals (Staging)",
"settings.dcSubToken": "Deploymentcenter Sub-Token",
"settings.subsystemsLoading": "Querying runtime data…",
"settings.subsystemsStopped": "Instance stopped — subsystems idle.",
"settings.save": "Save Settings",
"settings.discard": "Discard Changes",
"settings.saved": "Settings saved successfully.",
"settings.saveError": "Failed to save settings: {error}",
"info.title": "System & Status",
"info.noInstanceSelected": "No instance selected",
"info.noInstanceHint": "Select an active instance in the header above or switch to 'Instances' to start one.",
"info.loading": "Loading system data…",
"info.instanceAndVersion": "Instance & Version",
"info.instanceName": "Instance Name",
"info.instanceId": "Instance ID",
"info.version": "Version",
"info.build": "Build",
"info.environment": "Environment",
"info.dataDirectory": "Data Directory",
"info.subsystems": "Subsystems",
"info.openRouterStatus": "OpenRouter & Credits",
"instances.title": "Instance Management",
"instances.refresh": "Refresh",
"instances.newInstance": "New Instance",
"instances.activeBadge": "Active in WebUI",
"instances.selectActive": "Select as active",
"instances.start": "Start",
"instances.stop": "Stop",
"instances.bringToFront": "Bring window to front",
"instances.createTitle": "Create New Instance",
"instances.instanceOfflineError": "The instance is not reachable.",
"instances.noInstanceActiveError": "No instance is active. Start or select one under 'Instances'.",
"instances.instanceNameLabel": "Instance Name",
"instances.instanceNamePlaceholder": "e.g. production-1",
"instances.dailyBudgetUsd": "Daily Budget ($ USD)",
"instances.dailyBudgetTokens": "Daily Budget (Tokens)",
"instances.autostart": "Autostart with Manager",
"instances.created": "Instance created.",
"instances.createError": "Failed to create instance: {error}",
"staging.title": "Approvals",
"staging.refresh": "Refresh",
"staging.loading": "Loading approvals…",
"staging.empty": "No open approvals",
"staging.emptyHint": "Currently no tool calls are waiting for manual approval.",
"staging.approve": "Approve",
"staging.reject": "Reject",
"staging.callId": "Call ID",
"staging.agent": "Agent",
"staging.tool": "Tool",
"staging.time": "Timestamp",
"staging.frozenArgs": "Frozen Arguments:",
"staging.suggestHint": "This operation by {agent} was approved {approvals} times in the last 14 days. Should {tool}.{action} run without confirmation for this agent in the future?",
"staging.allowSimilar": "Allow similar in future: {agent} may run {tool}.{action} without confirmation from now on (revocable below under 'Standing Approvals').",
"staging.notActive": "Staging not active",
"staging.notActiveHint": "No staging service configured in this instance.",
"staging.approved": "Call approved{similar}: {message}",
"staging.similarSuffix": " and allowed for the future",
"staging.executedSuccess": "Executed successfully",
"staging.approveFailed": "Approval failed: {error}",
"staging.standingTitle": "Standing Approvals",
"staging.standingEmpty": "None. When approving, 'Allow similar in future' can be selected; permanent permissions are in the agent's tool config.",
"staging.granted": "Granted",
"staging.occasion": "Occasion",
"staging.revoke": "Revoke",
"staging.revokeConfirm": "Revoke standing approval? {agent} will have to submit {tool}.{action} individually again.",
"staging.revoked": "Revoked.",
"staging.revokeFailed": "Revocation failed: {error}",
"staging.rejectTitle": "Reject Tool Call",
"staging.rejectReasonLabel": "Reason (optional):",
"staging.rejectReasonPlaceholder": "Reason for rejection to the agent…",
"staging.rejectButton": "Reject Call",
"staging.rejected": "Call was rejected.",
"staging.rejectFailed": "Rejection failed: {error}",
"agentChats.title": "Agent Communication (A2A)",
"agentChats.allAgents": "All Agents",
"agentChats.filterTooltip": "Filter by participating agent (sender or receiver)",
"agentChats.fromTooltip": "Messages from this day",
"agentChats.toTooltip": "Messages up to and including this day",
"agentChats.searchPlaceholder": "Fulltext…",
"agentChats.searchTooltip": "Search message text and agent names",
"agentChats.refresh": "Refresh",
"agentChats.refreshTooltip": "Reload communication",
"agentChats.from": "from",
"agentChats.to": "to",
"agentChats.noInstance": "No instance selected",
"agentChats.selectInstancePrompt": "Please select an instance in the header above to view its agent communication.",
"agentChats.infoTitle": "Internal Communication",
"agentChats.infoHint": "Direct message exchanges between agents are logged here when agents communicate via the AgentComm tool (action send_message).",
"agentChats.loading": "Loading communication history…",
"agentChats.matchCount": "{filtered} of {total} messages",
"agentChats.totalCount": "{total} messages",
"agentChats.empty": "No Messages",
"agentChats.emptyFilter": "No internal communication matches these filters.",
"agentChats.emptyInstance": "No internal agent-to-agent communication has been logged in this instance yet.",
"agentChats.replyTo": "➔ Reply to",
"agentChats.arrow": "➔",
"agentChats.loadFailed": "Failed to load communication: {error}",
"usage.title": "Token Usage & Analysis",
"usage.loading": "Loading usage data…",
"usage.empty": "No Data",
"usage.emptyHint": "No token usage data recorded yet.",
"usage.totalCost": "Total Cost ({runs} runs)",
"usage.totalTokens": "Total Tokens",
"usage.inputTokens": "Input Tokens (Prompt)",
"usage.outputTokens": "Output Tokens (Completion)",
"usage.unknownCostRuns": "{count} run(s) without known price — their costs are not included in the total.",
"usage.tokensByAgent": "Tokens by Agent",
"usage.tokensByModel": "Tokens by Model",
"usage.recentRuns": "Recent Agent Runs",
"usage.time": "Timestamp",
"usage.agent": "Agent",
"usage.model": "Model",
"usage.status": "Status",
"usage.prompt": "Prompt",
"usage.completion": "Completion",
"usage.total": "Total",
"usage.cost": "Cost",
"usage.unknownPrice": "(price unknown)",
"usage.notAvailable": "Usage data not available",
"usage.forecastTitle": "Cost Forecast",
"usage.forecastDay": "Forecast per day",
"usage.forecastMonth": "Forecast per month (30 days)",
"usage.tasksDay": "thereof tasks per day",
"usage.otherDay": "other runs per day (Ø 7 days)",
"usage.budgetWarning": "{agent}: Planned tasks are forecast to cost {cost} per day, daily budget allows {budget}. Later runs of a day will be rejected.",
"usage.withoutBasis": "{count} task(s) without basis (no run, no model price) — not included in total.",
"usage.oneOffOpen": "Open one-off tasks cost approximately an additional {cost}.",
"usage.basisHint": "Basis per task: average of its recent runs, otherwise agent's average, otherwise an estimate from prompt length and list price (tends to be high).",
"usage.task": "Task",
"usage.runsDay": "Runs/Day",
"usage.perRun": "per Run",
"usage.basis": "Basis",
"usage.perDay": "per Day",
"usage.basisRuns": "Ø last {n} runs",
"usage.basisAgent": "Ø of agent ({n} runs)",
"usage.basisEstimate": "Estimate – no runs yet",
"usage.basisUnknown": "no basis",
"filter.range": "Period",
"filter.range.24h": "Last 24 hours",
@@ -137,5 +627,57 @@
"history.settings.effective.days": "kept {n} days",
"history.settings.save": "Save retention",
"history.settings.saved": "Retention saved.",
"history.settings.failed": "Saving failed: {error}"
"history.settings.failed": "Saving failed: {error}",
"desktop.mainWindow.title": "ClawdDotNet",
"desktop.mainWindow.sidebarTooltip": "Toggle sidebar",
"desktop.mainWindow.switchInstance": "Switch instance",
"desktop.mainWindow.instance": "INSTANCE",
"desktop.mainWindow.themeLight": "Light Mode",
"desktop.mainWindow.themeDark": "Dark Mode",
"desktop.mainWindow.openInstanceFolder": "Open Instance Folder",
"desktop.mainWindow.instanceManager": "Instance Manager",
"desktop.mainWindow.exit": "Exit",
"desktop.nav.work": "Work",
"desktop.nav.analytics": "Analytics",
"desktop.nav.system": "System",
"desktop.status.unknown": "unknown",
"desktop.status.design": "Design",
"desktop.status.noApiKey": "No API Key",
"desktop.status.noUsageData": "No usage data",
"desktop.settings.discard": "Discard",
"desktop.settings.save": "Save",
"desktop.settings.instance": "INSTANCE",
"desktop.settings.instanceName": "Instance Name",
"desktop.settings.apiKey": "OpenRouter API Key",
"desktop.settings.autostart": "Autostart by Manager",
"desktop.settings.autostartHint": "The instance manager starts this instance on launch and restarts it if it crashes.",
"desktop.settings.logDir": "Log Directory",
"desktop.settings.logDirHint": "Applies only to this instance. Empty means: Logs subfolder in instance directory.",
"desktop.settings.dailyCost": "Daily Budget ($ USD)",
"desktop.settings.dailyTokens": "Daily Budget (Tokens)",
"desktop.settings.general": "GENERAL",
"desktop.settings.instancesDir": "Instances Directory",
"desktop.settings.minLogLevel": "Minimum Log Level",
"desktop.settings.openRouterUrl": "OpenRouter Base URL",
"desktop.settings.language": "Language",
"desktop.settings.dcAndLicense": "DEPLOYMENTCENTER & LICENSE",
"desktop.settings.dcUrl": "Server URL",
"desktop.settings.dcToken": "Master Token",
"desktop.settings.dcEnv": "Environment",
"desktop.settings.errorReporting": "Automatically report errors",
"desktop.settings.updateCheck": "Check for updates on startup",
"desktop.settings.updateChannel": "Update Channel",
"desktop.settings.licenseKey": "License Key",
"desktop.chat.inputPlaceholder": "Type message... (Enter to send, Shift+Enter for new line)",
"desktop.chat.send": "Send",
"desktop.chat.statusReady": "Ready",
"desktop.chat.statusSending": "Sending…",
"desktop.chat.statusDesign": "Design Mode",
"desktop.dialog.addAgent": "Add Agent",
"desktop.dialog.addJob": "Add Job",
"desktop.dialog.addService": "Add Service",
"desktop.dialog.restoreBackup": "Restore Backup",
"desktop.dialog.license": "License Activation",
"desktop.dialog.instancePicker": "Choose Instance"
}
+47 -43
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="de">
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -14,12 +14,12 @@
<div class="karte-kopf">
<h2>ClawdDotNet</h2>
</div>
<p class="hinweis">
Bitte gib das Zugangstoken des Instanzmanagers ein (<code>webUiToken</code> in <code>manager.json</code>).
<p class="hinweis" data-i18n="login.prompt">
Please enter the instance manager access token (<code>webUiToken</code> in <code>manager.json</code>).
</p>
<form id="anmeldeformular" class="formular-raster">
<input type="password" id="token-eingabe" placeholder="Zugangstoken (mind. 20 Zeichen)" autocomplete="current-password" required>
<button type="submit" class="primaer">Anmelden</button>
<input type="password" id="token-eingabe" placeholder="Access token (at least 20 characters)" data-i18n-placeholder="login.tokenPlaceholder" autocomplete="current-password" required>
<button type="submit" class="primaer" data-i18n="login.submit">Sign In</button>
<p id="anmeldefehler" class="fehler" hidden></p>
</form>
</div>
@@ -33,7 +33,7 @@
<aside id="seitenleiste" class="seitenleiste">
<div class="seitenleiste-kopf">
<span class="marke">ClawdDotNet</span>
<button id="sidebar-toggle" class="schaltflaeche-icon flach" title="Seitenleiste ein-/ausklappen" type="button">
<button id="sidebar-toggle" class="schaltflaeche-icon flach" title="Toggle sidebar" data-i18n-title="nav.toggleSidebar" type="button">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6"/>
</svg>
@@ -42,40 +42,40 @@
<nav class="seitenleiste-inhalt">
<div class="nav-gruppe">
<div class="nav-gruppe-titel">Arbeiten</div>
<a href="#/chat" class="nav-link" title="Chat">
<div class="nav-gruppe-titel" data-i18n="nav.group.work">Work</div>
<a href="#/chat" class="nav-link" title="Chat" data-i18n-title="nav.chat">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
</svg>
<span class="nav-label">Chat</span>
<span class="nav-label" data-i18n="nav.chat">Chat</span>
</a>
<a href="#/agenten" class="nav-link" title="Agenten">
<a href="#/agenten" class="nav-link" title="Agents" data-i18n-title="nav.agents">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2 M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M23 21v-2a4 4 0 0 0-3-3.87 M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span class="nav-label">Agenten</span>
<span class="nav-label" data-i18n="nav.agents">Agents</span>
</a>
<a href="#/aufgaben" class="nav-link" title="Aufgaben">
<a href="#/aufgaben" class="nav-link" title="Tasks" data-i18n-title="nav.tasks">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path d="M9 11l3 3L22 4 M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
</svg>
<span class="nav-label">Aufgaben</span>
<span class="nav-label" data-i18n="nav.tasks">Tasks</span>
</a>
<a href="#/dienste" class="nav-link" title="Dienste">
<a href="#/dienste" class="nav-link" title="Services" data-i18n-title="nav.services">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path d="M4 14a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-6z M4 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4z M12 6v6 M8 8h.01 M16 8h.01"/>
</svg>
<span class="nav-label">Dienste</span>
<span class="nav-label" data-i18n="nav.services">Services</span>
</a>
</div>
<div class="nav-gruppe">
<div class="nav-gruppe-titel">Analyse</div>
<a href="#/verbrauch" class="nav-link" title="Tokennutzung" data-exakt>
<div class="nav-gruppe-titel" data-i18n="nav.group.analytics">Analytics</div>
<a href="#/verbrauch" class="nav-link" title="Token Usage" data-i18n-title="nav.usage" data-exakt>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path d="M3 3v18h18 M7 15v4 M11 11v8 M15 7v12 M19 12v7"/>
</svg>
<span class="nav-label">Tokennutzung</span>
<span class="nav-label" data-i18n="nav.usage">Token Usage</span>
</a>
<a href="#/verbrauch/runs" class="nav-link nav-unterlink" data-i18n-title="nav.usage.runs">
<span class="nav-label" data-i18n="nav.usage.runs">Runs</span>
@@ -89,55 +89,55 @@
</svg>
<span class="nav-label" data-i18n="nav.promptHistory">Prompt History</span>
</a>
<a href="#/agenten-chats" class="nav-link" title="Agenten-Chats">
<a href="#/agenten-chats" class="nav-link" title="Agent Chats" data-i18n-title="nav.agentChats">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path d="M8 12h.01M12 12h.01M16 12h.01 M21 12c0 4.4-4 8-9 8-1.3 0-2.6-.2-3.7-.7L3 20l1-4.2C3.4 14.6 3 13.3 3 12c0-4.4 4-8 9-8s9 3.6 9 8Z"/>
</svg>
<span class="nav-label">Agenten-Chats</span>
<span class="nav-label" data-i18n="nav.agentChats">Agent Chats</span>
</a>
</div>
<div class="nav-gruppe">
<div class="nav-gruppe-titel">System</div>
<a href="#/instanzen" class="nav-link" title="Instanzen">
<div class="nav-gruppe-titel" data-i18n="nav.group.system">System</div>
<a href="#/instanzen" class="nav-link" title="Instances" data-i18n-title="nav.instances">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path d="M2 20h20 M2 14h20 M2 8h20 M2 4h20 M6 6h.01 M6 12h.01 M6 18h.01"/>
</svg>
<span class="nav-label">Instanzen</span>
<span class="nav-label" data-i18n="nav.instances">Instances</span>
</a>
<a href="#/staging" class="nav-link" title="Freigaben">
<a href="#/staging" class="nav-link" title="Approvals" data-i18n-title="nav.staging">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z M9 12l2 2 4-4"/>
</svg>
<span class="nav-label">Freigaben</span>
<span class="nav-label" data-i18n="nav.staging">Approvals</span>
<span id="staging-badge" class="nav-badge" hidden>0</span>
</a>
<a href="#/protokoll" class="nav-link" title="Protokoll">
<a href="#/protokoll" class="nav-link" title="Logs" data-i18n-title="nav.logs">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20 M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z M8 7h8 M8 11h6"/>
</svg>
<span class="nav-label">Protokoll</span>
<span class="nav-label" data-i18n="nav.logs">Logs</span>
</a>
<a href="#/sicherung" class="nav-link" title="Sicherung">
<a href="#/sicherung" class="nav-link" title="Backup" data-i18n-title="nav.backup">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z M17 21v-8H7v8 M7 3v5h8"/>
</svg>
<span class="nav-label">Sicherung</span>
<span class="nav-label" data-i18n="nav.backup">Backup</span>
</a>
<a href="#/einstellungen" class="nav-link" title="Einstellungen">
<a href="#/einstellungen" class="nav-link" title="Settings" data-i18n-title="nav.settings">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
<span class="nav-label">Einstellungen</span>
<span class="nav-label" data-i18n="nav.settings">Settings</span>
</a>
<a href="#/info" class="nav-link" title="Info">
<a href="#/info" class="nav-link" title="Info" data-i18n-title="nav.info">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
<span class="nav-label">Info</span>
<span class="nav-label" data-i18n="nav.info">Info</span>
</a>
</div>
</nav>
@@ -147,7 +147,7 @@
<div class="hauptbereich">
<header class="kopfzeile">
<div class="kopfzeile-links">
<button id="mobil-menue-btn" class="schaltflaeche-icon flach" title="Seitenleiste ein-/ausklappen" type="button">
<button id="mobil-menue-btn" class="schaltflaeche-icon flach" title="Toggle sidebar" data-i18n-title="nav.toggleSidebar" type="button">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="6" x2="21" y2="6"/>
@@ -155,19 +155,23 @@
</svg>
</button>
<div class="kopf-instanz-container" id="kopf-instanz-container">
<label for="kopf-instanz-select" class="kopf-instanz-label">Instanz:</label>
<select id="kopf-instanz-select" class="eingabe-select klein kopf-instanz-select" title="Aktive Instanz wechseln">
<option value="">(Keine Instanz)</option>
<label for="kopf-instanz-select" class="kopf-instanz-label" data-i18n="header.instanceLabel">Instance:</label>
<select id="kopf-instanz-select" class="eingabe-select klein kopf-instanz-select" title="Switch active instance" data-i18n-title="header.switchInstance">
<option value="" data-i18n="header.noInstance">(No instance)</option>
</select>
<span id="kopf-instanz-status" class="plakette plakette-neutral">Lädt…</span>
<button id="kopf-instanz-start-btn" class="schaltflaeche klein primaer" title="Ausgewählte Instanz jetzt starten" type="button" hidden>Starten</button>
<span id="kopf-instanz-status" class="plakette plakette-neutral" data-i18n="common.loading">Loading…</span>
<button id="kopf-instanz-start-btn" class="schaltflaeche klein primaer" title="Start selected instance now" data-i18n="header.startInstance" data-i18n-title="header.startInstanceTooltip" type="button" hidden>Start</button>
</div>
</div>
<div class="kopfzeile-rechts">
<span id="kopf-openrouter-status" class="gedaempft text-klein"></span>
<span id="verbindung-punkt" class="status-punkt" title="Ereignisstrom: getrennt"></span>
<button id="theme-schalter" class="schaltflaeche-icon flach" title="Farbschema umschalten" type="button">
<span id="verbindung-punkt" class="status-punkt" title="Event stream: disconnected" data-i18n-title="header.eventStreamDisconnected"></span>
<select id="sprache-schalter" class="eingabe-select klein" title="Language" data-i18n-title="common.language">
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
<button id="theme-schalter" class="schaltflaeche-icon flach" title="Toggle color theme" data-i18n-title="header.toggleTheme" type="button">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
@@ -180,8 +184,8 @@
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
</button>
<button id="abmelden-btn" class="schaltflaeche flach klein" title="Abmelden" type="button">
Abmelden
<button id="abmelden-btn" class="schaltflaeche flach klein" title="Log Out" data-i18n="common.logout" data-i18n-title="common.logout" type="button">
Log Out
</button>
</div>
</header>
@@ -1,6 +1,8 @@
// ClawdDotNet — Sichere DOM-Helferfunktionen
// ClawdDotNet — safe DOM helpers
//
// Leitfaden-Regel 3.1: Fremdtext ist nie HTML. Kein innerHTML im gesamten Baum.
// Guide rule 3.1: foreign text is never HTML. No innerHTML anywhere in the tree.
import { t } from "./i18n.js";
const SVG_NS = "http://www.w3.org/2000/svg";
@@ -124,15 +126,15 @@ export function instanzZustandInfo(zustand) {
const z = (zustand || "").toLowerCase();
switch (z) {
case "running":
return { text: "Läuft", art: "erfolg" };
return { text: t("status.running"), art: "erfolg" };
case "stopped":
return { text: "Beendet", art: "neutral" };
return { text: t("status.stopped"), art: "neutral" };
case "silent":
return { text: "Verstummt", art: "warnung" };
return { text: t("status.silent"), art: "warnung" };
case "crashed":
return { text: "Abgestürzt", art: "gefahr" };
return { text: t("status.crashed"), art: "gefahr" };
default:
return { text: zustand || "Unbekannt", art: "neutral" };
return { text: zustand || t("status.unknown"), art: "neutral" };
}
}
@@ -189,7 +191,7 @@ export function erstelleModal(titel, inhalt, aktionen = [], optionen = {}) {
const h = erstelleElement("h2", { text: titel });
const schliessenBtn = erstelleElement(
"button",
{ klasse: "schaltflaeche-icon flach", title: "Schließen", type: "button" },
{ klasse: "schaltflaeche-icon flach", title: t("common.close"), type: "button" },
erstelleIcon(PFADE.schliessen, 16)
);
kopf.append(h, schliessenBtn);
@@ -43,6 +43,24 @@ export function currentLanguage() {
return activeLanguage;
}
/** Changes the active language, updates static markup, and dispatches a language-changed event. */
export async function setLanguage(wanted) {
try {
localStorage.setItem(LANGUAGE_KEY, wanted);
} catch {
// Storage blocked: proceed in-memory.
}
if (Object.keys(fallbackPack).length === 0) {
fallbackPack = await loadPack(DEFAULT_LANGUAGE);
}
activePack = wanted === DEFAULT_LANGUAGE ? fallbackPack : await loadPack(wanted);
activeLanguage = wanted;
document.documentElement.lang = wanted;
translateDocument();
window.dispatchEvent(new CustomEvent("clawd:language-changed", { detail: { language: wanted } }));
}
/**
* Text for a key. Placeholders in braces are replaced: t("runs.count", { n: 3 }).
* Values are inserted as plain text - callers put the result into textContent.
@@ -60,4 +78,7 @@ export function translateDocument(root = document) {
for (const element of root.querySelectorAll("[data-i18n-title]")) {
element.title = t(element.dataset.i18nTitle);
}
for (const element of root.querySelectorAll("[data-i18n-placeholder]")) {
element.placeholder = t(element.dataset.i18nPlaceholder);
}
}
@@ -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) {