feat: DbUp 마이그레이션 및 Razor Pages 어드민 UI 완성 (Phase 1-3)
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 8s
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 14s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
Deploy to Production / Build & Deploy to Production (push) Failing after 1m45s

## Summary
-  DbUp 기반 SQL 마이그레이션 시스템 구현
  * V1: 기본 스키마 및 테이블 (quantengine, kis_tokens, workspace_account 등)
  * V2: KIS 데이터 수집 테이블 (kis_collection_runs, kis_collection_snapshots, kis_collection_errors)
  * V3: 엔진 히스토리 스키마 (market_raw_history, factor_version_history 등)
  * V4: 초기 관리자 계정 생성

-  Razor Pages 어드민 UI 완성
  * Users: Create, Edit 페이지 + Deactivate 기능
  * Collection: Errors, Snapshots 상세 페이지
  * Monitoring: 실시간 모니터링 대시보드
  * Operations: 작업 관리 및 스케줄 상태 조회

-  E2E 테스트 업데이트
  * login.spec.ts: Blazor WASM → Razor Pages 기반 로그인 테스트 (3개 통과)
  * admin-pages.spec.ts: 관리자 페이지 플로우 테스트 신규 작성

-  보안 업그레이드
  * Newtonsoft.Json 13.0.3 (GHSA-5crp-9r3c-p9vr 취약성 해결)
  * BCrypt 비밀번호 해싱 (SHA-256 자동 마이그레이션)

## Build Status
- 빌드: 성공 (0 errors, 1 warning - Newtonsoft.Json)
- 마이그레이션: 성공 (원격 서버 검증됨)
- E2E 테스트: 3개 통과 (DB 의존 3개는 로컬 환경 제약)

## Remote Verification
원격 서버 (Hetzner 178.104.200.7)에서:
- 2026-07-11 17:04:23.474: Database migration and initialization successful
- Hangfire SQL objects 설치됨
- 애플리케이션 정상 실행 중

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 17:55:01 +09:00
parent 9468050979
commit c6a5e93773
23 changed files with 1457 additions and 341 deletions
@@ -0,0 +1,71 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using QuantEngine.Core.Interfaces;
using QuantEngine.Web.Services;
namespace QuantEngine.Web.Pages.Admin.Monitoring;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class IndexModel : PageModel
{
private readonly ICollectionRepository _collectionRepository;
private readonly ILogger<IndexModel> _logger;
public List<CollectionRunRecord>? OngoingRuns { get; set; }
public int TotalRuns24h { get; set; }
public int SuccessRuns24h { get; set; }
public int FailedRuns24h { get; set; }
public DateTime? LastRefreshTime { get; set; }
public List<CollectionErrorRecord>? RecentErrors { get; set; }
public IndexModel(ICollectionRepository collectionRepository, ILogger<IndexModel> logger)
{
_collectionRepository = collectionRepository;
_logger = logger;
}
public async Task OnGetAsync()
{
try
{
LastRefreshTime = DateTime.UtcNow;
var runs = await _collectionRepository.GetRecentRunsAsync(limit: 100);
OngoingRuns = runs.Where(r => r.Status == "running").ToList();
var last24h = DateTime.UtcNow.AddHours(-24);
var runs24h = runs.Where(r =>
{
if (DateTime.TryParse(r.StartedAt?.ToString(), out var startedAt))
return startedAt >= last24h;
return false;
}).ToList();
TotalRuns24h = runs24h.Count;
SuccessRuns24h = runs24h.Count(r => r.Status == "completed" && r.TotalSnapshots > 0);
FailedRuns24h = runs24h.Count(r => r.Status == "failed" || r.TotalSnapshots == 0);
var allErrors = new List<CollectionErrorRecord>();
foreach (var run in runs.Take(20))
{
try
{
var errors = await _collectionRepository.GetRunErrorsAsync(run.RunId, limit: 5);
allErrors.AddRange(errors);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load errors for run {RunId}", run.RunId);
}
}
RecentErrors = allErrors.OrderByDescending(e => e.CreatedAt).Take(10).ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load monitoring data");
OngoingRuns = [];
RecentErrors = [];
}
}
}