Stufe 1 (#764): Agent bearbeiten, Lizenz und Einstellungen in der WebUI
- PUT /api/agents/{id} und /texts: Stammdaten, Grenzen, Budget, Identity/Soul/
Systemprompt (L6/L7); "Jetzt ausfuehren" in der Agentenansicht (L9)
- Lizenzschluessel ueber die Weboberflaeche (L1): erst vom Lizenzserver
bestaetigt, dann gespeichert; hebt die Sperre ohne Neustart auf
- GET/PUT /api/settings: Instanz- und Anwendungseinstellungen inkl.
OpenRouter-Key (L3/L4/L5), Geheimnisse nur schreibend
- GET/PUT /api/verwaltung/settings: Deploymentcenter und Update-Kanal des Managers
- Einstellungsseite zeigt echte Werte statt fester Texte
- NonInteractiveLicensePrompt verwies auf --license-set-key, den es nie gab
- Sprachpakete werden bei jedem Lauf geprueft (LanguagePackTests)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a9ce72abe2
commit
8552755bb6
@@ -74,6 +74,20 @@ public sealed record FtpSettingsUpdateDto(
|
||||
bool Enabled, string Host, int Port, string Username, string? Password,
|
||||
string RemoteDirectory, bool RequireTls, int KeepCount);
|
||||
|
||||
/// <summary>#764: The license as the web interface sees it — never the key, only whether one is set.</summary>
|
||||
public sealed record LicenseStatusDto(bool Enforced, bool Holds, bool KeySet, string? HardwareId);
|
||||
|
||||
/// <summary>#764: A license key entered in the web interface.</summary>
|
||||
public sealed record LicenseKeyDto(string Key);
|
||||
|
||||
/// <summary>#764: The manager's own settings — without the master token, only whether one is set.</summary>
|
||||
public sealed record ManagerSettingsDto(
|
||||
string ConfigFile, string InstancesDirectory, int WebUiPort, string WebUiBinding,
|
||||
string DeploymentcenterUrl, bool MasterTokenSet, string UpdateChannel);
|
||||
|
||||
/// <summary>#764: A change to the manager's settings. <c>null</c> keeps a value, an empty token too.</summary>
|
||||
public sealed record ManagerSettingsUpdateDto(string? DeploymentcenterUrl, string? MasterToken, string? UpdateChannel);
|
||||
|
||||
/// <summary>
|
||||
/// Die HTTP-Fläche des Managers.
|
||||
///
|
||||
@@ -368,6 +382,37 @@ internal static class ManagerEndpoints
|
||||
}
|
||||
});
|
||||
|
||||
// ─── License and manager settings (#764) ───
|
||||
|
||||
verwaltung.MapGet("/license", (ManagerCore manager) =>
|
||||
Results.Ok(new LicenseStatusDto(
|
||||
manager.LicenseEnforced, manager.LicenseHolds,
|
||||
!string.IsNullOrWhiteSpace(manager.Settings.LicenseKey), manager.LicenseHardwareId)));
|
||||
|
||||
// The key goes to the license server before it is stored; a refused key leaves the
|
||||
// current one in place (LicenseGate.ApplyKeyAsync).
|
||||
verwaltung.MapPut("/license", async (LicenseKeyDto input, ManagerCore manager, CancellationToken ct) =>
|
||||
{
|
||||
var result = await manager.ApplyLicenseKeyAsync(input.Key ?? "", ct);
|
||||
return result.Applied
|
||||
? Results.Ok(new OutcomeDto(true, result.Message))
|
||||
: Results.BadRequest(new OutcomeDto(false, result.Message));
|
||||
});
|
||||
|
||||
verwaltung.MapGet("/settings", (ManagerCore manager) =>
|
||||
{
|
||||
var s = manager.Settings;
|
||||
return Results.Ok(new ManagerSettingsDto(
|
||||
manager.SettingsPath, s.InstancesDirectory, s.WebUiPort, s.WebUiBinding,
|
||||
s.DeploymentcenterUrl, !string.IsNullOrWhiteSpace(s.DeploymentcenterMasterToken), s.UpdateChannel));
|
||||
});
|
||||
|
||||
verwaltung.MapPut("/settings", (ManagerSettingsUpdateDto input, ManagerCore manager) =>
|
||||
{
|
||||
var (ok, message) = manager.UpdateSettings(input.DeploymentcenterUrl, input.MasterToken, input.UpdateChannel);
|
||||
return ok ? Results.Ok(new OutcomeDto(true, message)) : Results.BadRequest(new OutcomeDto(false, message));
|
||||
});
|
||||
|
||||
// ─── FTP-Upload der Sicherungen ───
|
||||
|
||||
verwaltung.MapGet("/sicherung/ftp", (ManagerBackups backups) =>
|
||||
|
||||
@@ -1517,6 +1517,14 @@ button.klein {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* #764: Settings cards side by side where there is room, stacked on a phone. */
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* ─── Responsive Optimierung (Handy / 375px) ─── */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@@ -417,6 +417,39 @@
|
||||
"agents.treatAsForeignReader": "Immer als Leser fremder Inhalte behandeln",
|
||||
"agents.treatAsForeignReaderHint": "Kann nur verschärfen. Die Stufe folgt aus den Werkzeugen. Diese Einstellung setzt sie für einen Agenten, dessen Eingaben auf anderem Weg von außen kommen.",
|
||||
"agents.foreignReaderSaved": "Berichtsweg von {name} gespeichert.",
|
||||
"agents.edit": "Bearbeiten",
|
||||
"agents.editTooltip": "Name, Modell, Grenzen, Budget und Prompttexte bearbeiten",
|
||||
"agents.editTitle": "Agent bearbeiten — {name}",
|
||||
"agents.saved": "Agent gespeichert.",
|
||||
"agents.runNow": "Jetzt ausführen",
|
||||
"agents.runNowTooltip": "Agent von Hand wecken, wie es ein geplanter Lauf täte",
|
||||
"agents.runStarted": "{name} läuft.",
|
||||
"agents.runFailed": "Lauf fehlgeschlagen: {error}",
|
||||
"agents.limits": "Grenzen",
|
||||
"agents.limitsValue": "{steps} Schritte · {context} Kontext-Tokens · {seconds} s",
|
||||
"agents.budget": "Tagesbudget",
|
||||
"agents.budgetValue": "{cost} · {tokens} Tokens",
|
||||
"agents.unlimited": "unbegrenzt",
|
||||
"agents.promptTextsHint": "Identity, Soul und Systemprompt liest das Modell vor jedem Lauf. Wer sie ändert, ändert, wie der Agent arbeitet.",
|
||||
"agents.field.displayName": "Anzeigename",
|
||||
"agents.field.model": "Modell",
|
||||
"agents.field.description": "Beschreibung",
|
||||
"agents.field.identity": "Identity",
|
||||
"agents.field.soul": "Soul",
|
||||
"agents.field.systemPrompt": "Systemprompt",
|
||||
"agents.field.promptCaching": "Prompt-Caching",
|
||||
"agents.field.maxSteps": "Max. Schritte je Lauf",
|
||||
"agents.field.maxContextTokens": "Max. Kontext-Tokens",
|
||||
"agents.field.maxToolResultChars": "Max. Zeichen je Werkzeugergebnis",
|
||||
"agents.field.timeoutSeconds": "Zeitlimit je Lauf (Sekunden)",
|
||||
"agents.field.dailyCostUsd": "Tagesbudget (USD, 0 = unbegrenzt)",
|
||||
"agents.field.dailyTokens": "Tages-Tokens (0 = unbegrenzt)",
|
||||
"agents.field.whenExhausted": "Wenn das Budget erschöpft ist",
|
||||
"agents.option.auto": "Automatisch",
|
||||
"agents.option.on": "An",
|
||||
"agents.option.off": "Aus",
|
||||
"agents.option.expire": "Lauf entfällt",
|
||||
"agents.option.catchUpOnce": "Einmal nachholen",
|
||||
|
||||
"tasks.title": "Aufgaben",
|
||||
"tasks.newTask": "Neue Aufgabe",
|
||||
@@ -558,34 +591,51 @@
|
||||
"logs.empty": "Noch keine Protokolleinträge vorhanden.",
|
||||
|
||||
"settings.title": "Einstellungen & Systemarchitektur",
|
||||
"settings.license": "Lizenz",
|
||||
"settings.licenseState": "Zustand",
|
||||
"settings.licenseValid": "Gültig",
|
||||
"settings.licenseInvalid": "Nicht gültig",
|
||||
"settings.licenseMissing": "Kein Schlüssel",
|
||||
"settings.licenseNoCheck": "Keine Lizenzprüfung",
|
||||
"settings.licenseUnavailable": "Der Lizenzzustand ließ sich nicht lesen.",
|
||||
"settings.licenseKeyHint": "Der Schlüssel wird vor dem Speichern beim Lizenzserver geprüft. Ein abgelehnter Schlüssel lässt den bisherigen stehen. Eine Lizenz gilt für alle Instanzen dieser Installation.",
|
||||
"settings.hardwareId": "Hardware-ID",
|
||||
"settings.secretSet": "Hinterlegt — leer lassen, um ihn zu behalten",
|
||||
"settings.secretNotSet": "Nicht gesetzt",
|
||||
"settings.managerCard": "Instanzmanager",
|
||||
"settings.instancesDirectory": "Instanzverzeichnis",
|
||||
"settings.webUi": "Weboberfläche",
|
||||
"settings.dcUrl": "Deploymentcenter-Adresse",
|
||||
"settings.dcMasterTokenHint": "Damit stellt der Manager jeder Instanz ein eigenes Melde-Token aus. Nur ein leeres Feld behält das gespeicherte Token.",
|
||||
"settings.managerRestartHint": "Änderungen hier greifen, wenn der Instanzmanager neu startet. Port, Bindung und Zugangstoken der Weboberfläche stehen in manager.json — ein falscher Wert hier sperrte diese Seite aus.",
|
||||
"settings.instanceStopped": "Instanz läuft nicht",
|
||||
"settings.instanceStoppedHint": "Die Instanzdatei gehört dem Instanzprozess und wird nur bearbeitet, solange er läuft. Instanz starten, um ihre Einstellungen zu ändern.",
|
||||
"settings.instanceCard": "Instanz",
|
||||
"settings.logDirectory": "Protokollverzeichnis",
|
||||
"settings.logDirectoryHint": "Leer nimmt die Vorgabe. Derzeit: {path}. Greift nach einem Neustart.",
|
||||
"settings.instanceDailyCost": "Tagesbudget der Instanz (USD, 0 = unbegrenzt)",
|
||||
"settings.instanceDailyTokens": "Tages-Tokens der Instanz (0 = unbegrenzt)",
|
||||
"settings.budgetHint": "Über alle Agenten dieser Instanz. Agenten haben zusätzlich eigene Budgets.",
|
||||
"settings.apiKeyHint": "Ohne Schlüssel startet die Agent-Engine nicht. Ein neuer Schlüssel greift nach einem Neustart der Instanz.",
|
||||
"settings.applicationCard": "Anwendung (alle Instanzen)",
|
||||
"settings.applicationHint": "settings.json auf dieser Maschine, von jeder Instanz beim Start gelesen.",
|
||||
"settings.minimumLogLevel": "Mindest-Protokollstufe",
|
||||
"settings.openRouterBaseUrl": "OpenRouter-Adresse",
|
||||
"settings.statusCheckInterval": "Statusprüfung alle (Sekunden)",
|
||||
"settings.dcToken": "Deploymentcenter-Token",
|
||||
"settings.dcEnvironment": "Umgebung",
|
||||
"settings.errorReporting": "Fehler an das Deploymentcenter melden",
|
||||
"settings.on": "An",
|
||||
"settings.off": "Aus",
|
||||
"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",
|
||||
@@ -594,41 +644,20 @@
|
||||
"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",
|
||||
|
||||
@@ -417,6 +417,39 @@
|
||||
"agents.treatAsForeignReader": "Always treat as reading outside content",
|
||||
"agents.treatAsForeignReaderHint": "Can only tighten. The tools decide the tier; this adds it for an agent whose input comes from outside some other way.",
|
||||
"agents.foreignReaderSaved": "Reporting line of {name} saved.",
|
||||
"agents.edit": "Edit",
|
||||
"agents.editTooltip": "Edit name, model, limits, budget and prompt texts",
|
||||
"agents.editTitle": "Edit agent — {name}",
|
||||
"agents.saved": "Agent saved.",
|
||||
"agents.runNow": "Run now",
|
||||
"agents.runNowTooltip": "Wake the agent by hand, as a scheduled run would",
|
||||
"agents.runStarted": "{name} is running.",
|
||||
"agents.runFailed": "Run failed: {error}",
|
||||
"agents.limits": "Limits",
|
||||
"agents.limitsValue": "{steps} steps · {context} context tokens · {seconds} s",
|
||||
"agents.budget": "Daily budget",
|
||||
"agents.budgetValue": "{cost} · {tokens} tokens",
|
||||
"agents.unlimited": "unlimited",
|
||||
"agents.promptTextsHint": "Identity, Soul and the system prompt are what the model reads before every run. Changing them changes how the agent works.",
|
||||
"agents.field.displayName": "Display name",
|
||||
"agents.field.model": "Model",
|
||||
"agents.field.description": "Description",
|
||||
"agents.field.identity": "Identity",
|
||||
"agents.field.soul": "Soul",
|
||||
"agents.field.systemPrompt": "System prompt",
|
||||
"agents.field.promptCaching": "Prompt caching",
|
||||
"agents.field.maxSteps": "Max. steps per run",
|
||||
"agents.field.maxContextTokens": "Max. context tokens",
|
||||
"agents.field.maxToolResultChars": "Max. characters per tool result",
|
||||
"agents.field.timeoutSeconds": "Time limit per run (seconds)",
|
||||
"agents.field.dailyCostUsd": "Daily budget (USD, 0 = unlimited)",
|
||||
"agents.field.dailyTokens": "Daily tokens (0 = unlimited)",
|
||||
"agents.field.whenExhausted": "When the budget is exhausted",
|
||||
"agents.option.auto": "Automatic",
|
||||
"agents.option.on": "On",
|
||||
"agents.option.off": "Off",
|
||||
"agents.option.expire": "Skip the run",
|
||||
"agents.option.catchUpOnce": "Catch up once",
|
||||
|
||||
"tasks.title": "Tasks",
|
||||
"tasks.newTask": "New Task",
|
||||
@@ -558,34 +591,51 @@
|
||||
"logs.empty": "No log messages yet.",
|
||||
|
||||
"settings.title": "Settings & System Architecture",
|
||||
"settings.license": "License",
|
||||
"settings.licenseState": "State",
|
||||
"settings.licenseValid": "Valid",
|
||||
"settings.licenseInvalid": "Not valid",
|
||||
"settings.licenseMissing": "No key",
|
||||
"settings.licenseNoCheck": "No license check",
|
||||
"settings.licenseUnavailable": "The license state could not be read.",
|
||||
"settings.licenseKeyHint": "The key is checked with the license server before it is stored. A refused key leaves the current one in place. One license covers every instance of this installation.",
|
||||
"settings.hardwareId": "Hardware ID",
|
||||
"settings.secretSet": "Stored — leave empty to keep",
|
||||
"settings.secretNotSet": "Not set",
|
||||
"settings.managerCard": "Instance manager",
|
||||
"settings.instancesDirectory": "Instances directory",
|
||||
"settings.webUi": "Web interface",
|
||||
"settings.dcUrl": "Deploymentcenter address",
|
||||
"settings.dcMasterTokenHint": "Used to issue each instance its own reporting token. Only an empty field keeps the stored token.",
|
||||
"settings.managerRestartHint": "Changes here take effect when the instance manager restarts. Port, binding and access token of the web interface are changed in manager.json — a wrong value here would lock this page out.",
|
||||
"settings.instanceStopped": "Instance not running",
|
||||
"settings.instanceStoppedHint": "The instance file belongs to the instance process and is only edited while it runs. Start the instance to change its settings.",
|
||||
"settings.instanceCard": "Instance",
|
||||
"settings.logDirectory": "Log directory",
|
||||
"settings.logDirectoryHint": "Empty uses the default. Currently: {path}. Takes effect after a restart.",
|
||||
"settings.instanceDailyCost": "Daily budget of the instance (USD, 0 = unlimited)",
|
||||
"settings.instanceDailyTokens": "Daily tokens of the instance (0 = unlimited)",
|
||||
"settings.budgetHint": "Across all agents of this instance. Agents also have their own budgets.",
|
||||
"settings.apiKeyHint": "Without a key the agent engine does not start. A new key takes effect after the instance restarts.",
|
||||
"settings.applicationCard": "Application (all instances)",
|
||||
"settings.applicationHint": "settings.json on this machine, read by every instance when it starts.",
|
||||
"settings.minimumLogLevel": "Minimum log level",
|
||||
"settings.openRouterBaseUrl": "OpenRouter address",
|
||||
"settings.statusCheckInterval": "Status check interval (seconds)",
|
||||
"settings.dcToken": "Deploymentcenter token",
|
||||
"settings.dcEnvironment": "Environment",
|
||||
"settings.errorReporting": "Report errors to the Deploymentcenter",
|
||||
"settings.on": "On",
|
||||
"settings.off": "Off",
|
||||
"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",
|
||||
@@ -594,41 +644,20 @@
|
||||
"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",
|
||||
|
||||
@@ -186,7 +186,31 @@ async function rendereDetails(agentId) {
|
||||
erstelleElement("span", { text: ` ${t("agents.delete")}` })
|
||||
);
|
||||
|
||||
aktionen.append(zumChatBtn, loeschenBtn);
|
||||
// #764: What the desktop agent page could do — edit the agent and wake it by hand.
|
||||
const editButton = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: t("agents.editTooltip"),
|
||||
onclick: () => openEditDialog(agent)
|
||||
},
|
||||
erstelleIcon(PFADE.einstellungen, 14),
|
||||
erstelleElement("span", { text: ` ${t("agents.edit")}` })
|
||||
);
|
||||
|
||||
const runButton = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: t("agents.runNowTooltip"),
|
||||
disabled: Boolean(agent.isRunning),
|
||||
onclick: () => runAgentNow(agent, runButton)
|
||||
},
|
||||
erstelleIcon(PFADE.abspielen, 14),
|
||||
erstelleElement("span", { text: ` ${t("agents.runNow")}` })
|
||||
);
|
||||
|
||||
aktionen.append(editButton, runButton, zumChatBtn, loeschenBtn);
|
||||
|
||||
// Stammdaten-Karte
|
||||
const stammdatenListe = erstelleElement("dl", { klasse: "datenliste" });
|
||||
@@ -194,7 +218,13 @@ async function rendereDetails(agentId) {
|
||||
[t("agents.name"), agent.displayName],
|
||||
[t("agents.id"), agent.agentId],
|
||||
[t("agents.model"), agent.model],
|
||||
[t("agents.description"), agent.description || "—"]
|
||||
[t("agents.description"), agent.description || "—"],
|
||||
[t("agents.limits"), t("agents.limitsValue", {
|
||||
steps: agent.maxSteps,
|
||||
context: agent.maxContextTokens,
|
||||
seconds: agent.timeoutSeconds
|
||||
})],
|
||||
[t("agents.budget"), formatBudget(agent)]
|
||||
];
|
||||
|
||||
for (const [bez, wert] of felder) {
|
||||
@@ -433,6 +463,127 @@ function oeffneNeuerAgentDialog() {
|
||||
modal.oeffnen();
|
||||
}
|
||||
|
||||
function formatBudget(agent) {
|
||||
const cost = Number(agent.dailyCostUsd) > 0 ? `$${Number(agent.dailyCostUsd).toFixed(2)}` : t("agents.unlimited");
|
||||
const tokens = Number(agent.dailyTokens) > 0 ? Number(agent.dailyTokens).toLocaleString() : t("agents.unlimited");
|
||||
return t("agents.budgetValue", { cost, tokens });
|
||||
}
|
||||
|
||||
async function runAgentNow(agent, button) {
|
||||
button.disabled = true;
|
||||
try {
|
||||
const result = await api(`/agents/${encodeURIComponent(agent.agentId)}/run`, { method: "POST" });
|
||||
zeigeMeldung(result?.message ?? t("agents.runStarted", { name: agent.displayName }), "info");
|
||||
} catch (err) {
|
||||
zeigeMeldung(t("agents.runFailed", { error: err.message }), "gefahr");
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* #764: Edits an agent — master data, limits, budget and the three prompt texts. The long
|
||||
* texts are fetched for this agent only; the agent list does not carry them.
|
||||
*/
|
||||
async function openEditDialog(agent) {
|
||||
let texts;
|
||||
try {
|
||||
texts = await api(`/agents/${encodeURIComponent(agent.agentId)}/texts`);
|
||||
} catch (err) {
|
||||
zeigeMeldung(t("agents.loadFailed", { error: err.message }), "gefahr");
|
||||
return;
|
||||
}
|
||||
|
||||
const form = erstelleElement("form", { klasse: "formular-raster", id: "agent-edit-form" });
|
||||
|
||||
const textInput = (value) => erstelleElement("input", { type: "text", value: value ?? "" });
|
||||
const numberInput = (value, min, step = "1") =>
|
||||
erstelleElement("input", { type: "number", min: String(min), step, value: String(value ?? "") });
|
||||
const textArea = (value, rows) => {
|
||||
const area = erstelleElement("textarea", { rows: String(rows) });
|
||||
area.value = value ?? "";
|
||||
return area;
|
||||
};
|
||||
const select = (options, value) => {
|
||||
const el = erstelleElement("select", {},
|
||||
...options.map((o) => erstelleElement("option", { value: o, text: t(`agents.option.${o}`) })));
|
||||
el.value = value;
|
||||
return el;
|
||||
};
|
||||
|
||||
const fields = {
|
||||
displayName: textInput(agent.displayName),
|
||||
model: textInput(agent.model),
|
||||
description: textArea(agent.description, 2),
|
||||
identity: textArea(texts.identity, 8),
|
||||
soul: textArea(texts.soul, 8),
|
||||
systemPrompt: textArea(texts.systemPrompt, 6),
|
||||
promptCaching: select(["auto", "on", "off"], agent.promptCaching || "auto"),
|
||||
maxSteps: numberInput(agent.maxSteps, 1),
|
||||
maxContextTokens: numberInput(agent.maxContextTokens, 1000),
|
||||
maxToolResultChars: numberInput(agent.maxToolResultChars, 100),
|
||||
timeoutSeconds: numberInput(agent.timeoutSeconds, 10),
|
||||
dailyCostUsd: numberInput(agent.dailyCostUsd, 0, "0.01"),
|
||||
dailyTokens: numberInput(agent.dailyTokens, 0),
|
||||
whenExhausted: select(["expire", "catchUpOnce"], agent.whenExhausted || "expire")
|
||||
};
|
||||
fields.displayName.required = true;
|
||||
fields.model.required = true;
|
||||
|
||||
for (const [key, input] of Object.entries(fields)) {
|
||||
form.append(erstelleElement("label", { text: t(`agents.field.${key}`) }), input);
|
||||
}
|
||||
form.append(erstelleElement("p", { klasse: "hinweis", text: t("agents.promptTextsHint") }));
|
||||
|
||||
const cancelButton = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
text: t("common.cancel"),
|
||||
onclick: () => modal.schliessen()
|
||||
});
|
||||
const saveButton = erstelleElement("button", {
|
||||
type: "submit",
|
||||
klasse: "schaltflaeche primaer",
|
||||
text: t("common.save")
|
||||
});
|
||||
|
||||
const modal = erstelleModal(t("agents.editTitle", { name: agent.displayName }), form, [cancelButton, saveButton], { klasse: "modal-breit" });
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const body = {
|
||||
displayName: fields.displayName.value.trim(),
|
||||
model: fields.model.value.trim(),
|
||||
description: fields.description.value,
|
||||
identity: fields.identity.value,
|
||||
soul: fields.soul.value,
|
||||
systemPrompt: fields.systemPrompt.value,
|
||||
promptCaching: fields.promptCaching.value,
|
||||
maxSteps: Number(fields.maxSteps.value),
|
||||
maxContextTokens: Number(fields.maxContextTokens.value),
|
||||
maxToolResultChars: Number(fields.maxToolResultChars.value),
|
||||
timeoutSeconds: Number(fields.timeoutSeconds.value),
|
||||
dailyCostUsd: Number(fields.dailyCostUsd.value),
|
||||
dailyTokens: Number(fields.dailyTokens.value),
|
||||
whenExhausted: fields.whenExhausted.value
|
||||
};
|
||||
|
||||
saveButton.disabled = true;
|
||||
try {
|
||||
const result = await api(`/agents/${encodeURIComponent(agent.agentId)}`, { method: "PUT", body });
|
||||
modal.schliessen();
|
||||
zeigeMeldung(result?.message ?? t("agents.saved"), "erfolg");
|
||||
await ladeUndRendereListe(agent.agentId);
|
||||
} catch (err) {
|
||||
zeigeMeldung(t("agents.saveFailed", { error: err.message }), "gefahr");
|
||||
} finally {
|
||||
saveButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
modal.oeffnen();
|
||||
}
|
||||
|
||||
async function loescheAgent(agentId, name) {
|
||||
if (!confirm(t("agents.deleteConfirm", { name, id: agentId }))) return;
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
// ClawdDotNet — Einstellungen & Systemarchitektur
|
||||
// ClawdDotNet — Settings (#764)
|
||||
//
|
||||
// Strikte Trennung zwischen globaler Manager-Ebene (Installation) und isolierter Instanz-Ebene.
|
||||
// Two levels, kept apart on purpose:
|
||||
// - the instance manager (manager.json): license, Deploymentcenter master token, update channel;
|
||||
// - the active instance (InstanceSettings.json, owned by the instance process) and the
|
||||
// application file settings.json that every instance on this machine reads.
|
||||
//
|
||||
// Until #764 this page only showed fixed text — the values could be edited in the desktop window
|
||||
// alone. Secrets (license key, tokens, OpenRouter key) are written here and never shown: the API
|
||||
// returns only whether one is set, and an empty field keeps the stored value.
|
||||
|
||||
import { api, holeAktiveInstanz } from "../modules/api.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
@@ -10,6 +17,7 @@ import {
|
||||
erstellePlakette,
|
||||
instanzZustandInfo,
|
||||
erstelleIcon,
|
||||
zeigeMeldung,
|
||||
PFADE
|
||||
} from "../modules/dom.js";
|
||||
import { erstelleMcpServerKarte } from "./mcp-servers.js";
|
||||
@@ -21,240 +29,306 @@ export async function rendereEinstellungen() {
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "einstellungen-ansicht" });
|
||||
|
||||
// Kopfbereich
|
||||
const kopf = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
const kopfText = erstelleElement("div");
|
||||
kopfText.append(
|
||||
const head = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
head.append(erstelleElement("div", {},
|
||||
erstelleElement("h1", { text: t("settings.title") }),
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: t("settings.intro")
|
||||
})
|
||||
);
|
||||
kopf.append(kopfText);
|
||||
layout.append(kopf);
|
||||
erstelleElement("p", { klasse: "hinweis", text: t("settings.intro") })));
|
||||
layout.append(head);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// SEKTION 1: Instanzmanager (Globale Ebene / Installation)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
const managerSektion = erstelleElement("section", { klasse: "einstellungen-sektion sektion-manager" });
|
||||
|
||||
const managerKopf = erstelleElement("div", { klasse: "sektion-titel-zeile" });
|
||||
const managerTitelLinks = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
managerTitelLinks.append(
|
||||
erstelleIcon(PFADE.server, 20),
|
||||
erstelleElement("h2", { text: t("settings.managerLevel") }),
|
||||
erstellePlakette(t("settings.managerBadge"), "warnung")
|
||||
);
|
||||
managerKopf.append(managerTitelLinks);
|
||||
|
||||
const managerIntro = erstelleElement("p", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: t("settings.managerIntro")
|
||||
});
|
||||
|
||||
const managerGitter = erstelleElement("div", { klasse: "info-gitter" });
|
||||
|
||||
// Karte 1: Weboberfläche & Netzwerk
|
||||
const dlNetz = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlNetz.append(
|
||||
erstelleElement("dt", { text: t("settings.configFile") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: "manager.json" })),
|
||||
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(t("settings.webUiAndNet"), [dlNetz]));
|
||||
|
||||
// Karte 2: Lizenz & Programm-Updates
|
||||
const dlLizenz = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlLizenz.append(
|
||||
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(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: 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(t("settings.dcAndOversight"), [dlAufsicht]));
|
||||
|
||||
managerSektion.append(managerKopf, managerIntro, managerGitter);
|
||||
layout.append(managerSektion);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// SEKTION 2: Aktive Instanz (Instanz-Ebene)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
const instanzSektion = erstelleElement("section", { klasse: "einstellungen-sektion sektion-instanz" });
|
||||
|
||||
const aktiveId = holeAktiveInstanz();
|
||||
let instanzenListe = [];
|
||||
try {
|
||||
instanzenListe = await api("/verwaltung/instanzen");
|
||||
} catch {
|
||||
// Wenn Liste nicht abrufbar, weiter
|
||||
}
|
||||
|
||||
const aktiveInstanz = instanzenListe.find((i) => i.id === aktiveId);
|
||||
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
|
||||
? 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: t("settings.instanceIntro")
|
||||
});
|
||||
|
||||
instanzSektion.append(instanzKopf, instanzIntro);
|
||||
|
||||
if (!aktiveInstanz) {
|
||||
const leerKarte = erstelleKarte(t("settings.noInstanceSelected"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: t("settings.noInstancePrompt")
|
||||
}),
|
||||
erstelleElement("a", {
|
||||
href: "#/instanzen",
|
||||
klasse: "schaltflaeche primaer klein",
|
||||
text: t("settings.toInstancesOverview")
|
||||
})
|
||||
]);
|
||||
instanzSektion.append(leerKarte);
|
||||
} else {
|
||||
// Daten von der laufenden Instanz abfragen (sofern sie läuft)
|
||||
const [instanzInfo, instanzStatus] = await Promise.all([
|
||||
api("/info").catch(() => null),
|
||||
api("/status").catch(() => null)
|
||||
]);
|
||||
|
||||
const instanzGitter = erstelleElement("div", { klasse: "info-gitter" });
|
||||
|
||||
// Karte 1: Instanz-Identität & Autostart
|
||||
const dlIdent = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlIdent.append(
|
||||
erstelleElement("dt", { text: t("settings.instanceName") }),
|
||||
erstelleElement("dd", {}, erstelleElement("strong", { text: aktiveInstanz.name })),
|
||||
erstelleElement("dt", { text: t("settings.instanceId") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: aktiveInstanz.id })),
|
||||
erstelleElement("dt", { text: t("settings.configFile") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/InstanceSettings.json` })),
|
||||
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(t("settings.identityAndProcess"), [dlIdent]));
|
||||
|
||||
// Karte 2: Isolierte Verzeichnisse & Speicher
|
||||
const dlPfade = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlPfade.append(
|
||||
erstelleElement("dt", { text: t("settings.logsPath") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/Logs/` })),
|
||||
erstelleElement("dt", { text: t("settings.backupsPath") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/Backups/` })),
|
||||
erstelleElement("dt", { text: t("settings.stateDbPath") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/state.db (exklusiv)` })),
|
||||
erstelleElement("dt", { text: t("settings.workspacePath") }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/Agents/SharedWorkspace/` }))
|
||||
);
|
||||
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: 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: t("settings.credits") }),
|
||||
erstelleElement("dd", {}, erstelleElement("strong", { text: instanzStatus.creditsText || "—" })),
|
||||
erstelleElement("dt", { text: t("settings.apiKey") }),
|
||||
erstelleElement("dd", { text: t("settings.apiKeyMasked") })
|
||||
);
|
||||
} else {
|
||||
dlModell.append(
|
||||
erstelleElement("dt", { text: "OpenRouter" }),
|
||||
erstelleElement("dd", {
|
||||
klasse: "gedaempft",
|
||||
text: aktiveInstanz.zustand === "Running"
|
||||
? t("settings.openRouterNoKey")
|
||||
: t("settings.openRouterNotRunning")
|
||||
})
|
||||
);
|
||||
}
|
||||
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: t("settings.agentEngine") }),
|
||||
erstelleElement("dd", {}, erstellePlakette(instanzInfo.engineActive ? "Aktiv" : "Aus", instanzInfo.engineActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: t("settings.scheduler") }),
|
||||
erstelleElement("dd", {}, erstellePlakette(instanzInfo.schedulerActive ? "Aktiv" : "Aus", instanzInfo.schedulerActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: t("settings.stagingService") }),
|
||||
erstelleElement("dd", {}, erstellePlakette(instanzInfo.stagingActive ? "Aktiv" : "Aus", instanzInfo.stagingActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: t("settings.dcSubToken") }),
|
||||
erstelleElement("dd", {}, erstellePlakette(instanzInfo.deploymentcenterActive ? "Aktiv" : "Aus", instanzInfo.deploymentcenterActive ? "erfolg" : "neutral"))
|
||||
);
|
||||
} else {
|
||||
dlSysteme.append(
|
||||
erstelleElement("dt", { text: t("info.subsystems") }),
|
||||
erstelleElement("dd", {
|
||||
klasse: "gedaempft",
|
||||
text: aktiveInstanz.zustand === "Running"
|
||||
? t("settings.subsystemsLoading")
|
||||
: t("settings.subsystemsStopped")
|
||||
})
|
||||
);
|
||||
}
|
||||
instanzGitter.append(erstelleKarte(t("settings.runtimeAndSubsystems"), [dlSysteme]));
|
||||
|
||||
instanzSektion.append(instanzGitter);
|
||||
|
||||
// #197: external MCP servers — only a running instance knows them.
|
||||
if (instanzInfo) instanzSektion.append(erstelleMcpServerKarte());
|
||||
}
|
||||
|
||||
layout.append(instanzSektion);
|
||||
layout.append(await managerSection(), await instanceSection());
|
||||
container.append(layout);
|
||||
}
|
||||
|
||||
// ─── Building blocks ───
|
||||
|
||||
function section(kind, icon, title, badges, intro) {
|
||||
const el = erstelleElement("section", { klasse: `einstellungen-sektion ${kind}` });
|
||||
el.append(
|
||||
erstelleElement("div", { klasse: "sektion-titel-zeile" },
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" },
|
||||
erstelleIcon(icon, 20), erstelleElement("h2", { text: title }), ...badges)),
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: intro }));
|
||||
return el;
|
||||
}
|
||||
|
||||
function textInput(value, type = "text") {
|
||||
return erstelleElement("input", { type, value: value ?? "" });
|
||||
}
|
||||
|
||||
function numberInput(value, min, step = "1") {
|
||||
return erstelleElement("input", { type: "number", min: String(min), step, value: String(value ?? 0) });
|
||||
}
|
||||
|
||||
/** A secret is never shown; the placeholder says whether one is stored. */
|
||||
function secretInput(isSet) {
|
||||
return erstelleElement("input", {
|
||||
type: "password",
|
||||
autocomplete: "new-password",
|
||||
placeholder: isSet ? t("settings.secretSet") : t("settings.secretNotSet")
|
||||
});
|
||||
}
|
||||
|
||||
function selectInput(options, value) {
|
||||
const el = erstelleElement("select", {}, ...options.map((o) => erstelleElement("option", { value: o, text: o })));
|
||||
el.value = value;
|
||||
return el;
|
||||
}
|
||||
|
||||
function checkbox(checked) {
|
||||
return erstelleElement("input", { type: "checkbox", checked: Boolean(checked) });
|
||||
}
|
||||
|
||||
/** A form of labelled fields with a save button that sends what `collect` returns. */
|
||||
function settingsForm(rows, path, collect, afterSave) {
|
||||
const form = erstelleElement("form", { klasse: "formular-raster" });
|
||||
for (const [label, input, hint] of rows) {
|
||||
if (input.type === "checkbox") {
|
||||
form.append(erstelleElement("label", { klasse: "gruppe-horizontal" }, input, erstelleElement("span", { text: label })));
|
||||
} else {
|
||||
form.append(erstelleElement("label", { text: label }), input);
|
||||
}
|
||||
if (hint) form.append(erstelleElement("div", { klasse: "gedaempft text-klein", text: hint }));
|
||||
}
|
||||
|
||||
const save = erstelleElement("button", { type: "submit", klasse: "schaltflaeche primaer klein", text: t("common.save") });
|
||||
form.append(erstelleElement("div", {}, save));
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
save.disabled = true;
|
||||
try {
|
||||
const result = await api(path, { method: "PUT", body: collect() });
|
||||
zeigeMeldung(result?.message ?? result?.meldung ?? t("common.saved"), "erfolg", 6000);
|
||||
await afterSave?.();
|
||||
} catch (err) {
|
||||
zeigeMeldung(err.message, "gefahr", 8000);
|
||||
} finally {
|
||||
save.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
function dataList(pairs) {
|
||||
const dl = erstelleElement("dl", { klasse: "datenliste" });
|
||||
for (const [label, value] of pairs) {
|
||||
dl.append(erstelleElement("dt", { text: label }),
|
||||
value instanceof Node ? erstelleElement("dd", {}, value) : erstelleElement("dd", { text: value ?? "—" }));
|
||||
}
|
||||
return dl;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await rendereEinstellungen();
|
||||
}
|
||||
|
||||
// ─── Instance manager ───
|
||||
|
||||
async function managerSection() {
|
||||
const el = section("sektion-manager", PFADE.server, t("settings.managerLevel"),
|
||||
[erstellePlakette(t("settings.managerBadge"), "warnung")], t("settings.managerIntro"));
|
||||
|
||||
const [license, manager] = await Promise.all([
|
||||
api("/verwaltung/license").catch(() => null),
|
||||
api("/verwaltung/settings").catch(() => null)
|
||||
]);
|
||||
|
||||
const grid = erstelleElement("div", { klasse: "settings-grid" });
|
||||
grid.append(licenseCard(license));
|
||||
if (manager) grid.append(managerCard(manager));
|
||||
el.append(grid);
|
||||
return el;
|
||||
}
|
||||
|
||||
function licenseCard(license) {
|
||||
if (!license) {
|
||||
return erstelleKarte(t("settings.license"), [erstelleElement("p", { klasse: "gedaempft", text: t("settings.licenseUnavailable") })]);
|
||||
}
|
||||
|
||||
const state = !license.enforced
|
||||
? erstellePlakette(t("settings.licenseNoCheck"), "neutral")
|
||||
: license.holds
|
||||
? erstellePlakette(t("settings.licenseValid"), "erfolg")
|
||||
: erstellePlakette(license.keySet ? t("settings.licenseInvalid") : t("settings.licenseMissing"), "gefahr");
|
||||
|
||||
const key = secretInput(license.keySet);
|
||||
const form = settingsForm(
|
||||
[[t("settings.licenseKey"), key, t("settings.licenseKeyHint")]],
|
||||
"/verwaltung/license",
|
||||
() => ({ key: key.value }),
|
||||
refresh);
|
||||
|
||||
return erstelleKarte(t("settings.license"), [
|
||||
dataList([
|
||||
[t("settings.licenseState"), state],
|
||||
[t("settings.hardwareId"), erstelleElement("code", { text: license.hardwareId ?? "—" })]
|
||||
]),
|
||||
form
|
||||
]);
|
||||
}
|
||||
|
||||
function managerCard(manager) {
|
||||
const dcUrl = textInput(manager.deploymentcenterUrl, "url");
|
||||
const masterToken = secretInput(manager.masterTokenSet);
|
||||
const channel = selectInput(["prod", "beta", "dev"], manager.updateChannel);
|
||||
|
||||
return erstelleKarte(t("settings.managerCard"), [
|
||||
dataList([
|
||||
[t("settings.configFile"), erstelleElement("code", { text: manager.configFile })],
|
||||
[t("settings.instancesDirectory"), erstelleElement("code", { text: manager.instancesDirectory })],
|
||||
[t("settings.webUi"), `${manager.webUiBinding} · ${manager.webUiPort}`]
|
||||
]),
|
||||
settingsForm(
|
||||
[
|
||||
[t("settings.dcUrl"), dcUrl],
|
||||
[t("settings.dcMasterToken"), masterToken, t("settings.dcMasterTokenHint")],
|
||||
[t("settings.updateChannel"), channel]
|
||||
],
|
||||
"/verwaltung/settings",
|
||||
() => ({ deploymentcenterUrl: dcUrl.value.trim(), masterToken: masterToken.value, updateChannel: channel.value }),
|
||||
refresh),
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("settings.managerRestartHint") })
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── Active instance ───
|
||||
|
||||
async function instanceSection() {
|
||||
const activeId = holeAktiveInstanz();
|
||||
const instances = await api("/verwaltung/instanzen").catch(() => []);
|
||||
const active = instances.find((i) => i.id === activeId);
|
||||
const state = active ? instanzZustandInfo(active.zustand) : null;
|
||||
|
||||
const el = section("sektion-instanz", PFADE.fenster,
|
||||
active ? t("settings.instanceSectionTitle", { name: active.name }) : t("settings.instanceSectionTitleNone"),
|
||||
[erstellePlakette(t("settings.instanceBadge"), "erfolg"),
|
||||
state ? erstellePlakette(state.text, state.art) : erstellePlakette(t("settings.noInstanceActive"), "neutral")],
|
||||
t("settings.instanceIntro"));
|
||||
|
||||
if (!active) {
|
||||
el.append(erstelleKarte(t("settings.noInstanceSelected"), [
|
||||
erstelleElement("p", { klasse: "hinweis", text: t("settings.noInstancePrompt") }),
|
||||
erstelleElement("a", { href: "#/instanzen", klasse: "schaltflaeche primaer klein", text: t("settings.toInstancesOverview") })
|
||||
]));
|
||||
return el;
|
||||
}
|
||||
|
||||
const [settings, status, info] = await Promise.all([
|
||||
api("/settings").catch(() => null),
|
||||
api("/status").catch(() => null),
|
||||
api("/info").catch(() => null)
|
||||
]);
|
||||
|
||||
// The instance file belongs to the instance process: it is only written while it runs.
|
||||
if (!settings) {
|
||||
el.append(erstelleKarte(t("settings.instanceStopped"), [
|
||||
erstelleElement("p", { klasse: "hinweis", text: t("settings.instanceStoppedHint") })
|
||||
]));
|
||||
return el;
|
||||
}
|
||||
|
||||
const grid = erstelleElement("div", { klasse: "settings-grid" });
|
||||
grid.append(instanceCard(settings), openRouterCard(settings, status), applicationCard(settings));
|
||||
if (info) grid.append(subsystemsCard(info));
|
||||
el.append(grid);
|
||||
|
||||
// #197: external MCP servers — only a running instance knows them.
|
||||
if (info) el.append(erstelleMcpServerKarte());
|
||||
return el;
|
||||
}
|
||||
|
||||
function instanceCard(s) {
|
||||
const name = textInput(s.instanceName);
|
||||
const logDirectory = textInput(s.logDirectory);
|
||||
const autostart = checkbox(s.autostartByManager);
|
||||
const cost = numberInput(s.dailyCostUsd, 0, "0.01");
|
||||
const tokens = numberInput(s.dailyTokens, 0);
|
||||
|
||||
return erstelleKarte(t("settings.instanceCard"), [settingsForm(
|
||||
[
|
||||
[t("settings.instanceName"), name],
|
||||
[t("settings.logDirectory"), logDirectory, t("settings.logDirectoryHint", { path: s.effectiveLogDirectory })],
|
||||
[t("settings.autostartByManager"), autostart],
|
||||
[t("settings.instanceDailyCost"), cost],
|
||||
[t("settings.instanceDailyTokens"), tokens, t("settings.budgetHint")]
|
||||
],
|
||||
"/settings",
|
||||
() => ({
|
||||
instanceName: name.value.trim(),
|
||||
logDirectory: logDirectory.value.trim(),
|
||||
autostartByManager: autostart.checked,
|
||||
dailyCostUsd: Number(cost.value),
|
||||
dailyTokens: Number(tokens.value)
|
||||
}),
|
||||
refresh)]);
|
||||
}
|
||||
|
||||
function openRouterCard(s, status) {
|
||||
const key = secretInput(s.openRouterApiKeySet);
|
||||
const statusList = status
|
||||
? dataList([
|
||||
[t("settings.openRouterStatus"), erstellePlakette(
|
||||
status.apiReachable ? t("settings.connected") : t("settings.offline"),
|
||||
status.apiReachable ? "erfolg" : "gefahr")],
|
||||
[t("settings.credits"), status.creditsText || "—"]
|
||||
])
|
||||
: erstelleElement("p", { klasse: "gedaempft", text: t("settings.openRouterNoKey") });
|
||||
|
||||
return erstelleKarte(t("settings.openRouterCard"), [
|
||||
statusList,
|
||||
settingsForm([[t("settings.apiKey"), key, t("settings.apiKeyHint")]], "/settings",
|
||||
() => ({ openRouterApiKey: key.value }), refresh)
|
||||
]);
|
||||
}
|
||||
|
||||
function applicationCard(s) {
|
||||
const level = selectInput(["Debug", "Info", "Warn", "Error"], s.minimumLogLevel);
|
||||
const baseUrl = textInput(s.openRouterBaseUrl, "url");
|
||||
const interval = numberInput(s.statusCheckIntervalSeconds, 10);
|
||||
const dcUrl = textInput(s.deploymentcenterUrl, "url");
|
||||
const dcToken = secretInput(s.deploymentcenterTokenSet);
|
||||
const environment = selectInput(["production", "development", "staging"], s.deploymentcenterEnvironment);
|
||||
const reporting = checkbox(s.errorReportingEnabled);
|
||||
const updateCheck = checkbox(s.updateCheckEnabled);
|
||||
const channel = selectInput(["prod", "beta", "dev"], s.updateChannel);
|
||||
|
||||
return erstelleKarte(t("settings.applicationCard"), [
|
||||
erstelleElement("p", { klasse: "hinweis", text: t("settings.applicationHint") }),
|
||||
settingsForm(
|
||||
[
|
||||
[t("settings.minimumLogLevel"), level],
|
||||
[t("settings.openRouterBaseUrl"), baseUrl],
|
||||
[t("settings.statusCheckInterval"), interval],
|
||||
[t("settings.dcUrl"), dcUrl],
|
||||
[t("settings.dcToken"), dcToken],
|
||||
[t("settings.dcEnvironment"), environment],
|
||||
[t("settings.errorReporting"), reporting],
|
||||
[t("settings.updateCheck"), updateCheck],
|
||||
[t("settings.updateChannel"), channel]
|
||||
],
|
||||
"/settings",
|
||||
() => ({
|
||||
minimumLogLevel: level.value,
|
||||
openRouterBaseUrl: baseUrl.value.trim(),
|
||||
statusCheckIntervalSeconds: Number(interval.value),
|
||||
deploymentcenterUrl: dcUrl.value.trim(),
|
||||
deploymentcenterToken: dcToken.value,
|
||||
deploymentcenterEnvironment: environment.value,
|
||||
errorReportingEnabled: reporting.checked,
|
||||
updateCheckEnabled: updateCheck.checked,
|
||||
updateChannel: channel.value
|
||||
}),
|
||||
refresh)
|
||||
]);
|
||||
}
|
||||
|
||||
function subsystemsCard(info) {
|
||||
const badge = (on) => erstellePlakette(on ? t("settings.on") : t("settings.off"), on ? "erfolg" : "neutral");
|
||||
return erstelleKarte(t("settings.runtimeAndSubsystems"), [dataList([
|
||||
[t("settings.agentEngine"), badge(info.engineActive)],
|
||||
[t("settings.scheduler"), badge(info.schedulerActive)],
|
||||
[t("settings.stagingService"), badge(info.stagingActive)],
|
||||
[t("settings.dcSubToken"), badge(info.deploymentcenterActive)]
|
||||
])]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user