361 lines
15 KiB
Plaintext
361 lines
15 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;
|
|
}
|
|
}
|