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