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:
@@ -56,6 +56,14 @@ public static class TestSupport
|
|||||||
Directory.CreateDirectory(tempPath);
|
Directory.CreateDirectory(tempPath);
|
||||||
return new PeriodScheduleService(tempPath);
|
return new PeriodScheduleService(tempPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Analog zu <see cref="BuildAiSettingsService"/>, 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<Student> all) : IStudentRepository
|
public class FakeStudents(List<Student> all) : IStudentRepository
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public sealed class SettingsViewModelTests
|
|||||||
new PeriodScheduleService(tempPath), supervisionDuties ?? new FakeSupervisionDuties(),
|
new PeriodScheduleService(tempPath), supervisionDuties ?? new FakeSupervisionDuties(),
|
||||||
new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(),
|
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(),
|
||||||
eventQueue ?? TestSupport.BuildEventQueue());
|
eventQueue ?? TestSupport.BuildEventQueue(), TestSupport.BuildAppLogger());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -148,7 +148,8 @@ public sealed class SettingsViewModelTests
|
|||||||
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||||
new FakeSchoolHolidays(), calendarSettings, new PeriodScheduleService(tempPath),
|
new FakeSchoolHolidays(), calendarSettings, new PeriodScheduleService(tempPath),
|
||||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
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";
|
vm.SelectedStateName = "Bayern";
|
||||||
|
|
||||||
@@ -171,7 +172,8 @@ public sealed class SettingsViewModelTests
|
|||||||
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||||
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
||||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
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].StartText = "08:00";
|
||||||
vm.PeriodTimes[0].EndText = "08:45";
|
vm.PeriodTimes[0].EndText = "08:45";
|
||||||
@@ -198,7 +200,8 @@ public sealed class SettingsViewModelTests
|
|||||||
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||||
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
||||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
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].StartText = "08:45";
|
||||||
vm.PeriodTimes[0].EndText = "08:00";
|
vm.PeriodTimes[0].EndText = "08:00";
|
||||||
|
|||||||
@@ -82,7 +82,17 @@ public static class AppBootstrapper
|
|||||||
{
|
{
|
||||||
var exePath = Environment.ProcessPath;
|
var exePath = Environment.ProcessPath;
|
||||||
if (!string.IsNullOrEmpty(exePath))
|
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);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -212,6 +212,12 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
/// den Stand des anderen Geräts ersetzt wird.
|
/// den Stand des anderen Geräts ersetzt wird.
|
||||||
public Func<Task<bool>>? OnConfirmPairingRestore { get; set; }
|
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 ───────────────────────────────────────────────────────────
|
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private readonly ISchoolHolidayRepository _schoolHolidays;
|
private readonly ISchoolHolidayRepository _schoolHolidays;
|
||||||
@@ -225,6 +231,7 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
private readonly EventQueue _eventQueue;
|
private readonly EventQueue _eventQueue;
|
||||||
private readonly SnapshotService? _snapshotService;
|
private readonly SnapshotService? _snapshotService;
|
||||||
private readonly CompetencyCatalogImportService _catalogImport;
|
private readonly CompetencyCatalogImportService _catalogImport;
|
||||||
|
private readonly AppLogger _logger;
|
||||||
|
|
||||||
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
|
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
|
||||||
IGradingKeyTemplateRepository gradingKeyTemplates, IGradingSchemeRepository gradingSchemes,
|
IGradingKeyTemplateRepository gradingKeyTemplates, IGradingSchemeRepository gradingSchemes,
|
||||||
@@ -236,8 +243,9 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
ISupervisionDutyRepository supervisionDuties, LetterTemplateService letterTemplates,
|
ISupervisionDutyRepository supervisionDuties, LetterTemplateService letterTemplates,
|
||||||
AiSettingsService aiSettings, AiPlanningService aiPlanning,
|
AiSettingsService aiSettings, AiPlanningService aiPlanning,
|
||||||
SyncSettingsService syncSettings, SyncAuthService syncAuth, EventQueue eventQueue,
|
SyncSettingsService syncSettings, SyncAuthService syncAuth, EventQueue eventQueue,
|
||||||
SnapshotService? snapshotService = null)
|
AppLogger logger, SnapshotService? snapshotService = null)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
_subjects = subjects;
|
_subjects = subjects;
|
||||||
_domainRepo = domainRepo;
|
_domainRepo = domainRepo;
|
||||||
_gradingKeyTemplates = gradingKeyTemplates;
|
_gradingKeyTemplates = gradingKeyTemplates;
|
||||||
@@ -564,6 +572,11 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
// _dbContext ist bereits disposed, PairingStatus ist aber ein reines ViewModel-Feld
|
// _dbContext ist bereits disposed, PairingStatus ist aber ein reines ViewModel-Feld
|
||||||
// ohne DB-Zugriff - das Setzen ist unabhängig davon noch sicher.
|
// ohne DB-Zugriff - das Setzen ist unabhängig davon noch sicher.
|
||||||
PairingStatus = $"Fehlgeschlagen: {ex.Message}";
|
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();
|
AppBootstrapper.RestartApplication();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public partial class SettingsView : UserControl
|
|||||||
vm.OnAppLockChanged = () => App.Services.GetRequiredService<AppLockViewModel>().ApplyConfig();
|
vm.OnAppLockChanged = () => App.Services.GetRequiredService<AppLockViewModel>().ApplyConfig();
|
||||||
vm.OnConfirmHardDelete = ShowHardDeleteConfirmDialog;
|
vm.OnConfirmHardDelete = ShowHardDeleteConfirmDialog;
|
||||||
vm.OnConfirmPairingRestore = ShowPairingRestoreConfirmDialog;
|
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);
|
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)
|
private async void OnImportClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var topLevel = TopLevel.GetTopLevel(this);
|
var topLevel = TopLevel.GetTopLevel(this);
|
||||||
|
|||||||
@@ -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
|
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
|
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.
|
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.
|
- [ ] **10.3.2** Warnung und Wiederherstellungspfad bei verlorenem Schlüssel.
|
||||||
- [x] **10.3.3** Prüfen, welche Daten unverschlüsselt über `PlainEventStore` laufen —
|
- [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
|
personenbezogene Daten dürfen das nicht. `Grade`/`ExamResult` (beide mit `StudentId` plus
|
||||||
|
|||||||
Reference in New Issue
Block a user