- Add ReactivatePipeline to handle subscription reactivation, including scaling Docker services, health verification, status updates, audit logging, and broadcasting status changes. - Introduce RotateCredentialsPipeline for OAuth2 credential rotation, managing the deletion of old apps, creation of new ones, credential storage, access verification, and audit logging. - Create StepRunner to manage job step execution, including lifecycle management and progress broadcasting via SignalR. - Implement SuspendPipeline for subscription suspension, scaling down services, updating statuses, logging audits, and broadcasting changes. - Add UpdateScreenLimitPipeline to update Xibo CMS screen limits and record snapshots. - Introduce XiboFeatureManifests for hardcoded feature ACLs per role. - Add docker-compose.dev.yml for local development with PostgreSQL setup.
209 lines
8.4 KiB
C#
209 lines
8.4 KiB
C#
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.Http;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Polly;
|
|
using Polly.Extensions.Http;
|
|
using Refit;
|
|
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<XiboContext>();
|
|
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<SettingsService>();
|
|
await settings.PreloadCacheAsync();
|
|
Log.Information("Bitwarden config settings pre-loaded");
|
|
|
|
// Import existing instance secrets that aren't yet tracked
|
|
var postInit = Services.GetRequiredService<PostInstanceInitService>();
|
|
await postInit.ImportExistingInstanceSecretsAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Warning(ex, "Failed to pre-load settings or import instance secrets on startup");
|
|
}
|
|
});
|
|
|
|
var vm = Services.GetRequiredService<MainWindowViewModel>();
|
|
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");
|
|
|
|
// Start the SignalR connection (fire-and-forget, reconnect handles failures)
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
var signalR = Services.GetRequiredService<ServerSignalRService>();
|
|
await signalR.StartAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Warning(ex, "Failed to start SignalR connection on startup");
|
|
}
|
|
});
|
|
|
|
desktop.ShutdownRequested += (_, _) =>
|
|
{
|
|
var ssh = Services.GetService<SshConnectionService>();
|
|
ssh?.Dispose();
|
|
var signalR = Services.GetService<ServerSignalRService>();
|
|
signalR?.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
|
};
|
|
}
|
|
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<IConfiguration>(config);
|
|
|
|
// Options
|
|
services.Configure<GitOptions>(config.GetSection(GitOptions.SectionName));
|
|
services.Configure<DockerOptions>(config.GetSection(DockerOptions.SectionName));
|
|
services.Configure<XiboOptions>(config.GetSection(XiboOptions.SectionName));
|
|
services.Configure<DatabaseOptions>(config.GetSection(DatabaseOptions.SectionName));
|
|
services.Configure<FileLoggingOptions>(config.GetSection(FileLoggingOptions.SectionName));
|
|
services.Configure<BitwardenOptions>(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<XiboContext>(options => options.UseSqlite(connStr));
|
|
|
|
// HTTP
|
|
services.AddHttpClient();
|
|
services.AddHttpClient("XiboApi");
|
|
services.AddHttpClient("XiboHealth");
|
|
services.AddHttpClient("AuthentikApi");
|
|
|
|
// ── Server API integration ──────────────────────────────────────────
|
|
services.AddSingleton<TokenStoreService>();
|
|
services.AddTransient<AuthHeaderHandler>();
|
|
|
|
var serverBaseUrl = config["ServerBaseUrl"] ?? "https://localhost:5001";
|
|
services.AddRefitClient<IServerApiClient>()
|
|
.ConfigureHttpClient(c => c.BaseAddress = new Uri(serverBaseUrl))
|
|
.AddHttpMessageHandler<AuthHeaderHandler>()
|
|
.AddPolicyHandler(HttpPolicyExtensions
|
|
.HandleTransientHttpError()
|
|
.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))));
|
|
|
|
services.AddSingleton<ServerSignalRService>();
|
|
|
|
// SSH services (singletons — maintain connections)
|
|
services.AddSingleton<SshConnectionService>();
|
|
|
|
// Docker services via SSH (singletons — SetHost() must persist across scopes)
|
|
services.AddSingleton<SshDockerCliService>();
|
|
services.AddSingleton<SshDockerSecretsService>();
|
|
services.AddSingleton<IDockerCliService>(sp => sp.GetRequiredService<SshDockerCliService>());
|
|
services.AddSingleton<IDockerSecretsService>(sp => sp.GetRequiredService<SshDockerSecretsService>());
|
|
|
|
// Core services
|
|
services.AddTransient<SettingsService>();
|
|
services.AddTransient<GitTemplateService>();
|
|
services.AddTransient<ComposeRenderService>();
|
|
services.AddTransient<ComposeValidationService>();
|
|
services.AddTransient<XiboApiService>();
|
|
services.AddTransient<InstanceService>();
|
|
services.AddTransient<IBitwardenSecretService, BitwardenSecretService>();
|
|
services.AddTransient<IAuthentikService, AuthentikService>();
|
|
services.AddTransient<IInvitationSetupService, InvitationSetupService>();
|
|
services.AddSingleton<PostInstanceInitService>();
|
|
|
|
// ViewModels
|
|
services.AddSingleton<MainWindowViewModel>(); // singleton: one main window, nav state shared
|
|
services.AddTransient<HostsViewModel>();
|
|
services.AddTransient<InstancesViewModel>();
|
|
services.AddTransient<InstanceDetailsViewModel>();
|
|
services.AddTransient<CreateInstanceViewModel>();
|
|
services.AddTransient<SecretsViewModel>();
|
|
services.AddTransient<SettingsViewModel>();
|
|
services.AddTransient<LogsViewModel>();
|
|
}
|
|
}
|