using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Serilog; using OTSSignsOrchestrator.Core.Configuration; using OTSSignsOrchestrator.Core.Data; using OTSSignsOrchestrator.Core.Services; using OTSSignsOrchestrator.Desktop.Services; using OTSSignsOrchestrator.Desktop.ViewModels; using OTSSignsOrchestrator.Desktop.Views; namespace OTSSignsOrchestrator.Desktop; public class App : Application { public static IServiceProvider Services { get; private set; } = null!; public override void Initialize() { AvaloniaXamlLoader.Load(this); } public override void OnFrameworkInitializationCompleted() { var services = new ServiceCollection(); ConfigureServices(services); Services = services.BuildServiceProvider(); // Apply migrations using (var scope = Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); db.Database.Migrate(); } Log.Information("ApplicationLifetime type: {Type}", ApplicationLifetime?.GetType().FullName ?? "null"); if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { Log.Information("Creating MainWindow..."); // Import existing instance secrets from Bitwarden (fire-and-forget, non-blocking) _ = Task.Run(async () => { try { // Pre-load config settings from Bitwarden so they're available immediately using var scope = Services.CreateScope(); var settings = scope.ServiceProvider.GetRequiredService(); await settings.PreloadCacheAsync(); Log.Information("Bitwarden config settings pre-loaded"); // Import existing instance secrets that aren't yet tracked var postInit = Services.GetRequiredService(); await postInit.ImportExistingInstanceSecretsAsync(); } catch (Exception ex) { Log.Warning(ex, "Failed to pre-load settings or import instance secrets on startup"); } }); var vm = Services.GetRequiredService(); Log.Information("MainWindowViewModel resolved"); var window = new MainWindow { DataContext = vm }; desktop.MainWindow = window; Log.Information("MainWindow assigned to lifetime"); window.Show(); window.Activate(); Log.Information("MainWindow Show() + Activate() called"); desktop.ShutdownRequested += (_, _) => { var ssh = Services.GetService(); ssh?.Dispose(); }; } else { Log.Warning("ApplicationLifetime is NOT IClassicDesktopStyleApplicationLifetime — window cannot be shown"); } base.OnFrameworkInitializationCompleted(); } private static void ConfigureServices(IServiceCollection services) { // Configuration (reloadOnChange so runtime writes to appsettings.json are picked up) var config = new ConfigurationBuilder() .SetBasePath(AppContext.BaseDirectory) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build(); services.AddSingleton(config); // Options services.Configure(config.GetSection(GitOptions.SectionName)); services.Configure(config.GetSection(DockerOptions.SectionName)); services.Configure(config.GetSection(XiboOptions.SectionName)); services.Configure(config.GetSection(DatabaseOptions.SectionName)); services.Configure(config.GetSection(FileLoggingOptions.SectionName)); services.Configure(config.GetSection(BitwardenOptions.SectionName)); // Logging services.AddLogging(builder => { builder.ClearProviders(); builder.AddSerilog(dispose: true); }); // Data Protection var keysDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "OTSSignsOrchestrator", "keys"); Directory.CreateDirectory(keysDir); services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(keysDir)) .SetApplicationName("OTSSignsOrchestrator"); // Database var connStr = config.GetConnectionString("Default") ?? "Data Source=otssigns-desktop.db"; services.AddDbContext(options => options.UseSqlite(connStr)); // HTTP services.AddHttpClient(); services.AddHttpClient("XiboApi"); services.AddHttpClient("XiboHealth"); services.AddHttpClient("AuthentikApi"); // SSH services (singletons — maintain connections) services.AddSingleton(); // Docker services via SSH (singletons — SetHost() must persist across scopes) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); // Core services services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddSingleton(); // ViewModels services.AddSingleton(); // singleton: one main window, nav state shared services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); } }