feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar

This commit is contained in:
Richard
2026-08-10 10:48:34 +02:00
parent a0e18d2a57
commit b5bf97ae74
187 changed files with 20054 additions and 882 deletions
@@ -0,0 +1,110 @@
using Microsoft.Extensions.Logging;
namespace ClawdDotNet.App.Services;
/// <summary>
/// Prüft die Lizenz im laufenden Betrieb nach.
///
/// <para>Ohne das wirkt ein Widerruf erst beim nächsten Start — bei einer Anwendung, die
/// als Dienst wochenlang läuft, ist das praktisch nie. Der Takt ist bewusst grob (alle
/// zwölf Stunden): Es geht um einen Notausschalter, nicht um eine Zugangskontrolle pro
/// Klick.</para>
///
/// <para><b>Nur ein Urteil zählt.</b> Ein Netzproblem, eine Drosselung oder eine
/// abgelaufene Offline-Frist beenden nichts — das SDK meldet solche Fälle als
/// <c>IsTransient</c>, und ein Serverausfall darf nicht alle laufenden Instanzen
/// mitnehmen. Der verschlüsselte Zwischenspeicher trägt über solche Lücken hinweg.</para>
/// </summary>
public sealed class LicenseWatch : IAsyncDisposable
{
private static readonly TimeSpan DefaultInterval = TimeSpan.FromHours(12);
private readonly LicenseGate _gate;
private readonly ILogger _logger;
private readonly TimeSpan _interval;
private CancellationTokenSource? _cts;
private Task? _loop;
/// <summary>
/// Die Lizenz gilt nicht mehr. Der Aufrufer beendet die Anwendung — geordnet, aber
/// ohne Rückfrage; der übergebene Text erklärt den Grund.
/// </summary>
public event Func<string, Task>? Revoked;
public LicenseWatch(LicenseGate gate, ILogger logger, TimeSpan? interval = null)
{
_gate = gate;
_logger = logger;
_interval = interval ?? DefaultInterval;
}
public void Start()
{
if (_loop is { IsCompleted: false })
return;
_cts = new CancellationTokenSource();
_loop = RunAsync(_cts.Token);
}
private async Task RunAsync(CancellationToken ct)
{
try
{
using var timer = new PeriodicTimer(_interval);
while (await timer.WaitForNextTickAsync(ct).ConfigureAwait(false))
{
var result = await _gate.RevalidateAsync(ct).ConfigureAwait(false);
if (result.IsValid)
continue;
if (result.IsTransient)
{
_logger.LogInformation(
"Lizenz-Nachprüfung ohne Ergebnis ({Status}) Betrieb läuft weiter.",
result.Status);
continue;
}
_logger.LogWarning("Lizenz gilt nicht mehr ({Status}) Instanz wird beendet.",
result.Status);
if (Revoked is { } handler)
{
await handler($"Die Lizenz ist nicht mehr gültig ({result.Status}). "
+ "ClawdDotNet wird beendet.").ConfigureAwait(false);
}
return;
}
}
catch (OperationCanceledException)
{
// Regulärer Stopp.
}
catch (Exception ex)
{
// Die Nachprüfung darf den Betrieb nicht mitnehmen.
_logger.LogWarning(ex, "Lizenz-Nachprüfung abgebrochen.");
}
}
public async ValueTask DisposeAsync()
{
if (_cts is null)
return;
await _cts.CancelAsync().ConfigureAwait(false);
if (_loop is not null)
{
try { await _loop.ConfigureAwait(false); }
catch (OperationCanceledException) { /* erwartet */ }
}
_cts.Dispose();
}
}