@page "/notifications/report" @attribute [Authorize(Policy = ScadaLink.Security.AuthorizationPolicies.RequireDeployment)] @using ScadaLink.Commons.Entities.Sites @using ScadaLink.Commons.Interfaces.Repositories @using ScadaLink.Commons.Messages.Notification @using ScadaLink.Communication @inject CommunicationService CommunicationService @inject ISiteRepository SiteRepository @inject IDialogService Dialog @inject ILogger Logger

Notification Report

@* ── Filters ── *@
@if (_listError != null) {
@_listError
} @* ── Notification list ── *@ @if (_notifications == null) { @if (_loading) {
Loading…
} } else if (_notifications.Count == 0) {
No notifications
No notifications match the current filters.
} else {
@foreach (var n in _notifications) { }
ID Type List Subject Status Retries Source site Created Delivered Actions
@ShortId(n.NotificationId) @n.Type @n.ListName @n.Subject @if (!string.IsNullOrEmpty(n.LastError)) {
@n.LastError
}
@n.Status @if (n.IsStuck) { Stuck } @n.RetryCount @SiteName(n.SourceSiteId) @* Bundle D (#23 M7-T10) drill-in: NotificationId is the audit CorrelationId, so the link deep-links into the central Audit Log pre-filtered to this notification's lifecycle events. *@ View audit history @if (n.Status == "Parked") { }
@if (_totalCount > _pageSize) {
Page @_pageNumber of @((_totalCount + _pageSize - 1) / _pageSize) · @_totalCount total
} }
@code { private const int _pageSize = 50; private ToastNotification _toast = default!; private List _sites = new(); // List private List? _notifications; private int _totalCount; private int _pageNumber = 1; private bool _loading; private string? _listError; private bool _actionInProgress; // Filters private string _statusFilter = string.Empty; private string _typeFilter = string.Empty; private string _siteFilter = string.Empty; private string _listFilter = string.Empty; private string _subjectFilter = string.Empty; private bool _stuckOnly; private DateTime? _fromFilter; private DateTime? _toFilter; protected override async Task OnInitializedAsync() { try { _sites = (await SiteRepository.GetAllSitesAsync()).ToList(); } 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 report source-site filter."); } await RefreshAll(); } private async Task RefreshAll() { await FetchPage(); } private async Task Search() { _pageNumber = 1; await FetchPage(); } private async Task PrevPage() { _pageNumber--; await FetchPage(); } private async Task NextPage() { _pageNumber++; await FetchPage(); } private async Task FetchPage() { _loading = true; _listError = null; try { var request = new NotificationOutboxQueryRequest( CorrelationId: Guid.NewGuid().ToString("N"), StatusFilter: NullIfEmpty(_statusFilter), TypeFilter: NullIfEmpty(_typeFilter), SourceSiteFilter: NullIfEmpty(_siteFilter), ListNameFilter: NullIfEmpty(_listFilter), StuckOnly: _stuckOnly, SubjectKeyword: NullIfEmpty(_subjectFilter), From: ToUtc(_fromFilter), To: ToUtc(_toFilter), PageNumber: _pageNumber, PageSize: _pageSize); var response = await CommunicationService.QueryNotificationOutboxAsync(request); if (response.Success) { _notifications = response.Notifications.ToList(); _totalCount = response.TotalCount; } else { _listError = response.ErrorMessage ?? "Query failed."; } } catch (Exception ex) { _listError = $"Query failed: {ex.Message}"; } _loading = false; } private async Task RetryNotification(NotificationSummary n) { var confirmed = await Dialog.ConfirmAsync( "Retry notification", $"Re-queue notification {ShortId(n.NotificationId)} (\"{n.Subject}\") for delivery?"); if (!confirmed) return; _actionInProgress = true; try { var response = await CommunicationService.RetryNotificationAsync( new RetryNotificationRequest(Guid.NewGuid().ToString("N"), n.NotificationId)); if (response.Success) { _toast.ShowSuccess($"Notification {ShortId(n.NotificationId)} re-queued for delivery."); await RefreshAll(); } else { _toast.ShowError(response.ErrorMessage ?? "Retry failed."); } } catch (Exception ex) { _toast.ShowError($"Retry failed: {ex.Message}"); } _actionInProgress = false; } private async Task DiscardNotification(NotificationSummary n) { var confirmed = await Dialog.ConfirmAsync( "Discard notification", $"Permanently discard notification {ShortId(n.NotificationId)} (\"{n.Subject}\")? This cannot be undone.", danger: true); if (!confirmed) return; _actionInProgress = true; try { var response = await CommunicationService.DiscardNotificationAsync( new DiscardNotificationRequest(Guid.NewGuid().ToString("N"), n.NotificationId)); if (response.Success) { _toast.ShowSuccess($"Notification {ShortId(n.NotificationId)} discarded."); await RefreshAll(); } else { _toast.ShowError(response.ErrorMessage ?? "Discard failed."); } } catch (Exception ex) { _toast.ShowError($"Discard failed: {ex.Message}"); } _actionInProgress = false; } private void ClearFilters() { _statusFilter = string.Empty; _typeFilter = string.Empty; _siteFilter = string.Empty; _listFilter = string.Empty; _subjectFilter = string.Empty; _stuckOnly = false; _fromFilter = null; _toFilter = null; } private bool HasActiveFilters => !string.IsNullOrEmpty(_statusFilter) || !string.IsNullOrEmpty(_typeFilter) || !string.IsNullOrEmpty(_siteFilter) || !string.IsNullOrEmpty(_listFilter) || !string.IsNullOrEmpty(_subjectFilter) || _stuckOnly || _fromFilter != null || _toFilter != null; private string SiteName(string siteId) => _sites.FirstOrDefault(s => s.SiteIdentifier == siteId)?.Name ?? siteId; private static string? NullIfEmpty(string s) => string.IsNullOrWhiteSpace(s) ? null : s.Trim(); private static DateTimeOffset? ToUtc(DateTime? local) => local == null ? null : new DateTimeOffset(DateTime.SpecifyKind(local.Value, DateTimeKind.Utc)); private static string ShortId(string id) => id[..Math.Min(12, id.Length)]; private static string StatusBadgeClass(string status) => status switch { "Delivered" => "bg-success", "Parked" => "bg-danger", "Retrying" => "bg-warning text-dark", "Pending" => "bg-info text-dark", "Discarded" => "bg-secondary", _ => "bg-light text-dark" }; }