346 lines
14 KiB
Plaintext
346 lines
14 KiB
Plaintext
@page "/"
|
|
@implements IDisposable
|
|
|
|
<PageTitle>Chrono DB Hub - Dashboard</PageTitle>
|
|
|
|
<div class="d-flex align-items-center justify-content-between mb-4">
|
|
<div>
|
|
<h2 class="fw-bold mb-1 text-white">Database Test Dashboard</h2>
|
|
<p class="text-secondary mb-0">Monitor, manage, and validate all database features referenced in Chrono
|
|
Cloud</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-chrono-secondary" @onclick="RefreshAsync" disabled="@isLoading">
|
|
<i class="bi bi-arrow-clockwise @(isLoading ? "spin" : "")"></i> Refresh Status
|
|
</button>
|
|
<a href="test-suite" class="btn btn-chrono-primary">
|
|
<i class="bi bi-play-circle-fill"></i> Run All Tests
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrEmpty(alertMessage))
|
|
{
|
|
<div class="alert @(alertIsSuccess ? "alert-success" : "alert-danger") alert-dismissible fade show" role="alert">
|
|
<i class="bi @(alertIsSuccess ? "bi-check-circle-fill" : "bi-exclamation-triangle-fill") me-2"></i>
|
|
@alertMessage
|
|
<button type="button" class="btn-close" @onclick="() => alertMessage = null"></button>
|
|
</div>
|
|
}
|
|
|
|
<!-- Database Health & Connection Banner -->
|
|
@if (health != null)
|
|
{
|
|
@if (!health.IsConnected)
|
|
{
|
|
<div class="chrono-card border-danger mb-4" style="background-color: rgba(248, 81, 73, 0.08);">
|
|
<div class="d-flex align-items-start justify-content-between">
|
|
<div class="d-flex gap-3">
|
|
<i class="bi bi-exclamation-octagon-fill text-danger fs-2"></i>
|
|
<div>
|
|
<h5 class="text-danger fw-bold mb-1">PostgreSQL Connection Offline</h5>
|
|
<p class="text-secondary small mb-2">
|
|
@(health.ErrorMessage ?? "Could not connect to the database server. Ensure PostgreSQL is running on the configured host/port.")
|
|
</p>
|
|
<div class="d-flex gap-2 mt-2">
|
|
<button class="btn btn-sm btn-chrono-primary" @onclick="SwitchToInMemoryAsync">
|
|
<i class="bi bi-lightning-charge-fill"></i> Switch to In-Memory DB (Instant Testing)
|
|
</button>
|
|
<a href="diagnostics" class="btn btn-sm btn-chrono-secondary">
|
|
<i class="bi bi-gear-fill"></i> Configure Connection
|
|
</a>
|
|
<button class="btn btn-sm btn-outline-secondary" @onclick="RefreshAsync">
|
|
<i class="bi bi-arrow-repeat"></i> Retry Connection
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
else if (health.ProviderType == DatabaseProviderType.InMemory)
|
|
{
|
|
<div class="chrono-card border-info mb-4" style="background-color: rgba(57, 197, 207, 0.08);">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div class="d-flex align-items-center gap-3">
|
|
<i class="bi bi-cpu-fill text-info fs-2"></i>
|
|
<div>
|
|
<h6 class="text-info fw-bold mb-0">In-Memory Database Active (@health.ConnectionString)</h6>
|
|
<span class="text-secondary small">Running zero-config in-memory tests. Full CRUD and test suites are active.</span>
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-chrono-secondary" @onclick="SwitchToPostgresAsync">
|
|
<i class="bi bi-database"></i> Switch to PostgreSQL
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
<!-- Metrics Row -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md">
|
|
<a href="card-notes" class="text-decoration-none">
|
|
<div class="metric-card">
|
|
<div class="metric-icon-box metric-icon-blue">
|
|
<i class="bi bi-card-text"></i>
|
|
</div>
|
|
<div>
|
|
<div
|
|
class="metric-value">@(health?.IsConnected == true ? health.CardNotesCount.ToString() : "-")</div>
|
|
<div class="metric-label">Card Notes</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="col-md">
|
|
<a href="users" class="text-decoration-none">
|
|
<div class="metric-card">
|
|
<div class="metric-icon-box metric-icon-yellow">
|
|
<i class="bi bi-people-fill"></i>
|
|
</div>
|
|
<div>
|
|
<div class="metric-value">@(health?.IsConnected == true ? health.UsersCount.ToString() : "-")</div>
|
|
<div class="metric-label">Users</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="col-md">
|
|
<a href="user-decks" class="text-decoration-none">
|
|
<div class="metric-card">
|
|
<div class="metric-icon-box metric-icon-green">
|
|
<i class="bi bi-collection-play"></i>
|
|
</div>
|
|
<div>
|
|
<div
|
|
class="metric-value">@(health?.IsConnected == true ? health.UserDecksCount.ToString() : "-")</div>
|
|
<div class="metric-label">User Decks</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="col-md">
|
|
<a href="passkeys" class="text-decoration-none">
|
|
<div class="metric-card">
|
|
<div class="metric-icon-box metric-icon-purple">
|
|
<i class="bi bi-key-fill"></i>
|
|
</div>
|
|
<div>
|
|
<div
|
|
class="metric-value">@(health?.IsConnected == true ? health.PasskeysCount.ToString() : "-")</div>
|
|
<div class="metric-label">Passkeys</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="col-md">
|
|
<a href="diagnostics" class="text-decoration-none">
|
|
<div class="metric-card">
|
|
<div class="metric-icon-box metric-icon-yellow">
|
|
<i class="bi bi-arrow-left-right"></i>
|
|
</div>
|
|
<div>
|
|
<div class="metric-value">
|
|
@(health?.ProviderType == DatabaseProviderType.PostgreSQL
|
|
? $"{health.AppliedMigrationsCount} / {health.AppliedMigrationsCount + health.PendingMigrationsCount}"
|
|
: "N/A")
|
|
</div>
|
|
<div class="metric-label">Migrations</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions & Features -->
|
|
<div class="row g-4">
|
|
<div class="col-lg-7">
|
|
<div class="chrono-card h-100">
|
|
<div class="chrono-card-header">
|
|
<h5 class="chrono-card-title">
|
|
<i class="bi bi-stars text-primary"></i>
|
|
Cloud Database Features Under Test
|
|
</h5>
|
|
<span class="badge bg-secondary">Cloud.AppDbContext</span>
|
|
</div>
|
|
|
|
<div class="d-flex flex-column gap-3">
|
|
<div class="p-3 rounded bg-dark border border-secondary border-opacity-25">
|
|
<div class="d-flex align-items-center justify-content-between mb-1">
|
|
<span class="fw-bold text-white"><i class="bi bi-card-text text-primary me-2"></i> Card Notes Entity (`CardNote`)</span>
|
|
<a href="card-notes" class="btn btn-sm btn-chrono-secondary">Manage Notes</a>
|
|
</div>
|
|
<p class="text-secondary small mb-0">
|
|
Tracks developer and user strategic card notes with primary key indexing on
|
|
<code>CardName</code>. Supports live search, adding, updating, and deletion.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="p-3 rounded bg-dark border border-secondary border-opacity-25">
|
|
<div class="d-flex align-items-center justify-content-between mb-1">
|
|
<span class="fw-bold text-white"><i class="bi bi-collection-play text-success me-2"></i> User Decks & JSONB (`UserDeck`)</span>
|
|
<a href="user-decks" class="btn btn-sm btn-chrono-secondary">Manage Decks</a>
|
|
</div>
|
|
<p class="text-secondary small mb-0">
|
|
Stores full player decks with native PostgreSQL <code>jsonb</code> mapping for
|
|
<code>Cards</code> list (with counts) and <code>Divers</code> list.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="p-3 rounded bg-dark border border-secondary border-opacity-25">
|
|
<div class="d-flex align-items-center justify-content-between mb-1">
|
|
<span class="fw-bold text-white"><i class="bi bi-key-fill text-purple me-2"></i> Passkey Credentials (`PasskeyCredential`)</span>
|
|
<a href="passkeys" class="btn btn-sm btn-chrono-secondary">Manage Passkeys</a>
|
|
</div>
|
|
<p class="text-secondary small mb-0">
|
|
Stores WebAuthn / FIDO2 binary credentials (raw <code>byte[]</code> for CredentialId, PublicKey,
|
|
UserHandle) with counter verification.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-5">
|
|
<div class="chrono-card h-100">
|
|
<div class="chrono-card-header">
|
|
<h5 class="chrono-card-title">
|
|
<i class="bi bi-wrench-adjustable text-warning"></i>
|
|
Quick Action Hub
|
|
</h5>
|
|
</div>
|
|
|
|
<div class="d-flex flex-column gap-2">
|
|
<button class="btn btn-chrono-primary py-2 text-start d-flex align-items-center justify-content-between"
|
|
@onclick="SeedDataAsync" disabled="@isActionBusy">
|
|
<span><i class="bi bi-box-arrow-in-down me-2"></i> Seed Realistic Sample Data</span>
|
|
<i class="bi bi-arrow-right"></i>
|
|
</button>
|
|
|
|
<button
|
|
class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between"
|
|
@onclick="ApplyMigrationsAsync" disabled="@isActionBusy">
|
|
<span><i class="bi bi-database-check me-2"></i> Apply Pending Migrations</span>
|
|
<i class="bi bi-arrow-right"></i>
|
|
</button>
|
|
|
|
@if (ConfigService.ProviderType == DatabaseProviderType.PostgreSQL)
|
|
{
|
|
<button
|
|
class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between"
|
|
@onclick="SwitchToInMemoryAsync">
|
|
<span><i class="bi bi-cpu me-2"></i> Switch to In-Memory DB</span>
|
|
<span class="badge bg-purple">Instant</span>
|
|
</button>
|
|
}
|
|
else
|
|
{
|
|
<button
|
|
class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between"
|
|
@onclick="SwitchToPostgresAsync">
|
|
<span><i class="bi bi-database me-2"></i> Switch to PostgreSQL</span>
|
|
<span class="badge bg-primary">Live</span>
|
|
</button>
|
|
}
|
|
|
|
<a href="test-suite"
|
|
class="btn btn-chrono-outline-primary py-2 text-start d-flex align-items-center justify-content-between">
|
|
<span><i class="bi bi-clipboard2-check me-2"></i> Open Test Suite Runner</span>
|
|
<i class="bi bi-arrow-right"></i>
|
|
</a>
|
|
|
|
<button
|
|
class="btn btn-chrono-danger py-2 text-start d-flex align-items-center justify-content-between mt-2"
|
|
@onclick="ResetDatabaseAsync" disabled="@isActionBusy">
|
|
<span><i class="bi bi-trash3-fill me-2"></i> Wipe & Reset Database</span>
|
|
<i class="bi bi-exclamation-triangle"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
private DatabaseHealthStatus? health;
|
|
private bool isLoading;
|
|
private bool isActionBusy;
|
|
private string? alertMessage;
|
|
private bool alertIsSuccess;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
ConfigService.OnConfigurationChanged += HandleConfigChanged;
|
|
await RefreshAsync();
|
|
}
|
|
|
|
private async Task RefreshAsync()
|
|
{
|
|
isLoading = true;
|
|
health = await DbService.CheckHealthAsync();
|
|
isLoading = false;
|
|
}
|
|
|
|
private void HandleConfigChanged()
|
|
{
|
|
_ = RefreshAsync();
|
|
}
|
|
|
|
private async Task SwitchToInMemoryAsync()
|
|
{
|
|
ConfigService.SetProvider(DatabaseProviderType.InMemory);
|
|
var result = await DbService.EnsureDatabaseCreatedAsync();
|
|
alertIsSuccess = result.Success;
|
|
alertMessage = "Switched to In-Memory Test Database. " + result.Message;
|
|
await RefreshAsync();
|
|
}
|
|
|
|
private async Task SwitchToPostgresAsync()
|
|
{
|
|
ConfigService.SetProvider(DatabaseProviderType.PostgreSQL);
|
|
alertIsSuccess = true;
|
|
alertMessage = "Switched provider to PostgreSQL.";
|
|
await RefreshAsync();
|
|
}
|
|
|
|
private async Task SeedDataAsync()
|
|
{
|
|
isActionBusy = true;
|
|
var result = await DbService.SeedSampleDataAsync();
|
|
isActionBusy = false;
|
|
alertIsSuccess = result.Success;
|
|
alertMessage = result.Message;
|
|
await RefreshAsync();
|
|
}
|
|
|
|
private async Task ApplyMigrationsAsync()
|
|
{
|
|
isActionBusy = true;
|
|
var result = await DbService.ApplyMigrationsAsync();
|
|
isActionBusy = false;
|
|
alertIsSuccess = result.Success;
|
|
alertMessage = result.Message;
|
|
await RefreshAsync();
|
|
}
|
|
|
|
private async Task ResetDatabaseAsync()
|
|
{
|
|
isActionBusy = true;
|
|
var result = await DbService.ResetDatabaseAsync();
|
|
isActionBusy = false;
|
|
alertIsSuccess = result.Success;
|
|
alertMessage = result.Message;
|
|
await RefreshAsync();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
|
}
|
|
|
|
}
|