refactor(central-ui): split Notification Report out of the Outbox page

This commit is contained in:
Joseph Doherty
2026-05-19 06:03:15 -04:00
parent 016f5d48a6
commit 34e464edab
3 changed files with 21 additions and 146 deletions

View File

@@ -1,4 +1,4 @@
@page "/monitoring/notification-outbox"
@page "/notifications/report"
@attribute [Authorize(Policy = ScadaLink.Security.AuthorizationPolicies.RequireDeployment)]
@using ScadaLink.Commons.Entities.Sites
@using ScadaLink.Commons.Interfaces.Repositories
@@ -7,70 +7,19 @@
@inject CommunicationService CommunicationService
@inject ISiteRepository SiteRepository
@inject IDialogService Dialog
@inject ILogger<NotificationOutbox> Logger
@inject ILogger<NotificationReport> Logger
<div class="container-fluid mt-3">
<ToastNotification @ref="_toast" />
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">Notification Outbox</h4>
<h4 class="mb-0">Notification Report</h4>
<button class="btn btn-outline-secondary btn-sm" @onclick="RefreshAll" disabled="@_loading">
@if (_loading) { <span class="spinner-border spinner-border-sm me-1" role="status"></span> }
Refresh
</button>
</div>
@* ── KPI tiles ── *@
@if (_kpiError != null)
{
<div class="alert alert-warning py-2">KPIs unavailable: @_kpiError</div>
}
else
{
<div class="row g-3 mb-3">
<div class="col-lg col-md-4 col-6">
<div class="card h-100">
<div class="card-body text-center py-3">
<h3 class="mb-0">@_kpi.QueueDepth</h3>
<small class="text-muted">Queue Depth</small>
</div>
</div>
</div>
<div class="col-lg col-md-4 col-6">
<div class="card h-100 @(_kpi.StuckCount > 0 ? "border-warning" : "")">
<div class="card-body text-center py-3">
<h3 class="mb-0 @(_kpi.StuckCount > 0 ? "text-warning" : "")">@_kpi.StuckCount</h3>
<small class="text-muted">Stuck</small>
</div>
</div>
</div>
<div class="col-lg col-md-4 col-6">
<div class="card h-100 @(_kpi.ParkedCount > 0 ? "border-danger" : "")">
<div class="card-body text-center py-3">
<h3 class="mb-0 @(_kpi.ParkedCount > 0 ? "text-danger" : "")">@_kpi.ParkedCount</h3>
<small class="text-muted">Parked</small>
</div>
</div>
</div>
<div class="col-lg col-md-4 col-6">
<div class="card h-100">
<div class="card-body text-center py-3">
<h3 class="mb-0 text-success">@_kpi.DeliveredLastInterval</h3>
<small class="text-muted">Delivered (last interval)</small>
</div>
</div>
</div>
<div class="col-lg col-md-4 col-6">
<div class="card h-100">
<div class="card-body text-center py-3">
<h3 class="mb-0">@FormatAge(_kpi.OldestPendingAge)</h3>
<small class="text-muted">Oldest Pending Age</small>
</div>
</div>
</div>
</div>
}
@* ── Filters ── *@
<div class="card mb-3">
<div class="card-body py-2">
@@ -259,11 +208,6 @@
private ToastNotification _toast = default!;
private List<Site> _sites = new();
// KPIs
private NotificationKpiResponse _kpi =
new(string.Empty, true, null, 0, 0, 0, 0, null);
private string? _kpiError;
// List
private List<NotificationSummary>? _notifications;
private int _totalCount;
@@ -291,7 +235,7 @@
catch (Exception ex)
{
// Non-fatal — source-site filter just falls back to the raw site IDs.
Logger.LogWarning(ex, "Failed to load sites for the outbox source-site filter.");
Logger.LogWarning(ex, "Failed to load sites for the report source-site filter.");
}
await RefreshAll();
@@ -299,31 +243,7 @@
private async Task RefreshAll()
{
// Race-free despite both tasks mutating component fields: Blazor Server runs
// every continuation on the circuit's single-threaded synchronization context.
await Task.WhenAll(LoadKpis(), FetchPage());
}
private async Task LoadKpis()
{
try
{
var response = await CommunicationService.GetNotificationKpisAsync(
new NotificationKpiRequest(Guid.NewGuid().ToString("N")));
if (response.Success)
{
_kpi = response;
_kpiError = null;
}
else
{
_kpiError = response.ErrorMessage ?? "KPI query failed.";
}
}
catch (Exception ex)
{
_kpiError = $"KPI query failed: {ex.Message}";
}
await FetchPage();
}
private async Task Search()
@@ -463,16 +383,6 @@
private static string ShortId(string id) => id[..Math.Min(12, id.Length)];
private static string FormatAge(TimeSpan? age)
{
if (age == null) return "—";
var t = age.Value;
if (t.TotalSeconds < 60) return $"{(int)t.TotalSeconds}s";
if (t.TotalMinutes < 60) return $"{(int)t.TotalMinutes}m";
if (t.TotalHours < 24) return $"{(int)t.TotalHours}h";
return $"{(int)t.TotalDays}d";
}
private static string StatusBadgeClass(string status) => status switch
{
"Delivered" => "bg-success",