0b503c20af
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 7s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 13s
Snapshot Admin Deployment / build-and-deploy (push) Failing after 1m21s
Deploy to Production / Build & Deploy to Production (push) Successful in 1m55s
- Program.cs: PlaceholderCollectionRepository/TokenCache/KisApiClient → 실제 구현체로 변경 - 데이터베이스 초기화: EnsureTablesAsync() 호출 (시작 시 테이블 자동 생성) - kis_tokens, kis_collection_runs, kis_collection_snapshots, kis_collection_errors 테이블 - Dapper 기반 SQL 쿼리 (파라미터화, SQL 주입 방지) - 인덱스: started_at, ticker, captured_at, run_id - PlaceholderImplementations.cs 제거 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
143 lines
4.7 KiB
C#
143 lines
4.7 KiB
C#
using QuantEngine.Web.Components;
|
|
using QuantEngine.Web.Services;
|
|
using QuantEngine.Infrastructure.Data;
|
|
using QuantEngine.Infrastructure.Repositories;
|
|
using QuantEngine.Infrastructure.Services;
|
|
using QuantEngine.Core.Interfaces;
|
|
using QuantEngine.Application.Services;
|
|
using System.Text.Json;
|
|
using Microsoft.FluentUI.AspNetCore.Components;
|
|
using Serilog;
|
|
using QuantEngine.Web.Infrastructure;
|
|
using QuantEngine.Web.Endpoints;
|
|
|
|
// Serilog Configuration with Telegram Sink
|
|
Log.Logger = new LoggerConfiguration()
|
|
.MinimumLevel.Information()
|
|
.WriteTo.Console()
|
|
.WriteTo.Sink(new TelegramSink("8734507814:AAFyacLMai8GB4K-hQ_Nd3t3D01A-h1ZdV0", "-5460205872"))
|
|
.CreateLogger();
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
builder.Host.UseSerilog();
|
|
|
|
// Add services to the container.
|
|
builder.Services.AddRazorComponents()
|
|
.AddInteractiveServerComponents();
|
|
|
|
// Fluent UI Services
|
|
builder.Services.AddFluentUIComponents();
|
|
|
|
// PostgreSQL Dapper Setup
|
|
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
|
|
?? "Host=127.0.0.1;Database=giteadb;Username=gitea;Password=C8RFlZ9fdQrBA1vyLhLDS4v70I8dJfRS2ERJW4+zsS4=;Search Path=quantengine;";
|
|
builder.Services.AddSingleton<IDbConnectionFactory>(new DbConnectionFactory(connectionString));
|
|
builder.Services.AddScoped<IWorkspaceRepository, WorkspaceRepository>();
|
|
builder.Services.AddScoped<IPostgresqlHistoryStore, PostgresqlHistoryStore>();
|
|
builder.Services.AddScoped<IPostgresqlHistorySnapshotReader, PostgresqlHistorySnapshotReader>();
|
|
builder.Services.AddScoped<HistoryIngestionService>();
|
|
|
|
// Collection Pipeline Services (PostgreSQL-backed implementations)
|
|
builder.Services.AddScoped<ICollectionRepository, CollectionRepository>();
|
|
builder.Services.AddScoped<ITokenCache, PostgresTokenCache>();
|
|
builder.Services.AddScoped<IKisApiClient, KisApiClient>();
|
|
|
|
// HTTP Client & API Services
|
|
builder.Services.AddHttpClient<ApiClient>();
|
|
builder.Services.AddScoped<ApiClient>();
|
|
|
|
var app = builder.Build();
|
|
|
|
// Initialize database tables (PostgreSQL-backed repositories)
|
|
using (var scope = app.Services.CreateScope())
|
|
{
|
|
var tokenCache = scope.ServiceProvider.GetRequiredService<ITokenCache>();
|
|
var collectionRepo = scope.ServiceProvider.GetRequiredService<ICollectionRepository>();
|
|
|
|
try
|
|
{
|
|
// Ensure tables exist on startup
|
|
await tokenCache.GetCachedTokenAsync("_init_test_");
|
|
await collectionRepo.GetDashboardStateAsync();
|
|
Log.Information("Database tables initialized successfully");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Warning($"Database initialization warning: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Enable reverse proxy subpath mapping
|
|
app.UsePathBase("/quant");
|
|
|
|
// Configure the HTTP request pipeline.
|
|
if (!app.Environment.IsDevelopment())
|
|
{
|
|
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
|
app.UseHsts();
|
|
}
|
|
// Redirect status code pages only for non-API routes
|
|
app.UseStatusCodePages(async ctx =>
|
|
{
|
|
if (!ctx.HttpContext.Request.Path.StartsWithSegments("/api"))
|
|
ctx.HttpContext.Response.Redirect("/not-found");
|
|
});
|
|
|
|
app.UseHttpsRedirection();
|
|
|
|
app.UseAntiforgery();
|
|
|
|
app.MapStaticAssets();
|
|
|
|
// Collection API Endpoints (must be before MapRazorComponents)
|
|
app.MapCollectionEndpoints();
|
|
|
|
app.MapGet("/api/history/{domain}", async (string domain, int? limit, IPostgresqlHistorySnapshotReader reader) =>
|
|
{
|
|
var rows = await reader.ReadAsync(domain, limit ?? 500);
|
|
return Results.Ok(new
|
|
{
|
|
formula_id = "POSTGRESQL_HISTORY_SNAPSHOT_API_V1",
|
|
gate = "PASS",
|
|
domain,
|
|
limit = limit ?? 500,
|
|
rows
|
|
});
|
|
});
|
|
|
|
app.MapPost("/api/history/{domain}", async (string domain, JsonElement payload, HistoryIngestionService ingestor) =>
|
|
{
|
|
if (payload.ValueKind != JsonValueKind.Object)
|
|
{
|
|
return Results.BadRequest(new { gate = "FAIL", error = "payload_must_be_object" });
|
|
}
|
|
|
|
var dict = JsonSerializer.Deserialize<Dictionary<string, object?>>(payload.GetRawText())
|
|
?? new Dictionary<string, object?>();
|
|
var affected = domain switch
|
|
{
|
|
"decision_result_history" => await ingestor.AppendDecisionAsync(dict),
|
|
"factor_output_history" => await ingestor.AppendFactorOutputAsync(dict),
|
|
"market_raw_history" => await ingestor.AppendMarketRawAsync(dict),
|
|
"market_vs_engine_gap_history" => await ingestor.AppendGapAsync(dict),
|
|
_ => -1
|
|
};
|
|
if (affected < 0)
|
|
{
|
|
return Results.BadRequest(new { gate = "FAIL", error = "unsupported_domain" });
|
|
}
|
|
return Results.Ok(new
|
|
{
|
|
formula_id = "POSTGRESQL_HISTORY_APPEND_API_V1",
|
|
gate = "PASS",
|
|
domain,
|
|
affected
|
|
});
|
|
});
|
|
|
|
app.MapRazorComponents<App>()
|
|
.AddInteractiveServerRenderMode();
|
|
|
|
app.Run();
|
|
|