fix: Remove fake hardcoded data, rebuild admin layout with real Tabler components

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.
This commit is contained in:
2026-07-12 01:54:00 +09:00
parent 4b29fafcff
commit 489da25f1b
7 changed files with 218 additions and 316 deletions
@@ -10,15 +10,23 @@ public class IndexModel : PageModel
{
private readonly IWorkspaceRepository _workspaceRepository;
private readonly ICollectionRepository _collectionRepository;
private readonly IWebHostEnvironment _environment;
private readonly ILogger<IndexModel> _logger;
public int? ActiveUsersCount { get; set; }
public int? RecentRunsCount { get; set; }
public bool IsDatabaseConnected { get; set; }
public string EnvironmentName => _environment.EnvironmentName;
public IndexModel(IWorkspaceRepository workspaceRepository, ICollectionRepository collectionRepository, ILogger<IndexModel> logger)
public IndexModel(
IWorkspaceRepository workspaceRepository,
ICollectionRepository collectionRepository,
IWebHostEnvironment environment,
ILogger<IndexModel> logger)
{
_workspaceRepository = workspaceRepository;
_collectionRepository = collectionRepository;
_environment = environment;
_logger = logger;
}
@@ -31,10 +39,16 @@ public class IndexModel : PageModel
var dashboard = await _collectionRepository.GetDashboardStateAsync();
RecentRunsCount = string.IsNullOrEmpty(dashboard?.LastRunId) ? 0 : 1;
// These two queries only complete if the DB round-trip actually
// succeeded, so reaching this line is the real signal -- do not
// hardcode a static "정상"/"연결됨" badge independent of it.
IsDatabaseConnected = true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Dashboard data loading failed");
IsDatabaseConnected = false;
}
}
}