using MeterVault.App.Api; using MeterVault.App.Components; using MeterVault.App.Localization; using MeterVault.Infrastructure; using MeterVault.Infrastructure.Options; using MeterVault.Infrastructure.Persistence; using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using MudBlazor.Services; using Serilog; Log.Logger = new LoggerConfiguration() .WriteTo.Console() .CreateBootstrapLogger(); try { var builder = WebApplication.CreateBuilder(args); builder.Host.UseSerilog((context, services, configuration) => configuration .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext() .WriteTo.Console()); builder.Services.Configure( builder.Configuration.GetSection(MeterVaultOptions.SectionName)); var connectionString = builder.Configuration.GetConnectionString("Default") ?? "Host=localhost;Port=5432;Database=metervault;Username=metervault;Password=metervault"; builder.Services.AddMeterVaultInfrastructure(connectionString); // Key ring for connector secrets typed into the admin UI. It must outlive the app directory: // the LXC updater republishes /opt/metervault on every update, so keys stored beside the // binaries would be destroyed and every saved token would need re-entering. Override with // MeterVault__DataProtectionKeyPath (Docker: point it at a mounted volume). var keyPath = builder.Configuration["MeterVault:DataProtectionKeyPath"]; if (string.IsNullOrWhiteSpace(keyPath)) { keyPath = OperatingSystem.IsWindows() ? Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MeterVault", "keys") : "/var/lib/metervault/keys"; } try { Directory.CreateDirectory(keyPath); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { // Falling back beats refusing to boot, but say so plainly: on the fallback path an update // that replaces the content root loses the keys, and stored secrets stop decrypting. var fallback = Path.Combine(builder.Environment.ContentRootPath, "keys"); Log.Warning(ex, "Cannot create data-protection key ring at {KeyPath}; falling back to {Fallback}. " + "Secrets entered in the admin UI will not survive a redeploy that replaces the content " + "root — set MeterVault__DataProtectionKeyPath to a writable persistent directory", keyPath, fallback); keyPath = fallback; Directory.CreateDirectory(keyPath); } builder.Services.AddDataProtection() .SetApplicationName("MeterVault") .PersistKeysToFileSystem(new DirectoryInfo(keyPath)); var options = builder.Configuration.GetSection(MeterVaultOptions.SectionName).Get() ?? new MeterVaultOptions(); if (options.EnableLiveIngestion) { builder.Services.AddMeterVaultIngestion(); } builder.Services.AddLocalization(); builder.Services.AddMudServices(); builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); // Serialize/accept enums as strings on the REST API (e.g. event Type "Delivery"). builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter())); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => c.SwaggerDoc("v1", new() { Title = "MeterVault API", Version = "v1" })); var app = builder.Build(); await MigrateDatabaseAsync(app).ConfigureAwait(false); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error", createScopeForErrors: true); app.UseHsts(); } app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); app.UseSerilogRequestLogging(); app.UseReverseProxyTrust(); // No HTTPS redirection: the app serves plain HTTP (port 8760) behind a reverse proxy // that terminates TLS (SDD §10). HTTPS redirection here would break the container and proxy. app.UseAntiforgery(); // UI language (SDD §12, M7). Must run before MapRazorComponents: a Blazor Server circuit takes // its culture from the request that opens it, so this is what every render downstream sees. // Order of preference is the culture cookie the picker writes, then Accept-Language, then // MeterVault__Locale — an instance can be pinned to one language, and a user can still switch. // TryResolve yields the neutral fallback when it fails, so defaultCulture is usable either way. if (!Loc.TryResolve(options.Locale, out var defaultCulture) && !string.IsNullOrWhiteSpace(options.Locale)) { Log.Warning( "MeterVault__Locale is '{Locale}', which has no translations; falling back to '{Fallback}'. " + "Supported: {Supported}", options.Locale, defaultCulture, string.Join(", ", Loc.SupportedCultures)); } app.UseRequestLocalization(new RequestLocalizationOptions() .SetDefaultCulture(defaultCulture) .AddSupportedCultures([.. Loc.SupportedCultures]) .AddSupportedUICultures([.. Loc.SupportedCultures])); app.UseSwagger(); app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "MeterVault API v1")); app.MapStaticAssets(); app.MapRazorComponents() .AddInteractiveServerRenderMode(); app.MapMeterVaultApi(); app.MapCultureEndpoints(); // Liveness/readiness probe for Gatus/Compose healthchecks (SDD §9). app.MapGet("/healthz", () => Results.Ok(new { status = "ok" })); await app.RunAsync().ConfigureAwait(false); } catch (Exception ex) { Log.Fatal(ex, "MeterVault terminated unexpectedly"); throw; } finally { await Log.CloseAndFlushAsync().ConfigureAwait(false); } static async Task MigrateDatabaseAsync(WebApplication app) { var options = app.Configuration .GetSection(MeterVaultOptions.SectionName) .Get() ?? new MeterVaultOptions(); if (!options.RunMigrationsAtStartup) { return; } await using var scope = app.Services.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); await db.Database.MigrateAsync().ConfigureAwait(false); await DatabaseSeeder.SeedAsync(db).ConfigureAwait(false); Log.Information("Database migrations applied and defaults seeded"); if (options.SeedReferenceData) { var importer = scope.ServiceProvider.GetRequiredService(); var dir = Path.Combine(AppContext.BaseDirectory, "sampledata"); await importer.LoadAsync(dir).ConfigureAwait(false); Log.Information("Reference dataset ensured (SeedReferenceData=true)"); } } /// Exposed for WebApplicationFactory-based integration tests. public partial class Program;