UI-Entwurf umgesetzt, Rocket.Chat-Tool, Deploymentcenter 2.4

Sicherungspunkt vor dem Aufraeumen. Buendelt die Arbeit, die seit dem
Abschluss der Avalonia-Portierung im Arbeitsverzeichnis lag.

Oberflaeche
- Entwurf aus Mockup/ umgesetzt: Theme.axaml (Farben je Thema, Barlow als
  mitgelieferte Schrift), Icons.axaml (Symbolgeometrien), Shell.axaml
  (eigene ControlThemes statt Fluent umzufaerben).
- Neue Steuerelemente StrokeIcon und BlueprintFrame, Seiten fuer
  Token-Verbrauch und Agenten-Chats, Werkzeug-Einstellungen als Seite
  statt eigenem Fenster, Texteditor-Fenster.
- ThemeManager mit hellem und dunklem Thema; die beiden Pinsel-Konverter
  entfallen, weil ein fester Farbwert den Themenwechsel nicht ueberlebt.

Rocket.Chat
- Neues Tool-Projekt (Client, Konfiguration, Workspace-Dateien) nach der
  Bauform des Telegram-Tools: rocketchat_poll als Tool-Job, geweckt wird
  nur, wenn wirklich etwas anliegt.
- send_file ist freigabepflichtig, send_message bewusst nicht: Der Raum
  ist Arbeitsraum, der Schutz sitzt an der Raum-Allowlist.
- Konzept-Doc um die Messung gegen die echte Instanz 8.7 ergaenzt; drei
  Annahmen waren falsch und sind korrigiert.

Deploymentcenter
- DC6 (Update anwenden) und DC7 (Erstinstallation ueber setup.json)
  erledigt, DC3 fuer win-x64/dev; deploy/publish.py als Release-Strecke.
- AppHost.DisposeAsync gegen doppeltes Herunterfahren gesperrt - sonst
  ueberschreibt eine zweite Abmeldung den Wartungszustand am Watchdog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-23 12:16:05 +02:00
co-authored by Claude Opus 5
parent b5bf97ae74
commit 33d95a6c3f
88 changed files with 11665 additions and 1029 deletions
@@ -210,9 +210,30 @@ public sealed class DeploymentcenterService : IAsyncDisposable
try
{
var client = new global::Deploymentcenter.Client.UpdateClient();
// Seit 2.4 liegt die Release-Ablage hinter HTTP-Basic-Auth; der Lizenzschlüssel
// ist der Zugang. Die Anwendung kennt ihn ohnehin, es muss nichts zusätzlich
// verteilt werden. Ohne ihn kommt ab dem Scharfschalten nur noch 401.
//
// platform bleibt offen: Das SDK setzt die Kennung des laufenden Systems ein.
// Sie wegzulassen wäre keine Option — der Server liefert dann ausschließlich
// plattformunabhängige Pakete.
var result = await client.CheckForUpdateAsync(
settings.DeploymentcenterUrl, LicenseInfo.ProductSlug, currentVersion,
settings.UpdateChannel, cancellationToken: ct);
settings.UpdateChannel,
credentials: global::Deploymentcenter.Client.ReleaseCredentials
.FromLicenseKey(settings.LicenseKey),
cancellationToken: ct);
// Ein 401 ist kein Netzwerkfehler, sondern eine Aussage über die Lizenz.
// Ohne die Unterscheidung sucht man bei einem abgelaufenen Vertrag am Netz.
if (result.Unauthorized)
{
_logger.LogWarning(
"Update-Prüfung abgewiesen: {Message} der Lizenzschlüssel öffnet die "
+ "Release-Ablage nicht (mehr).", result.Message);
return;
}
if (result.Error is not null)
{
@@ -247,6 +268,78 @@ public sealed class DeploymentcenterService : IAsyncDisposable
}
}
/// <summary>
/// Meldet dem Watchdog, dass das Herunterfahren gleich ein Update ist und kein
/// Ausfall. Muss <b>vor</b> dem Herunterfahren gerufen werden — die Meldung geht
/// beim Beenden des Heartbeats raus.
/// </summary>
public void AnnounceUpdate(string version)
=> _heartbeat?.AnnounceMaintenance($"Update auf {version} wird eingespielt.");
/// <summary>
/// Startet den <c>update-agent</c> und übergibt ihm das Feld.
///
/// <para><b>Beendet die Anwendung bewusst nicht selbst.</b> <c>LaunchUpdateAgent</c>
/// kann das (<c>exitCurrentApp</c>), täte es aber über
/// <see cref="Environment.Exit"/> — und das übergeht jedes Aufräumen: Der Watchdog
/// meldete nichts, Instanzdatenbank und Chatverläufe blieben auf halbem Weg stehen.
/// Der Aufrufer fährt deshalb erst geordnet herunter und beendet danach selbst.
/// Der Agent bekommt unsere Prozesskennung und wartet, bis wir wirklich weg
/// sind.</para>
/// </summary>
/// <returns>Der Grund, wenn es nicht losging; sonst <c>null</c>.</returns>
public string? StartUpdate(AppSettings settings, string currentVersion)
{
var agentPath = global::Deploymentcenter.Client.UpdateClient.ResolveAgentPath();
if (agentPath is null)
{
// Kein fest verdrahteter Name: Unter Linux trägt der Agent keine Endung.
_logger.LogWarning(
"Update angefordert, aber kein update-agent neben der Anwendung gefunden.");
return "Der update-agent wurde nicht gefunden. Er gehört neben die "
+ "Programmdatei; das Installationspaket bringt ihn normalerweise mit.";
}
try
{
var started = global::Deploymentcenter.Client.UpdateClient.LaunchUpdateAgent(
agentPath: agentPath,
projectId: LicenseInfo.ProductSlug,
channel: settings.UpdateChannel,
action: "update",
version: "latest",
exitCurrentApp: false,
currentVersion: currentVersion,
waitForCurrentProcess: true,
licenseKey: settings.LicenseKey,
// Ohne Signatur nicht installieren. Der SHA256 stammt aus derselben
// Quelle wie das Paket — wer den Webroot oder die FTP-Zugangsdaten
// kontrolliert, tauscht beide gemeinsam aus. Ausgerechnet auf dem Pfad,
// der anschließend fremden Code ausführt.
//
// Tragfähig, weil ab 0.1.2 jedes Release signiert ist. Ein Rücksprung
// auf 0.1.0 oder 0.1.1 bricht damit ab — das ist gewollt: Die beiden
// stammen aus der Zeit vor dem Signierschlüssel.
requireSignature: true);
if (!started)
return "Der update-agent ließ sich nicht starten.";
_logger.LogInformation("update-agent gestartet ({Path}), Anwendung wird beendet.",
agentPath);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "update-agent konnte nicht gestartet werden.");
return $"Der update-agent konnte nicht gestartet werden: {ex.Message}";
}
}
public async ValueTask DisposeAsync()
{
if (_heartbeat is not null)
@@ -36,7 +36,12 @@ public sealed class OpenRouterStatusService : IAsyncDisposable
/// <summary>Modelle, für die keine Preise vorliegen — werden in der Anzeige benannt.</summary>
private readonly ConcurrentDictionary<string, byte> _modelsWithoutPricing = new();
private const double UsdToEur = 0.92;
/// <summary>
/// Grober Umrechnungskurs fuer die Anzeige. Oeffentlich, damit die Tokennutzungs-
/// Seite denselben Wert verwendet — zwei Kopien eines veraltenden Kurses waeren
/// zwei Gelegenheiten, verschiedene Betraege fuer dieselben Laeufe zu zeigen.
/// </summary>
public const double UsdToEur = 0.92;
public bool IsApiReachable { get; private set; }
public string StatusText { get; private set; } = "Prüfe...";