Dashboard-Performance, CI-Workflow, Settings-Aufteilung, Backup-Härtung
- Dashboard: Fehlzeiten-Warnung lädt Mitarbeitssitzungen einmal vorab statt pro Schüler/Eintrag einzeln nachzuschlagen (N+1 vermieden); neues IParticipationSessionRepository.GetAll() dafür. - CI: .gitea/workflows/ci.yml baut und testet bei jedem Push/PR. Dabei fehlende Release|Any CPU-Konfiguration für 6 Projekte in der .sln behoben (LehrerApp.Data.Tests wurde bei Release-Builds der Solution bislang stillschweigend übersprungen). TreatWarningsAsErrors jetzt aktiv. - SettingsViewModel (1986 Zeilen) als partial class auf 20 Themen- dateien aufgeteilt, Verhalten unverändert. - Backup: optionaler zweiter Sicherungsordner (USB-Stick/Netzlaufwerk, best-effort) und Integritätsprüfung nach jedem Backup (DatabaseEncryptionService.CanOpenAndRead). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Data;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Planning;
|
||||
using LehrerApp.Sync;
|
||||
using LehrerApp.Sync.Crypto;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Settings;
|
||||
|
||||
public partial class SettingsViewModel
|
||||
{
|
||||
// ── Geräte-Pairing (10.3.1: Schlüsselübertragung auf ein zweites Gerät) ───
|
||||
//
|
||||
// SnapshotService ist nur registriert, wenn bereits eine Server-URL konfiguriert ist (siehe
|
||||
// AppBootstrapper) — daher optional/nullable statt eines Pflicht-Konstruktorparameters.
|
||||
|
||||
[ObservableProperty] private string _pairingCode = "";
|
||||
[ObservableProperty] private string _pairingCodeInput = "";
|
||||
[ObservableProperty] private string _pairingStatus = "";
|
||||
[ObservableProperty] private bool _pairingBusy;
|
||||
|
||||
/// Vom Code-Behind gesetzt: zeigt einen Bestätigungsdialog, bevor die lokale Datenbank durch
|
||||
/// den Stand des anderen Geräts ersetzt wird.
|
||||
public Func<Task<bool>>? OnConfirmPairingRestore { get; set; }
|
||||
|
||||
/// Vom Code-Behind gesetzt: zeigt einen Fehlerdialog, den der Nutzer aktiv wegklicken muss —
|
||||
/// notwendig, weil RedeemPairingCode() nach einem Fehlschlag sofort neu startet (_dbContext ist
|
||||
/// zu dem Zeitpunkt bereits disposed, siehe dort) und PairingStatus dadurch sonst nie sichtbar
|
||||
/// gerendert würde.
|
||||
public Func<string, Task>? OnShowPairingError { get; set; }
|
||||
|
||||
// ── Geräte-Pairing: Code erzeugen / einlösen ─────────────────────────────
|
||||
//
|
||||
// CreateAndUploadAsync lädt einen verschlüsselten Snapshot der lokalen Datenbank samt
|
||||
// Sync-Schlüssel hoch, RestoreFromCodeAsync ersetzt auf dem ZWEITEN Gerät die dortige
|
||||
// Datenbank vollständig durch diesen Snapshot (vorheriger Stand wird automatisch als
|
||||
// .backup-Datei gesichert, siehe SnapshotService) — deshalb vor dem Einlösen ein
|
||||
// Bestätigungsdialog wie bei RestoreBackup.
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CreatePairingCode()
|
||||
{
|
||||
if (_snapshotService is null) return;
|
||||
PairingBusy = true;
|
||||
PairingCode = "";
|
||||
PairingStatus = "";
|
||||
void OnProgress(SnapshotProgress p) => PairingStatus = p.Message;
|
||||
_snapshotService.ProgressChanged += OnProgress;
|
||||
try
|
||||
{
|
||||
var result = await _snapshotService.CreateAndUploadAsync();
|
||||
PairingCode = result.Code;
|
||||
PairingStatus = $"Gültig bis {result.ExpiresAt:dd.MM.yyyy HH:mm} — auf dem anderen Gerät eingeben.";
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException or InvalidOperationException)
|
||||
{
|
||||
PairingStatus = $"Fehlgeschlagen: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_snapshotService.ProgressChanged -= OnProgress;
|
||||
PairingBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RedeemPairingCode()
|
||||
{
|
||||
if (_snapshotService is null) return;
|
||||
if (string.IsNullOrWhiteSpace(PairingCodeInput)) { PairingStatus = "Bitte Code eingeben."; return; }
|
||||
if (OnConfirmPairingRestore is not null && !await OnConfirmPairingRestore()) return;
|
||||
|
||||
PairingBusy = true;
|
||||
PairingStatus = "";
|
||||
void OnProgress(SnapshotProgress p) => PairingStatus = p.Message;
|
||||
_snapshotService.ProgressChanged += OnProgress;
|
||||
|
||||
// LiteDB hält beim Öffnen einen exklusiven Dateilock — muss vor dem Überschreiben
|
||||
// geschlossen sein. Läuft danach ein Fehler (falscher Code, Server nicht erreichbar), ist
|
||||
// _dbContext bereits disposed und die App nicht mehr sicher weiter benutzbar, ohne dass
|
||||
// die Datenbankdatei selbst angefasst wurde (RestoreFromCodeAsync schreibt sie erst ganz
|
||||
// am Ende) — deshalb in JEDEM Fall (Erfolg wie Fehlschlag) neu starten, nicht nur bei
|
||||
// Erfolg. Ein Neustart öffnet dann wieder dieselbe, unveränderte Datenbank.
|
||||
_dbContext.Dispose();
|
||||
try
|
||||
{
|
||||
await _snapshotService.RestoreFromCodeAsync(PairingCodeInput.Trim(), AppBootstrapper.DbPath);
|
||||
}
|
||||
catch (Exception ex) when (ex is SnapshotNotFoundException or InvalidOperationException or HttpRequestException)
|
||||
{
|
||||
// _dbContext ist bereits disposed, PairingStatus ist aber ein reines ViewModel-Feld
|
||||
// ohne DB-Zugriff - das Setzen ist unabhängig davon noch sicher.
|
||||
PairingStatus = $"Fehlgeschlagen: {ex.Message}";
|
||||
_logger.Error("Geräte-Pairing (Redeem) fehlgeschlagen", ex);
|
||||
// Ohne diesen Dialog würde PairingStatus nie gerendert: der direkt folgende Neustart
|
||||
// (unten, auch im Fehlerfall nötig, siehe Kommentar oben) beendet den Prozess, bevor
|
||||
// Avalonia den nächsten Frame zeichnen könnte.
|
||||
if (OnShowPairingError is not null) await OnShowPairingError(ex.Message);
|
||||
}
|
||||
AppBootstrapper.RestartApplication();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user