using LehrerApp.Core.Interfaces; using LehrerApp.Core.Importing; using LehrerApp.Core.Services; using LehrerApp.Data; using LehrerApp.Data.Repositories; using LehrerApp.Desktop.Services; using LehrerApp.Desktop.ViewModels; using LehrerApp.Desktop.ViewModels.Groups; using LehrerApp.Desktop.ViewModels.Planning; using LehrerApp.Desktop.ViewModels.Settings; using LehrerApp.Desktop.ViewModels.Students; using LehrerApp.Desktop.ViewModels.Workload; using LehrerApp.Sync; using LehrerApp.Sync.Crypto; using LehrerApp.Sync.Models; using Microsoft.Extensions.DependencyInjection; namespace LehrerApp.Desktop; /// /// Konfiguriert den DI-Container für den Desktop-Client. /// /// WICHTIG: Alle Repositories sind Singleton – eine LiteDB-Datei pro Nutzer. /// Sync-Services werden nur registriert wenn eine Server-URL konfiguriert ist. /// public static class AppBootstrapper { public static string DbPath { get; private set; } = ""; public static string AppDataPath { get; private set; } = ""; /// /// Feste URL des KI-Backends (ai-backend/, TODO 4.5.9) — bewusst nicht in den Einstellungen /// editierbar, siehe Planungsdokument. Vor dem ersten produktiven Einsatz durch die tatsächlich /// deployte Domain ersetzen. /// public const string AiBackendUrl = "https://backapi.science-teaching.de/"; /// /// Vor gesetzt, wenn die Datenbank passwortgeschützt ist /// (siehe App.axaml.cs: Passwort-Abfrage vor dem Öffnen der Datenbank). /// public static string? DbPassword { get; set; } /// /// Vor der eigentlichen DI-Konfiguration verfügbar (z.B. für den globalen /// Exception-Handler in Program.cs, der schon vor greifen muss). /// public static AppLogger Logger { get; private set; } = null!; public static string ResolveAppDataPath() { if (!string.IsNullOrEmpty(AppDataPath)) return AppDataPath; var appData = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LehrerApp"); Directory.CreateDirectory(appData); AppDataPath = appData; return appData; } /// Vor dem Öffnen der Datenbank verfügbar (z.B. für die Passwort-Abfrage beim Start). public static string ResolveDbPath() { if (string.IsNullOrEmpty(DbPath)) DbPath = Path.Combine(ResolveAppDataPath(), "lehrerapp.db"); return DbPath; } public static AppLogger EnsureLogger() { Logger ??= new AppLogger(ResolveAppDataPath()); return Logger; } public static bool IsDbEncrypted() => new DatabaseEncryptionService().IsEncrypted(ResolveDbPath()); /// Beendet den Prozess und startet ihn neu (z.B. nach Wiederherstellung eines Backups /// oder einer Änderung der Datenbank-Verschlüsselung — die laufende LiteDB-Verbindung /// kann nicht sicher "heiß" auf eine andere Datei umgehängt werden). public static void RestartApplication() { var exePath = Environment.ProcessPath; if (!string.IsNullOrEmpty(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); } public static ServiceProvider BuildServices() { var services = new ServiceCollection(); // ── Pfade ───────────────────────────────────────────────────────────── var appData = ResolveAppDataPath(); ResolveDbPath(); var queuePath = Path.Combine(appData, "syncqueue.db"); var keyPath = Path.Combine(appData, "sync.key"); // ── Logging & Benachrichtigungen ───────────────────────────────────────── EnsureLogger(); services.AddSingleton(Logger); services.AddSingleton(); // ── Datensicherheit (13.3) ─────────────────────────────────────────── // Backup läuft vor dem Öffnen der Datenbank, rein dateibasiert (unabhängig von // Verschlüsselung) — reine Datei-Kopie, kein offener LiteDB-Handle nötig. var backup = new BackupService(appData); backup.CreateBackup(DbPath); services.AddSingleton(backup); services.AddSingleton(_ => new AppLockService(appData)); services.AddSingleton(); services.AddSingleton(_ => new PrivacySettingsService(appData)); services.AddSingleton(); services.AddSingleton(); services.AddSingleton, StudentMasterDataCsvImportHandler>(); services.AddSingleton, LessonStudentListCsvImportHandler>(); services.AddSingleton, MarksPerLessonCsvImportHandler>(); services.AddSingleton(); // ── Datenbank ───────────────────────────────────────────────────────── services.AddSingleton(_ => new LiteDbContext(DbPath, DbPassword)); // ── Repositories ────────────────────────────────────────────────────── services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // ── Services ────────────────────────────────────────────────────────── services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(_ => new SchoolCalendarSettingsService(appData)); services.AddSingleton(_ => new PeriodScheduleService(appData)); services.AddSingleton(_ => new WorkloadSettingsService(appData)); services.AddSingleton(_ => new DashboardSettingsService(appData)); services.AddSingleton(_ => new LetterTemplateService(appData)); services.AddSingleton(); // ── KI-Unterstützung (4.5.9, optional – nur wenn in den Einstellungen aktiviert) ────── services.AddSingleton(_ => new AiSettingsService(appData)); services.AddSingleton(_ => new HttpClient { BaseAddress = new Uri(AiBackendUrl) }); services.AddSingleton(); // ── Sync (optional – nur wenn Server konfiguriert) ──────────────────── var syncSettings = new SyncSettingsService(appData); services.AddSingleton(syncSettings); services.AddSingleton(_ => new SyncAuthService(new HttpClient())); services.AddSingleton(_ => new EventQueue(queuePath)); services.AddSingleton(sp => new ConflictResolver(sp.GetRequiredService())); services.AddSingleton(_ => { var key = SyncCrypto.LoadKey(keyPath) ?? SyncCrypto.GenerateKey(); SyncCrypto.SaveKey(key, keyPath); return key; }); var serverUrl = syncSettings.ServerUrl; var deviceId = LoadOrCreateDeviceId(appData); if (!string.IsNullOrEmpty(serverUrl)) { services.AddSingleton(sp => new EventApplier( sp.GetRequiredService(), sp.GetRequiredService(), BuildHttp(serverUrl, syncSettings))); services.AddSingleton(sp => new SyncEventPublisher( sp.GetRequiredService(), deviceId, sp.GetRequiredService())); services.AddSingleton(sp => new AttachmentSyncer( sp.GetRequiredService(), BuildHttp(serverUrl, syncSettings), sp.GetRequiredService())); services.AddSingleton(sp => new SyncEngine( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), BuildHttp(serverUrl, syncSettings), new SyncConfig { ServerUrl = serverUrl, DeviceId = deviceId, DeviceType = DeviceType.Desktop, AutoSyncIntervalMinutes = 5, })); services.AddSingleton(sp => new SnapshotService( BuildHttp(serverUrl, syncSettings), sp.GetRequiredService(), sp.GetRequiredService(), DeviceType.Desktop, DbPath, keyPath)); } // ── ViewModels ──────────────────────────────────────────────────────── // Singleton: einmal erstellt, überall dieselbe Instanz services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(sp => new SyncStatusViewModel(sp.GetService())); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // Transient: neue Instanz pro Navigation (für Detailseiten) services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); var provider = services.BuildServiceProvider(); // Sync-agnostischer Hook auf LiteDbContext (siehe LiteDbContext.OnChange) wird erst hier, // außerhalb der Repository-Registrierung, mit der tatsächlichen Sync-Logik verbunden. if (!string.IsNullOrEmpty(serverUrl)) provider.GetRequiredService().OnChange = provider.GetRequiredService().Publish; return provider; } // ── Hilfsmethoden ───────────────────────────────────────────────────────── private static HttpClient BuildHttp(string url, SyncSettingsService syncSettings) { var http = new HttpClient { BaseAddress = new Uri(url) }; var token = syncSettings.GetToken(); if (token is not null) http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); return http; } private static string LoadOrCreateDeviceId(string appData) { var p = Path.Combine(appData, "device.id"); if (File.Exists(p)) return File.ReadAllText(p).Trim(); var id = Guid.NewGuid().ToString(); File.WriteAllText(p, id); return id; } }