CI / build-and-test (push) Canceled after 0s
Ein Sync-Ausfall (offline/Fehler) war bisher nur als blasser Text in der Statuszeile sichtbar und konnte so stundenlang unbemerkt bleiben, bis auf einem anderen Gerät Daten fehlten. SyncStatusViewModel meldet neue Ausfälle jetzt einmalig aktiv per Toast (nicht bei jedem automatischen Retry erneut). DatabaseEncryptionService.IsEncrypted/VerifyPassword öffneten die Datenbankdatei mit einer zweiten, exklusiven (Direct) LiteDB-Verbindung, während LiteDbContext dieselbe Datei bereits im Shared-Modus offen hält - jeder Aufruf (z.B. beim Öffnen der Einstellungen) crashte reproduzierbar mit IOException. Beide öffnen jetzt ebenfalls im Shared-Modus und erzwingen einen tatsächlichen Lesezugriff, da Shared-Verbindungen die Datei lazy öffnen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
106 lines
5.0 KiB
C#
106 lines
5.0 KiB
C#
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using LehrerApp.Desktop.Services;
|
|
using LehrerApp.Sync;
|
|
using LehrerApp.Sync.Models;
|
|
|
|
namespace LehrerApp.Desktop.ViewModels;
|
|
|
|
public partial class SyncStatusViewModel : ObservableObject
|
|
{
|
|
private readonly SyncEngine? _engine;
|
|
private readonly NotificationService? _notifications;
|
|
private bool _incompatibleNotificationShown;
|
|
private SyncState _lastNotifiedState = SyncState.Idle;
|
|
|
|
public const string IncompatibleVersionMessage =
|
|
"Diese App-Version ist veraltet oder nicht mit dem Sync-Server kompatibel. " +
|
|
"Bitte aktualisiere die LehrerApp. Die Synchronisierung wurde zum Schutz deiner Daten angehalten.";
|
|
|
|
public const string OfflineMessage =
|
|
"Synchronisierung nicht möglich - Änderungen werden lokal gespeichert und automatisch " +
|
|
"nachgeholt, sobald die Verbindung zum Server wieder da ist.";
|
|
|
|
[ObservableProperty] private string _statusText = "Kein Server konfiguriert";
|
|
[ObservableProperty] private string _lastSyncText = "";
|
|
[ObservableProperty] private bool _isSyncing;
|
|
[ObservableProperty] private bool _isServerConfigured;
|
|
[ObservableProperty] private bool _isIncompatibleVersion;
|
|
[ObservableProperty] private bool _canAttemptSync;
|
|
[ObservableProperty] private int _pendingCount;
|
|
|
|
/// <summary>Feuert, wenn ein Sync tatsächlich Daten angewendet hat (siehe SyncEngine.
|
|
/// DataChanged, TODO 10.1.11) - MainWindowViewModel nutzt das, um die gerade sichtbare Seite
|
|
/// neu zu laden, da EventApplier absichtlich an jedem ViewModel vorbei direkt auf die LiteDB
|
|
/// schreibt.</summary>
|
|
public event Action? DataChanged;
|
|
|
|
public SyncStatusViewModel(SyncEngine? engine, NotificationService? notifications = null)
|
|
{
|
|
_engine = engine;
|
|
_notifications = notifications;
|
|
IsServerConfigured = engine is not null;
|
|
CanAttemptSync = IsServerConfigured;
|
|
if (_engine is not null)
|
|
{
|
|
_engine.StatusChanged += OnStatus;
|
|
_engine.DataChanged += () => DataChanged?.Invoke();
|
|
// SyncEngine feuert sein erstes StatusChanged bereits im eigenen Konstruktor
|
|
// (UpdateStatus) - der läuft aber schon, während wir hier noch in GetService<SyncEngine>()
|
|
// stecken, also VOR dem obigen Abonnieren. Ohne diesen Nachhol-Aufruf bliebe StatusText
|
|
// nach jedem Neustart beim Default "Kein Server konfiguriert", bis der nächste
|
|
// Auto-Sync oder Klick auf "Jetzt synchronisieren" den Text erstmals aktualisiert.
|
|
OnStatus(_engine.Status);
|
|
}
|
|
}
|
|
|
|
private void OnStatus(SyncStatus s)
|
|
{
|
|
IsSyncing = s.State == SyncState.Syncing;
|
|
IsIncompatibleVersion = s.State == SyncState.IncompatibleVersion;
|
|
CanAttemptSync = IsServerConfigured && !IsIncompatibleVersion;
|
|
PendingCount = s.PendingEvents;
|
|
StatusText = s.State switch
|
|
{
|
|
SyncState.Idle => PendingCount > 0 ? $"{PendingCount} ausstehend" : "Synchronisiert",
|
|
SyncState.Syncing => "Synchronisiere…",
|
|
SyncState.Offline => "Offline",
|
|
SyncState.Error => $"Fehler: {s.ErrorMessage}",
|
|
SyncState.IncompatibleVersion => "Update erforderlich",
|
|
_ => "",
|
|
};
|
|
LastSyncText = s.LastSyncAt.HasValue ? $"Zuletzt: {s.LastSyncAt:HH:mm}" : "Noch nie";
|
|
|
|
if (IsIncompatibleVersion && !_incompatibleNotificationShown)
|
|
{
|
|
_incompatibleNotificationShown = true;
|
|
_notifications?.ShowError(IncompatibleVersionMessage);
|
|
}
|
|
|
|
// Offline/Error sonst nur als blasser Text in der Statuszeile sichtbar war - über Stunden
|
|
// unbemerkt, bis ein Blick auf ein zweites Gerät fehlende Daten offenbarte. Einmalig pro
|
|
// neuem Ausfall (nicht bei jedem automatischen Retry alle paar Minuten) aktiv als Toast
|
|
// melden; _lastNotifiedState wird bei Rückkehr zu Idle zurückgesetzt, damit ein späterer,
|
|
// neuer Ausfall wieder gemeldet wird.
|
|
if (s.State is SyncState.Offline or SyncState.Error && _lastNotifiedState != s.State)
|
|
{
|
|
_lastNotifiedState = s.State;
|
|
_notifications?.ShowError(s.State == SyncState.Offline
|
|
? OfflineMessage
|
|
: $"Synchronisierung fehlgeschlagen: {s.ErrorMessage}");
|
|
}
|
|
else if (s.State is SyncState.Idle)
|
|
{
|
|
// Nicht auch bei SyncState.Syncing zurücksetzen: Jeder Sync-Versuch (auch ein
|
|
// automatischer Retry im selben Ausfall) durchläuft kurz Syncing, bevor er wieder in
|
|
// Offline/Error landet - ein Reset hier würde bei jedem Retry erneut einen Toast
|
|
// auslösen, statt nur einmal pro tatsächlich neuem Ausfall.
|
|
_lastNotifiedState = SyncState.Idle;
|
|
}
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanSync))]
|
|
private async Task SyncNow() { if (_engine is not null) await _engine.SyncNowAsync(); }
|
|
private bool CanSync() => _engine is not null && !IsSyncing;
|
|
}
|