diff --git a/LehrerApp.Desktop.Tests/Fakes.cs b/LehrerApp.Desktop.Tests/Fakes.cs
index a8594b0..b750865 100644
--- a/LehrerApp.Desktop.Tests/Fakes.cs
+++ b/LehrerApp.Desktop.Tests/Fakes.cs
@@ -56,6 +56,14 @@ public static class TestSupport
Directory.CreateDirectory(tempPath);
return new PeriodScheduleService(tempPath);
}
+
+ /// Analog zu , eigenes Temp-Verzeichnis je Aufruf.
+ public static AppLogger BuildAppLogger()
+ {
+ var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-applogger-tests-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(tempPath);
+ return new AppLogger(tempPath);
+ }
}
public class FakeStudents(List all) : IStudentRepository
diff --git a/LehrerApp.Desktop.Tests/SettingsViewModelTests.cs b/LehrerApp.Desktop.Tests/SettingsViewModelTests.cs
index ba1205e..7dcd607 100644
--- a/LehrerApp.Desktop.Tests/SettingsViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/SettingsViewModelTests.cs
@@ -32,7 +32,7 @@ public sealed class SettingsViewModelTests
new PeriodScheduleService(tempPath), supervisionDuties ?? new FakeSupervisionDuties(),
new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(),
- eventQueue ?? TestSupport.BuildEventQueue());
+ eventQueue ?? TestSupport.BuildEventQueue(), TestSupport.BuildAppLogger());
}
[Fact]
@@ -148,7 +148,8 @@ public sealed class SettingsViewModelTests
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
new FakeSchoolHolidays(), calendarSettings, new PeriodScheduleService(tempPath),
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
- TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue());
+ TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
+ TestSupport.BuildAppLogger());
vm.SelectedStateName = "Bayern";
@@ -171,7 +172,8 @@ public sealed class SettingsViewModelTests
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
- TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue());
+ TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
+ TestSupport.BuildAppLogger());
vm.PeriodTimes[0].StartText = "08:00";
vm.PeriodTimes[0].EndText = "08:45";
@@ -198,7 +200,8 @@ public sealed class SettingsViewModelTests
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
- TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue());
+ TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
+ TestSupport.BuildAppLogger());
vm.PeriodTimes[0].StartText = "08:45";
vm.PeriodTimes[0].EndText = "08:00";
diff --git a/LehrerApp.Desktop/AppBootstrapper.cs b/LehrerApp.Desktop/AppBootstrapper.cs
index 60097a5..ae1ae0b 100644
--- a/LehrerApp.Desktop/AppBootstrapper.cs
+++ b/LehrerApp.Desktop/AppBootstrapper.cs
@@ -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);
}
diff --git a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs
index 92a6b39..182a976 100644
--- a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs
@@ -212,6 +212,12 @@ public partial class SettingsViewModel : ObservableObject
/// den Stand des anderen Geräts ersetzt wird.
public Func>? 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? 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();
}
diff --git a/LehrerApp.Desktop/Views/Settings/SettingsView.axaml.cs b/LehrerApp.Desktop/Views/Settings/SettingsView.axaml.cs
index 42e3ad7..563f4a0 100644
--- a/LehrerApp.Desktop/Views/Settings/SettingsView.axaml.cs
+++ b/LehrerApp.Desktop/Views/Settings/SettingsView.axaml.cs
@@ -21,6 +21,7 @@ public partial class SettingsView : UserControl
vm.OnAppLockChanged = () => App.Services.GetRequiredService().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(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(owner);
+ }
+
private async void OnImportClick(object? sender, RoutedEventArgs e)
{
var topLevel = TopLevel.GetTopLevel(this);
diff --git a/TODO.md b/TODO.md
index 35fd703..654ad54 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1533,6 +1533,23 @@ die Docker-Verifikation unter 10.2.4 (kein Docker im Entwicklungsstand verfügba
Einlösen (überschreibt die lokale Datenbank vollständig, alter Stand wird automatisch als
Backup gesichert). Kein QR-Code (nur Passphrase-Code) — für zwei eigene Geräte per Hand
abtippen ausreichend, QR-Code wäre erst für eine Companion-App relevant.
+
+ **Nachtrag (Bugfix unsichtbarer Fehlschlag beim Einlösen):** Nutzer-Bug-Report — beim
+ Einlösen eines Codes auf dem zweiten Gerät schloss sich die App kommentarlos, nach dem
+ Neustart war der alte Datenbestand weiterhin da. Ursache: `RedeemPairingCode()` ruft
+ `AppBootstrapper.RestartApplication()` bewusst in JEDEM Fall auf (auch bei Fehlschlag, siehe
+ Kommentar dort — `_dbContext` ist zu dem Zeitpunkt bereits disposed), aber direkt im Anschluss
+ an das Setzen von `PairingStatus`, ohne dass Avalonia je einen Frame damit rendern konnte —
+ ein Fehlschlag (falscher/abgelaufener Code, Server nicht erreichbar) war dadurch komplett
+ unsichtbar, zusätzlich wurde die Exception nirgends geloggt. Behoben: `SettingsViewModel`
+ bekommt jetzt `AppLogger` injiziert und protokolliert den Fehler; ein neuer, vom Code-Behind
+ gesetzter `OnShowPairingError`-Callback zeigt vor dem Neustart einen `ConfirmDialog`, den der
+ Nutzer aktiv wegklicken muss — das garantiert, dass die Fehlermeldung tatsächlich gelesen
+ werden kann, bevor der Prozess beendet wird. Zusätzlich `AppBootstrapper.RestartApplication()`
+ robuster gemacht: unter `dotnet run`/IDE-Debug zeigt `Environment.ProcessPath` auf den
+ SDK-Host (`dotnet`/`dotnet.exe`) statt auf die App selbst, ein Neustart ohne Argumente hätte
+ dort nur die dotnet-CLI-Hilfe gezeigt statt die App neu zu starten — die ursprünglichen
+ Kommandozeilenargumente werden in diesem Fall jetzt erneut mitgegeben.
- [ ] **10.3.2** Warnung und Wiederherstellungspfad bei verlorenem Schlüssel.
- [x] **10.3.3** Prüfen, welche Daten unverschlüsselt über `PlainEventStore` laufen —
personenbezogene Daten dürfen das nicht. `Grade`/`ExamResult` (beide mit `StudentId` plus