Files
6d486f49 1d6207fbfe API Test
2026-08-17 00:45:08 -04:00

376 lines
16 KiB
Plaintext

@page "/diagnostics"
@implements IDisposable
<PageTitle>Database Configuration & Migrations - Chrono DB</PageTitle>
<div class="d-flex align-items-center justify-content-between mb-4">
<div>
<h2 class="fw-bold mb-1 text-white">Database Settings & Migrations</h2>
<p class="text-secondary mb-0">Configure database providers, update connection strings, inspect EF Core
migrations, and run maintenance tasks.</p>
</div>
<div class="d-flex gap-2">
<button class="btn btn-chrono-secondary" @onclick="RefreshAsync">
<i class="bi bi-arrow-clockwise"></i> Refresh Status
</button>
</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>
}
<div class="row g-4 mb-4">
<!-- Connection Configuration -->
<div class="col-lg-6">
<div class="chrono-card h-100">
<div class="chrono-card-header">
<h5 class="chrono-card-title">
<i class="bi bi-hdd-network-fill text-primary"></i>
Database Provider & Connection
</h5>
</div>
<div class="mb-3">
<label class="form-label text-secondary small fw-bold">ACTIVE PROVIDER</label>
<div class="d-flex gap-2">
<button
class="btn flex-fill @(selectedProvider == DatabaseProviderType.PostgreSQL ? "btn-chrono-primary" : "btn-chrono-secondary")"
@onclick="() => SelectProvider(DatabaseProviderType.PostgreSQL)">
<i class="bi bi-database me-1"></i> PostgreSQL (Cloud DB)
</button>
<button
class="btn flex-fill @(selectedProvider == DatabaseProviderType.InMemory ? "btn-chrono-primary" : "btn-chrono-secondary")"
@onclick="() => SelectProvider(DatabaseProviderType.InMemory)">
<i class="bi bi-cpu me-1"></i> In-Memory Test DB
</button>
</div>
</div>
@if (selectedProvider == DatabaseProviderType.PostgreSQL)
{
<div class="mb-3">
<label class="form-label text-secondary small fw-bold">POSTGRESQL CONNECTION STRING</label>
<textarea class="form-control form-control-chrono font-monospace" rows="3"
placeholder="Host=localhost;Port=5432;Database=chrono;Username=postgres;Password=postgres"
@bind="connectionStringInput"></textarea>
<div class="form-text text-secondary">
Standard Npgsql connection string format used by <code>Cloud.AppDbContext</code>.
</div>
</div>
<div class="mb-3">
<label class="form-label text-secondary small fw-bold">CONNECTION PRESETS</label>
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary"
@onclick='() => SetPreset("Host=localhost;Port=5432;Database=chrono;Username=postgres;Password=postgres")'>
Default Localhost (5432)
</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
@onclick='() => SetPreset("Host=host.docker.internal;Port=5432;Database=chrono;Username=postgres;Password=postgres")'>
Docker Host Internal
</button>
</div>
</div>
}
else
{
<div class="mb-3">
<label class="form-label text-secondary small fw-bold">IN-MEMORY DATABASE INSTANCE NAME</label>
<input type="text" class="form-control form-control-chrono font-monospace"
@bind="inMemoryNameInput"/>
<div class="form-text text-secondary">
In-memory EF Core provider creates an isolated database in RAM for instant testing.
</div>
</div>
}
<div class="d-flex justify-content-end gap-2 mt-4">
<button class="btn btn-chrono-primary" @onclick="SaveConfigurationAsync" disabled="@isSaving">
<i class="bi bi-check2-circle me-1"></i>
@(isSaving ? "Connecting..." : "Save & Test Connection")
</button>
</div>
</div>
</div>
<!-- Live Database Diagnostics & Table Counts -->
<div class="col-lg-6">
<div class="chrono-card h-100">
<div class="chrono-card-header">
<h5 class="chrono-card-title">
<i class="bi bi-activity text-info"></i>
Database Health & Entity Counts
</h5>
<span class="badge @(health?.IsConnected == true ? "bg-success" : "bg-danger")">
@(health?.IsConnected == true ? "ONLINE" : "OFFLINE")
</span>
</div>
@if (health != null)
{
<div class="d-flex flex-column gap-2 mb-4">
<div
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
<span class="text-secondary small">Connection Status</span>
<span class="fw-bold @(health.IsConnected ? "text-success" : "text-danger")">
@(health.IsConnected ? "Connected" : "Disconnected")
</span>
</div>
<div
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
<span class="text-secondary small">Query Latency</span>
<span class="text-white font-monospace">@(health.ResponseTimeMs) ms</span>
</div>
<div
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
<span class="text-secondary small">CardNotes Table Rows</span>
<span class="text-primary fw-bold">@health.CardNotesCount</span>
</div>
<div
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
<span class="text-secondary small">UserDecks Table Rows</span>
<span class="text-success fw-bold">@health.UserDecksCount</span>
</div>
<div
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
<span class="text-secondary small">PasskeyCredentials Table Rows</span>
<span class="text-purple fw-bold">@health.PasskeysCount</span>
</div>
</div>
@if (!health.IsConnected && !string.IsNullOrEmpty(health.ErrorMessage))
{
<div class="alert alert-danger p-2 small mb-0">
<i class="bi bi-exclamation-triangle-fill me-1"></i>
@health.ErrorMessage
</div>
}
}
</div>
</div>
</div>
<!-- EF Core Migrations Section -->
<div class="chrono-card">
<div class="chrono-card-header">
<h5 class="chrono-card-title">
<i class="bi bi-layer-forward text-warning"></i>
EF Core Migrations Status
</h5>
@if (migrationReport?.SupportsMigrations == true && migrationReport.PendingMigrations.Any())
{
<button class="btn btn-chrono-primary btn-sm" @onclick="ApplyMigrationsAsync" disabled="@isActionBusy">
<i class="bi bi-arrow-up-circle-fill me-1"></i> Apply Pending Migrations
(@migrationReport.PendingMigrations.Count)
</button>
}
</div>
@if (migrationReport == null)
{
<div class="text-center py-4 text-secondary">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
Loading migration report...
</div>
}
else if (!migrationReport.SupportsMigrations)
{
<div class="p-3 bg-dark rounded border border-secondary text-secondary small">
<i class="bi bi-info-circle me-1"></i>
Migrations are managed via relational schema (PostgreSQL). The active in-memory provider does not use
relational migration tables.
</div>
}
else
{
<div class="row g-4">
<!-- Applied Migrations -->
<div class="col-md-6">
<h6 class="text-secondary small fw-bold mb-2">
<i class="bi bi-check2-all text-success me-1"></i>
APPLIED MIGRATIONS (@migrationReport.AppliedMigrations.Count)
</h6>
@if (!migrationReport.AppliedMigrations.Any())
{
<div class="text-secondary small fst-italic p-2 bg-dark rounded border border-secondary">
No migrations have been applied to this database yet.
</div>
}
else
{
<div class="d-flex flex-column gap-2" style="max-height: 250px; overflow-y: auto;">
@foreach (var m in migrationReport.AppliedMigrations)
{
<div
class="p-2 rounded bg-dark border border-success border-opacity-50 d-flex align-items-center justify-content-between">
<span class="text-white small font-monospace">@m</span>
<span class="badge bg-success"><i class="bi bi-check"></i> Applied</span>
</div>
}
</div>
}
</div>
<!-- Pending Migrations -->
<div class="col-md-6">
<h6 class="text-secondary small fw-bold mb-2">
<i class="bi bi-clock-history text-warning me-1"></i>
PENDING MIGRATIONS (@migrationReport.PendingMigrations.Count)
</h6>
@if (!migrationReport.PendingMigrations.Any())
{
<div class="p-3 bg-dark rounded border border-success border-opacity-25 text-success small">
<i class="bi bi-shield-check me-1"></i> All migrations are applied. Schema is in sync with
AppDbContext.
</div>
}
else
{
<div class="d-flex flex-column gap-2" style="max-height: 250px; overflow-y: auto;">
@foreach (var m in migrationReport.PendingMigrations)
{
<div
class="p-2 rounded bg-dark border border-warning border-opacity-50 d-flex align-items-center justify-content-between">
<span class="text-warning small font-monospace">@m</span>
<span class="badge bg-warning text-dark"><i class="bi bi-exclamation-triangle"></i> Pending</span>
</div>
}
</div>
}
</div>
</div>
}
</div>
<!-- Database Maintenance Section -->
<div class="chrono-card">
<div class="chrono-card-header">
<h5 class="chrono-card-title text-danger">
<i class="bi bi-tools text-danger"></i>
Database Maintenance & Tools
</h5>
</div>
<div class="d-flex flex-wrap gap-3">
<button class="btn btn-chrono-secondary" @onclick="SeedDataAsync" disabled="@isActionBusy">
<i class="bi bi-box-arrow-in-down me-1"></i> Seed Realistic Sample Data
</button>
<button class="btn btn-chrono-danger" @onclick="ResetDatabaseAsync" disabled="@isActionBusy">
<i class="bi bi-trash3-fill me-1"></i> Wipe / Reset All Tables
</button>
</div>
</div>
@code {
private DatabaseHealthStatus? health;
private MigrationReport? migrationReport;
private DatabaseProviderType selectedProvider;
private string connectionStringInput = "";
private string inMemoryNameInput = "";
private bool isSaving;
private bool isActionBusy;
private string? alertMessage;
private bool alertIsSuccess;
protected override async Task OnInitializedAsync()
{
ConfigService.OnConfigurationChanged += HandleConfigChanged;
selectedProvider = ConfigService.ProviderType;
connectionStringInput = ConfigService.ConnectionString;
inMemoryNameInput = ConfigService.InMemoryDbName;
await RefreshAsync();
}
private void HandleConfigChanged()
{
selectedProvider = ConfigService.ProviderType;
connectionStringInput = ConfigService.ConnectionString;
inMemoryNameInput = ConfigService.InMemoryDbName;
_ = RefreshAsync();
}
private async Task RefreshAsync()
{
health = await DbService.CheckHealthAsync();
migrationReport = await DbService.GetMigrationStatusAsync();
}
private void SelectProvider(DatabaseProviderType provider)
{
selectedProvider = provider;
}
private void SetPreset(string conn)
{
connectionStringInput = conn;
}
private async Task SaveConfigurationAsync()
{
isSaving = true;
ConfigService.SetConnectionString(connectionStringInput);
ConfigService.SetInMemoryDbName(inMemoryNameInput);
ConfigService.SetProvider(selectedProvider);
if (selectedProvider == DatabaseProviderType.InMemory)
{
await DbService.EnsureDatabaseCreatedAsync();
}
await RefreshAsync();
isSaving = false;
alertIsSuccess = health?.IsConnected == true;
alertMessage = health?.IsConnected == true
? "Configuration updated successfully and connection verified."
: $"Configuration updated, but connection failed: {health?.ErrorMessage}";
}
private async Task ApplyMigrationsAsync()
{
isActionBusy = true;
var result = await DbService.ApplyMigrationsAsync();
isActionBusy = false;
alertIsSuccess = result.Success;
alertMessage = result.Message;
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 ResetDatabaseAsync()
{
isActionBusy = true;
var result = await DbService.ResetDatabaseAsync();
isActionBusy = false;
alertIsSuccess = result.Success;
alertMessage = result.Message;
await RefreshAsync();
}
public void Dispose()
{
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
}
}