using FastEndpoints; using Hangfire; using Hangfire.PostgreSql; using KArtSell.BuildingBlocks.Capabilities; using KArtSell.BuildingBlocks.Data; using KArtSell.BuildingBlocks.Reliability; using KArtSell.BuildingBlocks.Time; using KArtSell.Host.Security; using KArtSell.Modules.ModelOperations; using KArtSell.Modules.ModelOperations.Scheduling; using KArtSell.Modules.SignalEngine; using Microsoft.AspNetCore.Authentication; using Npgsql; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Serilog; var builder = WebApplication.CreateBuilder(args); builder.Host.UseSerilog((context, services, logger) => logger .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext() .WriteTo.Console()); var connectionString = builder.Configuration.GetConnectionString("Postgres") ?? throw new InvalidOperationException("ConnectionStrings:Postgres is required."); var modelOperationsDispatcherEnabled = builder.Configuration.GetValue("ModelOperations:DispatcherEnabled"); var modelOperationsDispatcherCron = builder.Configuration["ModelOperations:DispatcherCron"] ?? "*/15 * * * *"; builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(CapabilityOptions.SectionName)) .Validate(x => !x.AutomaticOrder, "AutomaticOrder must remain OFF in this package.") .Validate(x => !x.KisOrderAdapter, "KisOrderAdapter must remain OFF until a separately approved release.") .Validate(x => !modelOperationsDispatcherEnabled || x.ShadowEvaluation, "ModelOperations dispatcher requires ShadowEvaluation capability and remains evidence-only.") .ValidateOnStart(); var dataSource = new NpgsqlDataSourceBuilder(connectionString).Build(); builder.Services.AddSingleton(dataSource); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddProblemDetails(); builder.Services.AddFastEndpoints(); const string authenticationScheme = "KArtSell"; var authenticationMode = builder.Configuration["Authentication:Mode"] ?? "FailClosed"; var authenticationBuilder = builder.Services .AddAuthentication(options => { options.DefaultAuthenticateScheme = authenticationScheme; options.DefaultChallengeScheme = authenticationScheme; }); if (builder.Environment.IsDevelopment() && authenticationMode.Equals("DevelopmentHeader", StringComparison.OrdinalIgnoreCase)) { authenticationBuilder.AddScheme( authenticationScheme, _ => { }); } else { authenticationBuilder.AddScheme( authenticationScheme, _ => { }); } builder.Services.AddAuthorization(); builder.Services.AddSignalEngineModule(); builder.Services.AddModelOperationsModule(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddHangfire(config => config.UsePostgreSqlStorage(options => options.UseNpgsqlConnection(connectionString))); builder.Services.AddHangfireServer(options => { options.Queues = [ "q-control", "q-market-data", "q-fundamentals", "q-feature-risk", "q-recommendation", "q-evaluation", "q-reconciliation", "q-research", "q-backfill" ]; options.WorkerCount = Math.Max(2, Environment.ProcessorCount / 2); }); builder.Services.AddOpenTelemetry() .ConfigureResource(resource => resource.AddService("KArtSell.Host")) .WithTracing(tracing => tracing .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddOtlpExporter()) .WithMetrics(metrics => metrics .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddRuntimeInstrumentation() .AddOtlpExporter()); var app = builder.Build(); app.UseExceptionHandler(); app.UseStatusCodePages(); app.UseSerilogRequestLogging(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseAuthentication(); app.UseAuthorization(); app.UseFastEndpoints(config => config.Endpoints.RoutePrefix = "api"); app.Services.RegisterModelOperationsSchedules(modelOperationsDispatcherEnabled, modelOperationsDispatcherCron); app.MapGet("/health/live", () => Results.Ok(new { status = "ok", automaticOrderCapability = "OFF", algorithmStatus = "RESEARCH_CANDIDATE_NOT_PRODUCTION" })); app.MapGet("/health/ready", async (NpgsqlDataSource source, CancellationToken ct) => { await using var connection = await source.OpenConnectionAsync(ct); await using var command = connection.CreateCommand(); command.CommandText = "select 1"; await command.ExecuteScalarAsync(ct); return Results.Ok(new { status = "ready", database = "reachable" }); }); app.Run(); public partial class Program;