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:
@@ -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...";
|
||||
|
||||
Reference in New Issue
Block a user