489da25f1b
Root cause: user asked why logout was missing. Playwright audit against production found logout works fine, but surfaced two real defects and led to a wider audit that found extensive fabricated data across the admin pages -- none of it backed by the database despite CLAUDE.md's policy that all data must come from DB records. Layout (_AdminLayout.cshtml): - Full rewrite using Tabler's actual navbar-vertical/page-wrapper/footer component structure instead of ad-hoc inline CSS. The old layout had no footer element at all, and its mobile breakpoint CSS hid the sidebar off-screen (left: -260px) with no hamburger button to bring it back -- verified via Playwright screenshot at 375px width that the entire nav menu was inaccessible on mobile, leaving only Logout reachable. Tabler's navbar-toggler + Bootstrap collapse (bundled in tabler.min.js) now restores it; verified the toggle actually opens the menu via Playwright. - Active nav-link highlighting moved from client-side JS string matching to a server-side Razor helper against Context.Request.Path. Fake/hardcoded data removed or replaced with real DB/Hangfire state: - Dashboard: deleted the "최근 시스템 이벤트" table (3 rows hardcoded from DateTime.Now with fake descriptions like "시스템 초기화" / "데이터베이스 백업" -- no backing table exists). Removed hardcoded "정상"/"연결됨" status badges and "버전: v0.1.0"/"업타임: 정상"; replaced with a real IsDatabaseConnected flag (true only if the page's actual DB queries succeeded) and the real IWebHostEnvironment.EnvironmentName. - Monitoring: removed hardcoded "API 서버: 운영 중" (no real signal backs it) and wired "데이터베이스: 연결 정상/끊김" to the same real success/failure state as the page's own DB calls. - Operations: this page was entirely fabricated -- ScheduledJobs, RecentExecutions, IsJobProcessorRunning, PendingJobsCount, and StatusMessage were all static values with zero connection to Hangfire, despite Hangfire actually running in production (confirmed via journalctl: ServerWatchdog, RecurringJobScheduler dispatchers active) with 4 real recurring jobs registered in SchedulerService (daily-collection, hourly-price-update, weekly-report, monthly-optimization). Rewrote to query JobStorage.Current.GetConnection().GetRecurringJobs() and GetMonitoringApi() directly: real scheduled jobs, real succeeded/ failed executions, real server count, real enqueued count. Verified locally (SSH-tunneled to prod DB) that this now returns the actual 4 registered jobs with correct next-run times and one real RunDailyCollectionAsync execution. Also fixed the page-title duplication on Monitoring/Operations (ViewData["Title"] included "- QuantEngine" AND the layout appended it again -> "모니터링 - QuantEngine - QuantEngine" in the browser tab). Separately discovered (not fixed in this commit, flagging for follow-up): Hangfire's SchedulerService.InitializeSchedules() fails every startup with "Cannot resolve scoped service 'SchedulerService' from root provider" -- the 4 recurring jobs above still show up because they persist from an earlier successful registration, but re-registration is silently broken on every current boot. Verified end-to-end with Playwright against a local instance (SSH tunnel to production Postgres): login, all 5 admin pages render without errors, mobile hamburger opens the sidebar, and Operations shows genuine Hangfire data.
184 lines
8.4 KiB
Plaintext
184 lines
8.4 KiB
Plaintext
@page
|
|
@model QuantEngine.Web.Pages.Admin.Monitoring.IndexModel
|
|
@{
|
|
ViewData["Title"] = "모니터링";
|
|
}
|
|
|
|
<div class="page-header d-print-none">
|
|
<div class="row align-items-center">
|
|
<div class="col">
|
|
<h2 class="page-title">실시간 모니터링</h2>
|
|
</div>
|
|
<div class="col-auto">
|
|
<a href="javascript:location.reload()" class="btn btn-secondary">새로고침</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="page-body">
|
|
<div class="row row-deck row-cards">
|
|
<!-- 진행 중인 작업 -->
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">진행 중인 작업</h3>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-vcenter card-table">
|
|
<thead>
|
|
<tr>
|
|
<th>작업 ID</th>
|
|
<th>상태</th>
|
|
<th>시작 시간</th>
|
|
<th>진행률</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (Model.OngoingRuns?.Any() == true)
|
|
{
|
|
@foreach (var run in Model.OngoingRuns)
|
|
{
|
|
<tr>
|
|
<td><code>@run.RunId</code></td>
|
|
<td><span class="badge bg-warning">진행 중</span></td>
|
|
<td>@(DateTime.TryParse(run.StartedAt?.ToString(), out var dt) ? dt.ToString("yyyy-MM-dd HH:mm:ss") : (run.StartedAt?.ToString() ?? "-"))</td>
|
|
<td>
|
|
@if (run.TotalSnapshots > 0)
|
|
{
|
|
var progressPercent = (int)((run.TotalSnapshots * 100) / (run.TotalSnapshots + (run.TotalErrors ?? 0) + 1));
|
|
<div class="progress progress-sm">
|
|
<div class="progress-bar bg-info" style="width: @progressPercent%"></div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">-</span>
|
|
}
|
|
</td>
|
|
</tr>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<tr>
|
|
<td colspan="4" class="text-center text-muted">진행 중인 작업이 없습니다</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 최근 실행 통계 -->
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">최근 24시간 통계</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row row-sm">
|
|
<div class="col-auto">
|
|
<div class="text-muted">전체 실행</div>
|
|
<div class="h2">@Model.TotalRuns24h</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<div class="text-muted">성공</div>
|
|
<div class="h2 text-success">@Model.SuccessRuns24h</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<div class="text-muted">실패</div>
|
|
<div class="h2 text-danger">@Model.FailedRuns24h</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<div class="text-muted">성공률</div>
|
|
<div class="h2">@(Model.TotalRuns24h > 0 ? ((Model.SuccessRuns24h * 100) / Model.TotalRuns24h).ToString("F0") : 0)%</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 시스템 상태 -->
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">시스템 상태</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="list-group list-group-flush">
|
|
<div class="list-group-item">
|
|
<div class="row align-items-center">
|
|
<div class="col">
|
|
<strong>데이터베이스</strong>
|
|
</div>
|
|
<div class="col-auto">
|
|
@if (Model.IsDatabaseConnected)
|
|
{
|
|
<span class="badge bg-success">연결 정상</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="badge bg-danger">연결 끊김</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="list-group-item">
|
|
<div class="row align-items-center">
|
|
<div class="col">
|
|
<strong>마지막 갱신</strong>
|
|
</div>
|
|
<div class="col-auto">
|
|
<span class="text-muted">@(Model.LastRefreshTime?.ToString("HH:mm:ss") ?? "-")</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 최근 에러 -->
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">최근 에러 (상위 10개)</h3>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-vcenter card-table">
|
|
<thead>
|
|
<tr>
|
|
<th>실행 ID</th>
|
|
<th>에러 종류</th>
|
|
<th>메시지</th>
|
|
<th>발생 시간</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (Model.RecentErrors?.Any() == true)
|
|
{
|
|
@foreach (var error in Model.RecentErrors)
|
|
{
|
|
<tr>
|
|
<td><small><code>@error.RunId</code></small></td>
|
|
<td><span class="badge bg-danger">@error.ErrorKind</span></td>
|
|
<td class="text-muted">@(error.ErrorMessage?.Length > 50 ? error.ErrorMessage.Substring(0, 50) + "..." : error.ErrorMessage ?? "-")</td>
|
|
<td><small>@(DateTime.TryParse(error.CreatedAt?.ToString(), out var dt) ? dt.ToString("HH:mm:ss") : (error.CreatedAt?.ToString() ?? "-"))</small></td>
|
|
</tr>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<tr>
|
|
<td colspan="4" class="text-center text-muted">최근 에러가 없습니다</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|