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:
Richard
2026-10-02 13:30:20 +02:00
co-authored by Claude Opus 5.5
parent a9ce72abe2
commit 8552755bb6
20 changed files with 1677 additions and 322 deletions
+70
View File
@@ -118,6 +118,9 @@ public sealed class ManagerCore : IAsyncDisposable
public ManagerSettings Settings => _settings.Settings;
/// <summary>Where <c>manager.json</c> lives — shown in the web interface.</summary>
public string SettingsPath => _settings.SettingsPath;
// ─── Hochfahren ───
/// <summary>
@@ -313,6 +316,73 @@ public sealed class ManagerCore : IAsyncDisposable
/// betreiben: Der Weg vom Widerruf zum Anhalten ist die Zusage, nicht die Frage, wer
/// ihn ausgelöst hat.</para>
/// </summary>
/// <summary>Whether a license check is configured at all — without one nothing is locked.</summary>
public bool LicenseEnforced => _license is not null;
/// <summary>The hardware id the license server sees, for support; <c>null</c> without a license check.</summary>
public string? LicenseHardwareId => _license?.HardwareId;
/// <summary>
/// #764: A license key from the web interface. The first install of a service has no other
/// way to get one: the manager never asks (<c>NonInteractiveLicensePrompt</c>).
///
/// <para><b>A confirmed key lifts the lock at once</b> — the watch starts and the autostart
/// instances come up, as they would have at startup. Without that, a freshly licensed
/// manager would still refuse every start until someone restarted the service.</para>
///
/// <para>The autostart runs without the request's token: whoever closes the browser right
/// after saving should not leave half the instances started.</para>
/// </summary>
public async Task<LicenseApplyResult> ApplyLicenseKeyAsync(string key, CancellationToken ct = default)
{
if (_license is null)
return new LicenseApplyResult(false, false, "This instance manager has no license check configured.");
var result = await _license.ApplyKeyAsync(key, ct).ConfigureAwait(false);
if (!result.Applied)
return result;
var wasLocked = !LicenseHolds;
LicenseHolds = true;
_watch?.Start();
if (wasLocked)
{
_logger.LogInformation("License confirmed - instances may start again.");
await StartAutostartInstancesAsync(CancellationToken.None).ConfigureAwait(false);
}
return result;
}
/// <summary>
/// #764: Changes the manager's own settings in <c>manager.json</c> that the web interface
/// offers. <c>null</c> keeps a value; an empty master token keeps the one there is — it is
/// never sent out, so a form cannot send it back.
///
/// <para>Everything here is read once, when the manager starts (license server, token
/// source, update check). The answer says so instead of pretending the change is live.</para>
///
/// <para>Port, binding and the access token of the web interface are deliberately not
/// offered: a wrong value there locks out the very page it was set from.</para>
/// </summary>
public (bool Ok, string Message) UpdateSettings(string? deploymentcenterUrl, string? masterToken, string? updateChannel)
{
if (deploymentcenterUrl is { } url && url.Trim().Length > 0
&& !(Uri.TryCreate(url.Trim(), UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp)))
return (false, "The Deploymentcenter address must be an http(s) address.");
if (updateChannel is { } channel && channel.Trim() is not ("prod" or "beta" or "dev"))
return (false, "The update channel is one of prod, beta, dev.");
var settings = _settings.Settings;
if (deploymentcenterUrl is not null) settings.DeploymentcenterUrl = deploymentcenterUrl.Trim();
if (!string.IsNullOrWhiteSpace(masterToken)) settings.DeploymentcenterMasterToken = masterToken.Trim();
if (updateChannel is not null) settings.UpdateChannel = updateChannel.Trim();
_settings.Save();
return (true, "Saved. Takes effect when the instance manager restarts.");
}
public async Task HandleLicenseRevokedAsync(string grund)
{
LicenseHolds = false;