Update-Pruefung regelmaessig und ab Werk an, Installation nur auf Klick
CI / Build & Tests (Linux) (push) Canceled after 0s
CI / Plattformneutralität & Hygiene (push) Canceled after 0s

Richards Vorgabe vom 14.09.2026 (#587): Updates nie automatisch installieren,
aber regelmaessig pruefen und darauf hinweisen.

Bisher pruefte der Desktop genau einmal beim Start, das WebUI nur beim
Seitenaufruf, und ab Werk gar nicht (UpdateCheckEnabled=false). Eine
wochenlang laufende oder frisch installierte Anwendung erfuhr nie von einem
neuen Release.

- UpdateCheckEnabled ab Werk true, neu UpdateCheckIntervalHours (6, 1..168)
- UpdateCheckBackgroundService: erste Pruefung nach 30 s, dann im Takt; laeuft
  auch ohne WebUI und kopflos; Hinweis im Log einmal je neuer Fassung
- Registrierung ueber AddPolyTraderUpdateCheck, nicht mehr am WebUI
- Desktop-Banner haengt an UpdateStatusService.Checked; "Spaeter" blendet bis
  zur naechsten neueren Fassung aus
- WebUI-Banner holt alle 10 Minuten den vorgehaltenen Stand

Auf der Linux-VM geprueft: 0.1.9 ohne Handeinstellung erkennt 0.2.0, ein
Hinweis im Log, Banner erscheint, "Spaeter" blendet aus, Beenden in 0,1 s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-14 12:16:33 +02:00
co-authored by Claude Opus 5
parent 14c89aea7d
commit b4d8a2a647
9 changed files with 241 additions and 58 deletions
+3
View File
@@ -368,6 +368,9 @@ namespace PolyTrader.App.Avalonia
services.AddHostedService(sp => sp.GetRequiredService<WatchdogHeartbeatService>());
services.AddHostedService(sp => sp.GetRequiredService<DeploymentcenterErrorReporter>());
// #587: regelmäßige Update-Prüfung mit Hinweis - installiert nie selbst.
PolyTrader.Web.Updates.UpdateCheckServiceCollectionExtensions.AddPolyTraderUpdateCheck(services);
// Shell-Zustand (Fensterverwaltung). Auch im kopflosen Betrieb harmlos.
services.AddSingleton<AvaloniaUiHost>();
})
@@ -20,6 +20,7 @@ using PolyTrader.Core.Modularity;
using PolyTrader.Core.Persistence;
using PolyTrader.Core.Security;
using PolyTrader.Modules.Supervisor.Persistence;
using PolyTrader.Web.Updates;
using PolyTraderSharp;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
@@ -51,8 +52,11 @@ namespace PolyTrader.App.Avalonia.Views
private int _statusTicks;
/// <summary>Ergebnis der letzten Update-Prüfung – für den Klick auf „Jetzt installieren …".</summary>
private UpdateCheckResult? _updateCheck;
/// <summary>Fassung aus der letzten Update-Prüfung – für den Klick auf „Jetzt installieren …".</summary>
private string? _updateVersion;
/// <summary>Mit „Später" ausgeblendete Fassung – das Banner kommt erst für eine noch neuere wieder.</summary>
private string? _dismissedVersion;
public LauncherWindow() => AvaloniaXamlLoader.Load(this);
@@ -84,16 +88,25 @@ namespace PolyTrader.App.Avalonia.Views
this.FindControl<Button>("btnInstallUpdate")!.Click += async (_, _) => await InstallUpdateAsync();
this.FindControl<Button>("btnDismissUpdate")!.Click += (_, _) =>
{
_dismissedVersion = _updateVersion;
this.FindControl<Border>("pnlUpdateBanner")!.IsVisible = false;
};
_statusTimer.Tick += (_, _) => UpdateStatus();
_statusTimer.Start();
UpdateStatus();
RefreshData();
// D-4: einmalige, stille Prüfung beim Start - fire-and-forget, darf den Start nie
// verzögern oder blockieren. Nur aktiv, wenn Deploymentcenter:UpdateCheckEnabled gesetzt ist.
_ = CheckForUpdateAsync();
// #587: Geprüft wird regelmäßig im Hintergrund (UpdateCheckBackgroundService), hier nur angezeigt.
// Bis zum 14.09.2026 prüfte diese Seite genau einmal beim Start - eine wochenlang laufende
// Anwendung erfuhr nie von einem neuen Release. Kein Abmelden, siehe ThemeChanged unten.
var updates = services.GetService<UpdateStatusService>();
if (updates != null)
{
updates.Checked += OnUpdateChecked;
if (updates.Last is { } last) OnUpdateChecked(last);
}
// Die Umschalterflaechen werden im Code gesetzt und folgen dem Thema nicht von selbst.
// Kein Abmelden: die Seite lebt in der Einfenster-Shell bis zum Programmende, ein
@@ -185,56 +198,29 @@ namespace PolyTrader.App.Avalonia.Views
// ===== Update-Prüfung (D-4) =====
/// <summary>
/// Prüft beim Start still auf ein neues Release. Installiert NIE automatisch (Leitentscheidung
/// 4 aus dem Umsetzungsplan) - ein `is_critical`-Release ändert nur den Hinweistext, nicht das
/// Verhalten. Wirft nie: ein nicht erreichbares Deploymentcenter darf den Start nicht stören.
/// Zeigt das Ergebnis einer Hintergrundprüfung an. Installiert NIE automatisch (Leitentscheidung 4,
/// bestätigt am 14.09.2026) - ein `is_critical`-Release ändert nur den Hinweistext, nicht das Verhalten.
/// Kein Update, Fehler oder fehlende Lizenz blenden das Banner aus; die Einzelheiten stehen im WebUI
/// unter Einstellungen → Version &amp; Updates.
/// </summary>
private async Task CheckForUpdateAsync()
private void OnUpdateChecked(UpdateCheckSnapshot snapshot)
{
try
Dispatcher.UIThread.Post(() =>
{
var dcOptions = _services.GetService<DeploymentcenterOptions>();
if (dcOptions == null || !dcOptions.UpdateCheckEnabled || string.IsNullOrWhiteSpace(dcOptions.BaseUrl))
return;
var settings = _services.GetService<ServerSettings>();
string? licenseKey = null;
try { licenseKey = SecretProtection.Unprotect(settings?.LicenseKey ?? string.Empty); }
catch { /* nicht lesbarer Schlüssel -> Prüfung läuft ohne Zugangsdaten weiter */ }
var updateClient = new UpdateClient();
var result = await updateClient.CheckForUpdateAsync(
baseUrl: dcOptions.BaseUrl,
projectId: dcOptions.ProjectSlug,
currentVersion: BuildInfo.Version,
channel: dcOptions.Channel,
credentials: ReleaseCredentials.FromLicenseKey(licenseKey));
if (result.Unauthorized)
var banner = this.FindControl<Border>("pnlUpdateBanner")!;
if (!snapshot.UpdateAvailable || string.IsNullOrEmpty(snapshot.LatestVersion))
{
// Kein Netzwerkfehler: die Lizenz trägt nicht mehr für den Paketabruf. Kein
// Alarm im Launcher nötig - die Lizenzprüfung selbst (D-2) hat das schon gemeldet.
_updateVersion = null;
banner.IsVisible = false;
return;
}
if (!result.UpdateAvailable || result.LatestRelease == null) return;
_updateCheck = result;
string version = result.LatestRelease.Version;
string critical = result.IsCritical ? " (empfohlenes Sicherheitsupdate)" : "";
Dispatcher.UIThread.Post(() =>
{
this.FindControl<TextBlock>("lblUpdateInfo")!.Text =
$"Update verfügbar: v{version}{critical}. {result.LatestRelease.Changelog}".TrimEnd();
this.FindControl<Border>("pnlUpdateBanner")!.IsVisible = true;
});
}
catch
{
// Update-Prüfung darf den Start nie stören - stiller Rückzug, kein Log-Spam beim
// vermutlich häufigsten Fall (Deploymentcenter nicht erreichbar).
}
_updateVersion = snapshot.LatestVersion;
string critical = snapshot.IsCritical ? " (empfohlenes Sicherheitsupdate)" : "";
this.FindControl<TextBlock>("lblUpdateInfo")!.Text =
$"Update verfügbar: v{snapshot.LatestVersion}{critical}. {snapshot.Changelog}".TrimEnd();
banner.IsVisible = !string.Equals(_dismissedVersion, snapshot.LatestVersion, StringComparison.OrdinalIgnoreCase);
});
}
/// <summary>
@@ -244,7 +230,7 @@ namespace PolyTrader.App.Avalonia.Views
/// </summary>
private async Task InstallUpdateAsync()
{
if (_updateCheck?.LatestRelease == null) return;
if (_updateVersion == null) return;
var dcOptions = _services.GetService<DeploymentcenterOptions>();
if (dcOptions == null) return;
@@ -259,7 +245,7 @@ namespace PolyTrader.App.Avalonia.Views
}
bool confirmed = await DialogWindow.Confirm(this, "Update installieren",
$"Version {_updateCheck.LatestRelease.Version} herunterladen und PolyTrader danach " +
$"Version {_updateVersion} herunterladen und PolyTrader danach " +
"neu starten?\n\nOffene Vorgänge werden dabei genauso geordnet beendet wie über „Beenden\".",
"Installieren");
if (!confirmed) return;
+3 -1
View File
@@ -15,7 +15,9 @@
"WatchdogIntervalSeconds": 60,
"WatchdogEnabled": false,
"ErrorReportingEnabled": false,
"UpdateCheckEnabled": false
"_UpdateCheck_comment": "Prueft regelmaessig auf ein neues Release und weist darauf hin. Installiert wird nie automatisch, nur auf Klick im Desktop.",
"UpdateCheckEnabled": true,
"UpdateCheckIntervalHours": 6
},
"Logging": {
"LogLevel": {