-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
+
@@ -65,20 +92,50 @@
}
@foreach (var entry in _entries)
{
+ var entityIdShort = entry.EntityId is { Length: > 0 }
+ ? entry.EntityId[..Math.Min(12, entry.EntityId.Length)]
+ : "";
+ var hasState = !string.IsNullOrWhiteSpace(entry.AfterStateJson);
+ var isLarge = hasState && entry.AfterStateJson!.Length > 1024;
|
@entry.User |
@entry.Action |
@entry.EntityType |
- @entry.EntityId |
+
+ @if (!string.IsNullOrEmpty(entry.EntityId))
+ {
+ @entityIdShort…
+
+ }
+ else
+ {
+ —
+ }
+ |
@entry.EntityName |
- @if (!string.IsNullOrWhiteSpace(entry.AfterStateJson))
+ @if (hasState)
{
-
+ if (isLarge)
+ {
+
+ }
+ else
+ {
+
+ }
}
else
{
@@ -86,11 +143,11 @@
}
|
- @if (_expandedEntryId == entry.Id && !string.IsNullOrWhiteSpace(entry.AfterStateJson))
+ @if (hasState && !isLarge && _expandedEntryId == entry.Id)
{
- @FormatJson(entry.AfterStateJson)
+ @FormatJson(entry.AfterStateJson!)
|
}
@@ -99,10 +156,33 @@
-
Page @_page of @((_totalCount + _pageSize - 1) / _pageSize) (@_totalCount total)
+
Page @_page of @TotalPages (@_totalCount total)
-
+
+
+
+ }
+
+ @if (_modalEntry != null)
+ {
+
+
}
@@ -122,15 +202,30 @@
private bool _searching;
private string? _errorMessage;
private int? _expandedEntryId;
+ private AuditLogEntry? _modalEntry;
private ToastNotification _toast = default!;
+ private int TotalPages => _pageSize > 0 ? Math.Max(1, (_totalCount + _pageSize - 1) / _pageSize) : 1;
+ private bool HasMore => _page * _pageSize < _totalCount;
+
private async Task Search()
{
_page = 1;
await FetchPage();
}
+ private async Task ClearFilters()
+ {
+ _filterUser = null;
+ _filterEntityType = null;
+ _filterAction = null;
+ _filterFrom = null;
+ _filterTo = null;
+ _page = 1;
+ await FetchPage();
+ }
+
private async Task PrevPage() { _page--; await FetchPage(); }
private async Task NextPage() { _page++; await FetchPage(); }
@@ -164,6 +259,29 @@
_expandedEntryId = _expandedEntryId == entryId ? null : entryId;
}
+ private void ShowStateModal(AuditLogEntry entry)
+ {
+ _modalEntry = entry;
+ }
+
+ private void CloseStateModal()
+ {
+ _modalEntry = null;
+ }
+
+ private async Task CopyAsync(string text)
+ {
+ try
+ {
+ await JS.InvokeVoidAsync("navigator.clipboard.writeText", text);
+ _toast.ShowSuccess("Copied to clipboard.");
+ }
+ catch
+ {
+ _toast.ShowError("Copy failed.");
+ }
+ }
+
private static string GetActionBadge(string action) => action switch
{
"Create" => "bg-success",
diff --git a/src/ScadaLink.CentralUI/Components/Pages/Monitoring/EventLogs.razor b/src/ScadaLink.CentralUI/Components/Pages/Monitoring/EventLogs.razor
index 8efff47..3ea3a2d 100644
--- a/src/ScadaLink.CentralUI/Components/Pages/Monitoring/EventLogs.razor
+++ b/src/ScadaLink.CentralUI/Components/Pages/Monitoring/EventLogs.razor
@@ -8,55 +8,92 @@
@inject CommunicationService CommunicationService
-
Site Event Logs
+
+
Site Event Logs
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -70,6 +107,7 @@
+ |
Timestamp |
Type |
Severity |
@@ -81,28 +119,59 @@
@if (_entries.Count == 0)
{
- | No events found. |
+ | No events found. |
}
- @foreach (var entry in _entries)
+ @for (int i = 0; i < _entries.Count; i++)
{
-
+ var idx = i;
+ var entry = _entries[idx];
+ var rowClass = entry.Severity == "Error" ? "table-danger"
+ : entry.Severity == "Warning" ? "table-warning"
+ : "";
+ var expanded = _expandedRows.Contains(idx);
+
+ |
+
+ |
|
@entry.EventType |
- @entry.Severity |
+
+
+ @SeverityGlyph(entry.Severity) @entry.Severity
+
+ |
@(entry.InstanceId ?? "—") |
@entry.Source |
- @entry.Message |
+ @entry.Message |
+ @if (expanded)
+ {
+
+
+ @entry.Message
+ |
+
+ }
}
-
@_entries.Count entries loaded
- @if (_hasMore)
- {
-
- }
+
Showing @_entries.Count entries
+
+ @if (_hasMore)
+ {
+
+ }
+ else if (_entries.Count > 0)
+ {
+ End of results
+ }
+
}
@@ -123,6 +192,23 @@
private bool _searching;
private string? _errorMessage;
private ToastNotification _toast = default!;
+ private readonly HashSet
_expandedRows = new();
+
+ private int ActiveFilterCount
+ {
+ get
+ {
+ var n = 0;
+ if (!string.IsNullOrEmpty(_selectedSiteId)) n++;
+ if (!string.IsNullOrWhiteSpace(_filterEventType)) n++;
+ if (!string.IsNullOrEmpty(_filterSeverity)) n++;
+ if (_filterFrom.HasValue) n++;
+ if (_filterTo.HasValue) n++;
+ if (!string.IsNullOrWhiteSpace(_filterKeyword)) n++;
+ if (!string.IsNullOrWhiteSpace(_filterInstanceName)) n++;
+ return n;
+ }
+ }
protected override async Task OnInitializedAsync()
{
@@ -133,11 +219,20 @@
{
_entries = new();
_continuationToken = null;
+ _expandedRows.Clear();
await FetchPage();
}
private async Task LoadMore() => await FetchPage();
+ private void ToggleRow(int idx)
+ {
+ if (!_expandedRows.Add(idx))
+ {
+ _expandedRows.Remove(idx);
+ }
+ }
+
private async Task FetchPage()
{
_searching = true;
@@ -185,4 +280,12 @@
"Info" => "bg-info text-dark",
_ => "bg-secondary"
};
+
+ private static string SeverityGlyph(string severity) => severity switch
+ {
+ "Error" => "⛔",
+ "Warning" => "⚠",
+ "Info" => "ℹ",
+ _ => "•"
+ };
}
diff --git a/src/ScadaLink.CentralUI/Components/Pages/Monitoring/Health.razor b/src/ScadaLink.CentralUI/Components/Pages/Monitoring/Health.razor
index f57bae8..b655802 100644
--- a/src/ScadaLink.CentralUI/Components/Pages/Monitoring/Health.razor
+++ b/src/ScadaLink.CentralUI/Components/Pages/Monitoring/Health.razor
@@ -24,36 +24,28 @@
else
{
@* Overview cards *@
-
-
-
+
+
+
@_siteStates.Values.Count(s => s.IsOnline)
Sites Online
-
-
+
+
@_siteStates.Values.Count(s => !s.IsOnline)
Sites Offline
-
-
+
+
-
@_siteStates.Count
- Total Sites
-
-
-
-
-
-
-
@_siteStates.Values.Sum(s => s.LatestReport?.ScriptErrorCount ?? 0)
- Total Script Errors
+ @_siteStates.Values.Count(SiteHasActiveErrors)
+ Sites with active errors
@@ -63,21 +55,22 @@
@foreach (var (siteId, state) in _siteStates.OrderBy(s => s.Key))
{
var siteName = GetSiteName(siteId);
+ var detailsCollapseId = $"site-details-{siteId}";
@@ -86,7 +79,7 @@
var report = state.LatestReport;
@* Column 1: Nodes *@
-
+
Nodes
@@ -96,8 +89,18 @@
{
| @node.Hostname |
- @(node.IsOnline ? "Online" : "Offline") |
- @node.Role |
+
+
+ @(node.IsOnline ? OnlineGlyph : OfflineGlyph) @(node.IsOnline ? "Online" : "Offline")
+
+ |
+
+
+ @(node.Role == "Primary" ? PrimaryGlyph : StandbyGlyph) @node.Role
+
+ |
}
}
@@ -105,128 +108,164 @@
{
| @(report.NodeHostname != "" ? report.NodeHostname : "Node") |
- @(state.IsOnline ? "Online" : "Offline") |
- @(report.NodeRole == "Active" ? "Primary" : "Standby") |
+
+
+ @(state.IsOnline ? OnlineGlyph : OfflineGlyph) @(state.IsOnline ? "Online" : "Offline")
+
+ |
+
+ @{
+ var roleLabel = report.NodeRole == "Active" ? "Primary" : "Standby";
+ }
+
+ @(roleLabel == "Primary" ? PrimaryGlyph : StandbyGlyph) @roleLabel
+
+ |
}
- @* Column 2: Data Connections *@
-
-
Data Connections
- @if (report.DataConnectionStatuses.Count == 0)
- {
-
None
- }
- else
- {
- @foreach (var (connName, health) in report.DataConnectionStatuses)
+ @* Column 2: Data Connections (collapsible) *@
+
+
+
+ @if (report.DataConnectionStatuses.Count == 0)
{
- var endpoint = report.DataConnectionEndpoints?.GetValueOrDefault(connName);
- var quality = report.DataConnectionTagQuality?.GetValueOrDefault(connName);
-
-
-
@connName
-
@(endpoint ?? health.ToString())
+
None
+ }
+ else
+ {
+ @foreach (var (connName, health) in report.DataConnectionStatuses)
+ {
+ var endpoint = report.DataConnectionEndpoints?.GetValueOrDefault(connName);
+ var quality = report.DataConnectionTagQuality?.GetValueOrDefault(connName);
+
+
+ @connName
+ @(endpoint ?? health.ToString())
+
+ @if (quality != null)
+ {
+
+
+
+ | Tags good |
+ @quality.Good.ToString("N0") |
+
+
+ | Tags bad |
+ @quality.Bad.ToString("N0") |
+
+
+ | Tags uncertain |
+ @quality.Uncertain.ToString("N0") |
+
+
+
+ }
- @if (quality != null)
- {
-
-
-
- | Tags good |
- @quality.Good.ToString("N0") |
-
-
- | Tags bad |
- @quality.Bad.ToString("N0") |
-
-
- | Tags uncertain |
- @quality.Uncertain.ToString("N0") |
-
-
-
- }
-
+ }
}
- }
+
- @* Column 3: Instances + Store-and-Forward *@
-
-
Instances
-
-
-
- | Deployed |
- @report.DeployedInstanceCount |
-
-
- | Enabled |
- @report.EnabledInstanceCount |
-
-
- | Disabled |
- @report.DisabledInstanceCount |
-
-
-
+ @* Column 3: Instances + Store-and-Forward (collapsible) *@
+
+
+
+
Instances
+
+
+
+ | Deployed |
+ @report.DeployedInstanceCount |
+
+
+ | Enabled |
+ @report.EnabledInstanceCount |
+
+
+ | Disabled |
+ @report.DisabledInstanceCount |
+
+
+
-
Store-and-Forward Buffers
- @if (report.StoreAndForwardBufferDepths.Count == 0)
- {
-
Empty
- }
- else
- {
- @foreach (var (category, depth) in report.StoreAndForwardBufferDepths)
+
Store-and-Forward Buffers
+ @if (report.StoreAndForwardBufferDepths.Count == 0)
{
-
- @category
- @depth
-
+
Empty
}
- }
+ else
+ {
+ @foreach (var (category, depth) in report.StoreAndForwardBufferDepths)
+ {
+
+ @category
+ @depth
+
+ }
+ }
+
- @* Column 4: Error Counts + Parked Messages *@
-
-
Error Counts
-
-
-
- | Script Errors |
-
- @report.ScriptErrorCount
- |
-
-
- | Alarm Eval Errors |
-
- @report.AlarmEvaluationErrorCount
- |
-
-
- | Dead Letters |
-
- @report.DeadLetterCount
- |
-
-
-
+ @* Column 4: Error Counts + Parked Messages (collapsible) *@
+
+
+
+
Error Counts
+
+
+
+ | Script Errors |
+
+ @report.ScriptErrorCount
+ |
+
+
+ | Alarm Eval Errors |
+
+ @report.AlarmEvaluationErrorCount
+ |
+
+
+ | Dead Letters |
+
+ @report.DeadLetterCount
+ |
+
+
+
-
Parked Messages
- @if (report.ParkedMessageCount == 0)
- {
-
Empty
- }
- else
- {
-
@report.ParkedMessageCount
- }
+
Parked Messages
+ @if (report.ParkedMessageCount == 0)
+ {
+
Empty
+ }
+ else
+ {
+
@report.ParkedMessageCount
+ }
+
}
@@ -241,11 +280,26 @@
@code {
+ // Shape-coded status glyphs to pair with badge colour.
+ private const string OnlineGlyph = "●"; // ●
+ private const string OfflineGlyph = "○"; // ○
+ private const string PrimaryGlyph = "▲"; // ▲
+ private const string StandbyGlyph = "△"; // △
+
private IReadOnlyDictionary
_siteStates = new Dictionary();
private Dictionary _siteNames = new();
private Timer? _refreshTimer;
private int _autoRefreshSeconds = 10;
+ private static bool SiteHasActiveErrors(SiteHealthState state)
+ {
+ var report = state.LatestReport;
+ if (report == null) return false;
+ return report.ScriptErrorCount > 0
+ || report.AlarmEvaluationErrorCount > 0
+ || report.DeadLetterCount > 0;
+ }
+
protected override async Task OnInitializedAsync()
{
// Load site names for display
diff --git a/src/ScadaLink.CentralUI/Components/Pages/Monitoring/ParkedMessages.razor b/src/ScadaLink.CentralUI/Components/Pages/Monitoring/ParkedMessages.razor
index a216ef8..9cf0825 100644
--- a/src/ScadaLink.CentralUI/Components/Pages/Monitoring/ParkedMessages.razor
+++ b/src/ScadaLink.CentralUI/Components/Pages/Monitoring/ParkedMessages.razor
@@ -6,17 +6,18 @@
@using ScadaLink.Communication
@inject ISiteRepository SiteRepository
@inject CommunicationService CommunicationService
+@inject IJSRuntime JS
Parked Messages
-
+
-
+
-
-
-
+
@@ -43,6 +44,7 @@
+ |
Message ID |
Target System |
Method |
@@ -56,27 +58,70 @@
@if (_messages.Count == 0)
{
- | No parked messages. |
+ | No parked messages. |
}
- @foreach (var msg in _messages)
+ @for (int i = 0; i < _messages.Count; i++)
{
+ var idx = i;
+ var msg = _messages[idx];
+ var idShort = msg.MessageId[..Math.Min(12, msg.MessageId.Length)];
+ var expanded = _expandedRows.Contains(idx);
+ var retryActive = _actionInProgress && _activeMessageId == msg.MessageId && _activeAction == "Retry";
+ var discardActive = _actionInProgress && _activeMessageId == msg.MessageId && _activeAction == "Discard";
- @msg.MessageId[..Math.Min(12, msg.MessageId.Length)] |
+
+
+ |
+
+ @idShort…
+
+ |
@msg.TargetSystem |
@msg.MethodName |
- @msg.ErrorMessage |
+ @msg.ErrorMessage |
@msg.AttemptCount |
|
|
+ @onclick="() => RetryMessage(msg)"
+ disabled="@_actionInProgress"
+ title="Retry message (move back to pending)"
+ aria-label="Retry message @idShort">
+ @if (retryActive)
+ {
+
+ }
+ Retry
+
+ @onclick="() => DiscardMessage(msg)"
+ disabled="@_actionInProgress"
+ title="Permanently discard message"
+ aria-label="Discard message @idShort">
+ @if (discardActive)
+ {
+
+ }
+ Discard
+
|
+ @if (expanded)
+ {
+
+
+ @msg.ErrorMessage
+ |
+
+ }
}
@@ -105,8 +150,11 @@
private string? _errorMessage;
private bool _actionInProgress;
+ private string? _activeMessageId;
+ private string? _activeAction;
private ToastNotification _toast = default!;
private ConfirmDialog _confirmDialog = default!;
+ private readonly HashSet
_expandedRows = new();
protected override async Task OnInitializedAsync()
{
@@ -116,12 +164,34 @@
private async Task Search()
{
_pageNumber = 1;
+ _expandedRows.Clear();
await FetchPage();
}
private async Task PrevPage() { _pageNumber--; await FetchPage(); }
private async Task NextPage() { _pageNumber++; await FetchPage(); }
+ private void ToggleRow(int idx)
+ {
+ if (!_expandedRows.Add(idx))
+ {
+ _expandedRows.Remove(idx);
+ }
+ }
+
+ private async Task CopyAsync(string text)
+ {
+ try
+ {
+ await JS.InvokeVoidAsync("navigator.clipboard.writeText", text);
+ _toast.ShowSuccess("Copied to clipboard.");
+ }
+ catch
+ {
+ _toast.ShowError("Copy failed.");
+ }
+ }
+
private async Task FetchPage()
{
_searching = true;
@@ -141,6 +211,7 @@
{
_messages = response.Messages.ToList();
_totalCount = response.TotalCount;
+ _expandedRows.Clear();
}
else
{
@@ -157,6 +228,8 @@
private async Task RetryMessage(ParkedMessageEntry msg)
{
_actionInProgress = true;
+ _activeMessageId = msg.MessageId;
+ _activeAction = "Retry";
try
{
var request = new ParkedMessageRetryRequest(
@@ -179,6 +252,8 @@
{
_toast.ShowError($"Retry failed: {ex.Message}");
}
+ _activeMessageId = null;
+ _activeAction = null;
_actionInProgress = false;
}
@@ -190,6 +265,8 @@
if (!confirmed) return;
_actionInProgress = true;
+ _activeMessageId = msg.MessageId;
+ _activeAction = "Discard";
try
{
var request = new ParkedMessageDiscardRequest(
@@ -212,6 +289,8 @@
{
_toast.ShowError($"Discard failed: {ex.Message}");
}
+ _activeMessageId = null;
+ _activeAction = null;
_actionInProgress = false;
}
}