fix: Geräte-Pairing - Fehlschlag beim Einlösen war unsichtbar

RedeemPairingCode() rief nach einem Fehlschlag sofort RestartApplication()
auf, direkt nach dem Setzen von PairingStatus - die Fehlermeldung konnte
nie gerendert werden, und wurde auch nirgends geloggt. Ein Fehlschlag
(falscher/abgelaufener Code, Server nicht erreichbar) wirkte dadurch wie
ein kommentarloser Absturz ohne jede Spur.

- SettingsViewModel loggt den Fehler jetzt über AppLogger und zeigt vor
  dem Neustart einen Bestätigungsdialog mit der echten Fehlermeldung an.
- AppBootstrapper.RestartApplication() gibt unter dotnet run/IDE-Debug
  (wo ProcessPath auf den dotnet-Host statt die App zeigt) die
  ursprünglichen Kommandozeilenargumente beim Neustart mit, statt nur
  die dotnet-CLI-Hilfe zu öffnen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 16:35:11 +02:00
co-authored by Claude Sonnet 5
parent d59d658bba
commit f2cb3c98c6
6 changed files with 71 additions and 6 deletions
+11 -1
View File
@@ -82,7 +82,17 @@ public static class AppBootstrapper
{
var exePath = Environment.ProcessPath;
if (!string.IsNullOrEmpty(exePath))
System.Diagnostics.Process.Start(exePath);
{
var psi = new System.Diagnostics.ProcessStartInfo(exePath) { UseShellExecute = false };
// Unter "dotnet run"/IDE-Debug zeigt ProcessPath auf den SDK-Host (dotnet[.exe]) statt
// auf die App selbst - ein Neustart ohne Argumente würde nur die dotnet-CLI-Hilfe
// anzeigen statt die App neu zu starten. Die ursprünglichen Kommandozeilenargumente
// (u.a. der DLL-Pfad) erneut mitgeben deckt auch diesen Fall ab.
if (Path.GetFileNameWithoutExtension(exePath).Equals("dotnet", StringComparison.OrdinalIgnoreCase))
foreach (var arg in Environment.GetCommandLineArgs())
psi.ArgumentList.Add(arg);
System.Diagnostics.Process.Start(psi);
}
Environment.Exit(0);
}
@@ -212,6 +212,12 @@ public partial class SettingsViewModel : ObservableObject
/// 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; }
// ── Konstruktor ───────────────────────────────────────────────────────────
private readonly ISchoolHolidayRepository _schoolHolidays;
@@ -225,6 +231,7 @@ public partial class SettingsViewModel : ObservableObject
private readonly EventQueue _eventQueue;
private readonly SnapshotService? _snapshotService;
private readonly CompetencyCatalogImportService _catalogImport;
private readonly AppLogger _logger;
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
IGradingKeyTemplateRepository gradingKeyTemplates, IGradingSchemeRepository gradingSchemes,
@@ -236,8 +243,9 @@ public partial class SettingsViewModel : ObservableObject
ISupervisionDutyRepository supervisionDuties, LetterTemplateService letterTemplates,
AiSettingsService aiSettings, AiPlanningService aiPlanning,
SyncSettingsService syncSettings, SyncAuthService syncAuth, EventQueue eventQueue,
SnapshotService? snapshotService = null)
AppLogger logger, SnapshotService? snapshotService = null)
{
_logger = logger;
_subjects = subjects;
_domainRepo = domainRepo;
_gradingKeyTemplates = gradingKeyTemplates;
@@ -564,6 +572,11 @@ public partial class SettingsViewModel : ObservableObject
// _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();
}
@@ -21,6 +21,7 @@ public partial class SettingsView : UserControl
vm.OnAppLockChanged = () => App.Services.GetRequiredService<AppLockViewModel>().ApplyConfig();
vm.OnConfirmHardDelete = ShowHardDeleteConfirmDialog;
vm.OnConfirmPairingRestore = ShowPairingRestoreConfirmDialog;
vm.OnShowPairingError = ShowPairingErrorDialog;
}
}
@@ -66,6 +67,19 @@ public partial class SettingsView : UserControl
return owner is not null && await dialog.ShowDialog<bool>(owner);
}
private async Task ShowPairingErrorDialog(string message)
{
var info = new ConfirmDialogInfo
{
Title = "Kopplung fehlgeschlagen",
Message = $"{message}\n\nDie App wird jetzt neu gestartet (unveränderte Datenbank).",
ConfirmText = "OK",
};
var dialog = new ConfirmDialog { DataContext = info };
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is not null) await dialog.ShowDialog<bool>(owner);
}
private async void OnImportClick(object? sender, RoutedEventArgs e)
{
var topLevel = TopLevel.GetTopLevel(this);