diff --git a/src/ScadaLink.CentralUI/Components/Pages/Monitoring/Health.razor b/src/ScadaLink.CentralUI/Components/Pages/Monitoring/Health.razor index b671e49..2aa846a 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Monitoring/Health.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Monitoring/Health.razor @@ -1,9 +1,12 @@ @page "/monitoring/health" @attribute [Authorize] @using ScadaLink.Commons.Types.Enums +@using ScadaLink.Commons.Entities.Sites +@using ScadaLink.Commons.Interfaces.Repositories @using ScadaLink.HealthMonitoring @implements IDisposable @inject ICentralHealthAggregator HealthAggregator +@inject ISiteRepository SiteRepository
@@ -56,11 +59,12 @@
- @* Per-site detail *@ + @* Per-site detail cards *@ @foreach (var (siteId, state) in _siteStates.OrderBy(s => s.Key)) { + var siteName = GetSiteName(siteId);
-
+
@if (state.IsOnline) { @@ -70,24 +74,34 @@ { Offline } - @siteId - @if (state.LatestReport?.NodeRole != null) - { - @state.LatestReport.NodeRole - } + @siteName (@siteId)
Last report: @state.LastReportReceivedAt.LocalDateTime.ToString("HH:mm:ss") | Seq: @state.LastSequenceNumber
-
+
@if (state.LatestReport != null) { var report = state.LatestReport; -
- @* Connection Health *@ -
-
Data Connections
+
+ @* Column 1: Nodes *@ +
+
Nodes
+ + + + + + + + +
@(report.NodeHostname != "" ? report.NodeHostname : "Node")@(state.IsOnline ? "Online" : "Offline")@(report.NodeRole == "Active" ? "Primary" : "Standby")
+
+ + @* Column 2: Data Connections *@ +
+
Data Connections
@if (report.DataConnectionStatuses.Count == 0) { None @@ -96,34 +110,77 @@ { @foreach (var (connName, health) in report.DataConnectionStatuses) { -
- @connName - @health + 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")
+ }
} }
- @* Instances *@ -
-
Instances
-
- Deployed - @report.DeployedInstanceCount -
-
- Enabled - @report.EnabledInstanceCount -
-
- Disabled - @report.DisabledInstanceCount -
+ @* Column 3: Instances + Store-and-Forward *@ +
+
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) + { +
+ @category + @depth +
+ } + }
- @* Error Counts *@ -
-
Error Counts
+ @* Column 4: Error Counts + Parked Messages *@ +
+
Error Counts
@@ -146,24 +203,15 @@
-
- @* S&F Buffer Depths *@ -
-
Store-and-Forward Buffers
- @if (report.StoreAndForwardBufferDepths.Count == 0) +
Parked Messages
+ @if (report.ParkedMessageCount == 0) { Empty } else { - @foreach (var (category, depth) in report.StoreAndForwardBufferDepths) - { -
- @category - @depth -
- } + @report.ParkedMessageCount }
@@ -180,11 +228,23 @@ @code { private IReadOnlyDictionary _siteStates = new Dictionary(); + private Dictionary _siteNames = new(); private Timer? _refreshTimer; private int _autoRefreshSeconds = 10; - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { + // Load site names for display + try + { + var sites = await SiteRepository.GetAllSitesAsync(); + _siteNames = sites.ToDictionary(s => s.SiteIdentifier, s => s.Name); + } + catch + { + // Non-fatal — fall back to showing siteId only + } + RefreshNow(); _refreshTimer = new Timer(_ => { @@ -201,6 +261,11 @@ _siteStates = HealthAggregator.GetAllSiteStates(); } + private string GetSiteName(string siteId) + { + return _siteNames.GetValueOrDefault(siteId, siteId); + } + private static string GetConnectionHealthBadge(ConnectionHealth health) => health switch { ConnectionHealth.Connected => "bg-success", diff --git a/src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs b/src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs index 8ec2e07..39da31c 100644 --- a/src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs +++ b/src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs @@ -15,4 +15,8 @@ public record SiteHealthReport( int DeployedInstanceCount, int EnabledInstanceCount, int DisabledInstanceCount, - string NodeRole = "Unknown"); + string NodeRole = "Unknown", + string NodeHostname = "", + IReadOnlyDictionary? DataConnectionEndpoints = null, + IReadOnlyDictionary? DataConnectionTagQuality = null, + int ParkedMessageCount = 0); diff --git a/src/ScadaLink.Commons/Messages/Health/TagQualityCounts.cs b/src/ScadaLink.Commons/Messages/Health/TagQualityCounts.cs new file mode 100644 index 0000000..232d800 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Health/TagQualityCounts.cs @@ -0,0 +1,3 @@ +namespace ScadaLink.Commons.Messages.Health; + +public record TagQualityCounts(int Good, int Bad, int Uncertain); diff --git a/src/ScadaLink.HealthMonitoring/ISiteHealthCollector.cs b/src/ScadaLink.HealthMonitoring/ISiteHealthCollector.cs index 68b122e..7a2236e 100644 --- a/src/ScadaLink.HealthMonitoring/ISiteHealthCollector.cs +++ b/src/ScadaLink.HealthMonitoring/ISiteHealthCollector.cs @@ -15,8 +15,12 @@ public interface ISiteHealthCollector void UpdateConnectionHealth(string connectionName, ConnectionHealth health); void RemoveConnection(string connectionName); void UpdateTagResolution(string connectionName, int totalSubscribed, int successfullyResolved); + void UpdateConnectionEndpoint(string connectionName, string endpoint); + void UpdateTagQuality(string connectionName, int good, int bad, int uncertain); void SetStoreAndForwardDepths(IReadOnlyDictionary depths); void SetInstanceCounts(int deployed, int enabled, int disabled); + void SetParkedMessageCount(int count); + void SetNodeHostname(string hostname); void SetActiveNode(bool isActive); bool IsActiveNode { get; } SiteHealthReport CollectReport(string siteId); diff --git a/src/ScadaLink.HealthMonitoring/SiteHealthCollector.cs b/src/ScadaLink.HealthMonitoring/SiteHealthCollector.cs index 6b3fa59..68cceec 100644 --- a/src/ScadaLink.HealthMonitoring/SiteHealthCollector.cs +++ b/src/ScadaLink.HealthMonitoring/SiteHealthCollector.cs @@ -15,8 +15,12 @@ public class SiteHealthCollector : ISiteHealthCollector private int _deadLetterCount; private readonly ConcurrentDictionary _connectionStatuses = new(); private readonly ConcurrentDictionary _tagResolutionCounts = new(); + private readonly ConcurrentDictionary _connectionEndpoints = new(); + private readonly ConcurrentDictionary _tagQualityCounts = new(); private IReadOnlyDictionary _sfBufferDepths = new Dictionary(); private int _deployedInstanceCount, _enabledInstanceCount, _disabledInstanceCount; + private int _parkedMessageCount; + private volatile string _nodeHostname = ""; private volatile bool _isActiveNode; /// @@ -60,6 +64,8 @@ public class SiteHealthCollector : ISiteHealthCollector { _connectionStatuses.TryRemove(connectionName, out _); _tagResolutionCounts.TryRemove(connectionName, out _); + _connectionEndpoints.TryRemove(connectionName, out _); + _tagQualityCounts.TryRemove(connectionName, out _); } /// @@ -71,6 +77,23 @@ public class SiteHealthCollector : ISiteHealthCollector _tagResolutionCounts[connectionName] = new TagResolutionStatus(totalSubscribed, successfullyResolved); } + public void UpdateConnectionEndpoint(string connectionName, string endpoint) + { + _connectionEndpoints[connectionName] = endpoint; + } + + public void UpdateTagQuality(string connectionName, int good, int bad, int uncertain) + { + _tagQualityCounts[connectionName] = new TagQualityCounts(good, bad, uncertain); + } + + public void SetParkedMessageCount(int count) + { + Interlocked.Exchange(ref _parkedMessageCount, count); + } + + public void SetNodeHostname(string hostname) => _nodeHostname = hostname; + /// /// Set the current store-and-forward buffer depths snapshot. /// Called before report collection with data from the S&F service. @@ -110,6 +133,8 @@ public class SiteHealthCollector : ISiteHealthCollector // Snapshot current connection and tag resolution state var connectionStatuses = new Dictionary(_connectionStatuses); var tagResolution = new Dictionary(_tagResolutionCounts); + var connectionEndpoints = new Dictionary(_connectionEndpoints); + var tagQuality = new Dictionary(_tagQualityCounts); // Snapshot current S&F buffer depths var sfBufferDepths = new Dictionary(_sfBufferDepths); @@ -130,6 +155,10 @@ public class SiteHealthCollector : ISiteHealthCollector DeployedInstanceCount: _deployedInstanceCount, EnabledInstanceCount: _enabledInstanceCount, DisabledInstanceCount: _disabledInstanceCount, - NodeRole: nodeRole); + NodeRole: nodeRole, + NodeHostname: _nodeHostname, + DataConnectionEndpoints: connectionEndpoints, + DataConnectionTagQuality: tagQuality, + ParkedMessageCount: Interlocked.CompareExchange(ref _parkedMessageCount, 0, 0)); } }