Eigenständiges token-geschütztes Verwaltungs-WebGUI (Admin WebGUI) hinzugefügt

- Unabhängige Kestrel-Instanz auf Port 5001 (vollständig getrennt vom Analyse-Webserver)
- Admin API Endpunkte ohne Swagger-Exposition
- Token-Gatekeeper mit sofortigem Login-Overlay bei unautorisiertem Zugriff
- Live-Terminal via SSE-Log-Streaming und Log-Download-Übersicht
- Steuerung von Workern, Analyse-Webserver, Wartungsaktionen und Einstellungen
- Nahtlose Integration in Avalonia Desktop-Shell und Linux Headless-Modus
- Unit-Tests für Admin-Filter, Optionen und Log-Puffer
This commit is contained in:
Richard
2026-09-07 21:09:03 +02:00
parent 301249f3b8
commit 1131d257cf
19 changed files with 3566 additions and 16 deletions
@@ -18,7 +18,6 @@ public sealed partial class MainWindowViewModel : ObservableObject
private const int MaxLogLines = 500;
private readonly PredictalyticsHost _host;
private CancellationTokenSource? _workerCts;
private DcHeartbeatService? _heartbeat;
private double? _lastDbSizeMb;
@@ -79,6 +78,22 @@ public sealed partial class MainWindowViewModel : ObservableObject
RestartHeartbeat();
if (Options.AdminServerEnabled)
{
_ = Task.Run(async () =>
{
try
{
await _host.StartAdminServerAsync();
Log.Information("Verwaltungs-WebGUI erreichbar unter {Url}", Options.AdminServerUrl);
}
catch (Exception ex)
{
Log.Error(ex, "Verwaltungs-WebGUI konnte nicht gestartet werden");
}
});
}
if (Options.DcUpdateCheckEnabled)
{
_ = CheckForUpdatesAsync(silent: true);
@@ -292,23 +307,17 @@ public sealed partial class MainWindowViewModel : ObservableObject
// ─── Befehle ───
[RelayCommand]
private async Task ToggleServerAsync()
private void ToggleServer()
{
if (!_host.WorkersRunning)
{
_workerCts = new CancellationTokenSource();
Log.Information("🚀 Starte Hintergrund-Worker...");
try
{
await _host.StartWorkersAsync(_workerCts.Token);
}
catch (OperationCanceledException) { }
catch (Exception ex) { Log.Error(ex, "Worker error"); }
_host.StartWorkers();
}
else
{
Log.Information("⏹ Stoppe Hintergrund-Worker...");
_workerCts?.Cancel();
_host.StopWorkers();
Log.Information("Worker gestoppt.");
}
UpdateStatus();
@@ -354,6 +363,17 @@ public sealed partial class MainWindowViewModel : ObservableObject
[RelayCommand]
private void OpenWebUi() => OpenInShell(Options.WebserverUrl);
[RelayCommand]
private void OpenAdminWebUi() => OpenInShell(Options.AdminServerUrl);
[RelayCommand]
private void GenerateAdminToken()
{
Options.AdminToken = PredictalyticsOptions.GenerateSecureToken();
OnPropertyChanged(nameof(Options));
Log.Information("Neuer Admin-Token generiert. Zum Übernehmen 'Speichern & übernehmen' klicken.");
}
[RelayCommand]
private void OpenLogFolder()
{
@@ -476,7 +496,8 @@ public sealed partial class MainWindowViewModel : ObservableObject
_heartbeat?.NotifyStopping();
_heartbeat?.Dispose();
_heartbeat = null;
_workerCts?.Cancel();
_host.StopWorkers();
try { _host.StopWebServerAsync().GetAwaiter().GetResult(); } catch { /* beendet sich ohnehin */ }
try { _host.StopAdminServerAsync().GetAwaiter().GetResult(); } catch { /* beendet sich ohnehin */ }
}
}