Add redundancy panel to status dashboard

Shows mode, role, ServiceLevel, ApplicationUri, and redundant server
set when redundancy is enabled. Primary renders with a green border,
secondary with yellow. Also included in the JSON API response.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-28 15:27:52 -04:00
parent afd6c33d9d
commit f0a076ec26
3 changed files with 86 additions and 2 deletions

View File

@@ -215,7 +215,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
// Step 13: Dashboard
_healthCheck = new HealthCheckService();
_statusReport = new StatusReportService(_healthCheck, _config.Dashboard.RefreshIntervalSeconds);
_statusReport.SetComponents(effectiveMxClient, _metrics, _galaxyStats, _serverHost, _nodeManager);
_statusReport.SetComponents(effectiveMxClient, _metrics, _galaxyStats, _serverHost, _nodeManager,
_config.Redundancy, _config.OpcUa.ApplicationUri);
if (_config.Dashboard.Enabled)
{

View File

@@ -39,6 +39,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
/// </summary>
public Dictionary<string, MetricsStatistics> Operations { get; set; } = new();
/// <summary>
/// Gets or sets the redundancy state when redundancy is enabled.
/// </summary>
public RedundancyInfo? Redundancy { get; set; }
/// <summary>
/// Gets or sets footer details such as the snapshot timestamp and service version.
/// </summary>
@@ -160,6 +165,42 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
public long TotalEvents { get; set; }
}
/// <summary>
/// Dashboard model for redundancy state. Only populated when redundancy is enabled.
/// </summary>
public class RedundancyInfo
{
/// <summary>
/// Gets or sets whether redundancy is enabled.
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Gets or sets the redundancy mode (e.g., "Warm", "Hot").
/// </summary>
public string Mode { get; set; } = "";
/// <summary>
/// Gets or sets this instance's role ("Primary" or "Secondary").
/// </summary>
public string Role { get; set; } = "";
/// <summary>
/// Gets or sets the current ServiceLevel byte.
/// </summary>
public byte ServiceLevel { get; set; }
/// <summary>
/// Gets or sets this instance's ApplicationUri.
/// </summary>
public string ApplicationUri { get; set; } = "";
/// <summary>
/// Gets or sets the list of all server URIs in the redundant set.
/// </summary>
public List<string> ServerUris { get; set; } = new();
}
/// <summary>
/// Dashboard model for the status page footer.
/// </summary>

View File

@@ -1,6 +1,7 @@
using System;
using System.Text;
using System.Text.Json;
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository;
using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
@@ -21,6 +22,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
private GalaxyRepositoryStats? _galaxyStats;
private OpcUaServerHost? _serverHost;
private LmxNodeManager? _nodeManager;
private RedundancyConfiguration? _redundancyConfig;
private string? _applicationUri;
/// <summary>
/// Initializes a new status report service for the dashboard using the supplied health-check policy and refresh interval.
@@ -43,13 +46,16 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
/// <param name="nodeManager">The node manager whose queue depth and MXAccess event throughput should be surfaced on the dashboard.</param>
public void SetComponents(IMxAccessClient? mxAccessClient, PerformanceMetrics? metrics,
GalaxyRepositoryStats? galaxyStats, OpcUaServerHost? serverHost,
LmxNodeManager? nodeManager = null)
LmxNodeManager? nodeManager = null,
RedundancyConfiguration? redundancyConfig = null, string? applicationUri = null)
{
_mxAccessClient = mxAccessClient;
_metrics = metrics;
_galaxyStats = galaxyStats;
_serverHost = serverHost;
_nodeManager = nodeManager;
_redundancyConfig = redundancyConfig;
_applicationUri = applicationUri;
}
/// <summary>
@@ -90,6 +96,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
TotalEvents = _nodeManager?.TotalMxChangeEvents ?? 0
},
Operations = _metrics?.GetStatistics() ?? new(),
Redundancy = BuildRedundancyInfo(),
Footer = new FooterInfo
{
Timestamp = DateTime.UtcNow,
@@ -98,6 +105,30 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
};
}
private RedundancyInfo? BuildRedundancyInfo()
{
if (_redundancyConfig == null || !_redundancyConfig.Enabled)
return null;
var mxConnected = (_mxAccessClient?.State ?? ConnectionState.Disconnected) == ConnectionState.Connected;
var dbConnected = _galaxyStats?.DbConnected ?? false;
var isPrimary = string.Equals(_redundancyConfig.Role, "Primary", StringComparison.OrdinalIgnoreCase);
var baseLevel = isPrimary
? _redundancyConfig.ServiceLevelBase
: Math.Max(0, _redundancyConfig.ServiceLevelBase - 50);
var calculator = new ServiceLevelCalculator();
return new RedundancyInfo
{
Enabled = true,
Mode = _redundancyConfig.Mode,
Role = _redundancyConfig.Role,
ServiceLevel = calculator.Calculate(baseLevel, mxConnected, dbConnected),
ApplicationUri = _applicationUri ?? "",
ServerUris = new System.Collections.Generic.List<string>(_redundancyConfig.ServerUris)
};
}
/// <summary>
/// Generates the operator-facing HTML dashboard for the current bridge status.
/// </summary>
@@ -131,6 +162,17 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
sb.AppendLine($"<p>Status: <b>{data.Health.Status}</b> — {data.Health.Message}</p>");
sb.AppendLine("</div>");
// Redundancy panel (only when enabled)
if (data.Redundancy != null)
{
var roleColor = data.Redundancy.Role == "Primary" ? "green" : "yellow";
sb.AppendLine($"<div class='panel {roleColor}'><h2>Redundancy</h2>");
sb.AppendLine($"<p>Mode: <b>{data.Redundancy.Mode}</b> | Role: <b>{data.Redundancy.Role}</b> | Service Level: <b>{data.Redundancy.ServiceLevel}</b></p>");
sb.AppendLine($"<p>Application URI: {data.Redundancy.ApplicationUri}</p>");
sb.AppendLine($"<p>Redundant Set: {string.Join(", ", data.Redundancy.ServerUris)}</p>");
sb.AppendLine("</div>");
}
// Subscriptions panel
sb.AppendLine("<div class='panel gray'><h2>Subscriptions</h2>");
sb.AppendLine($"<p>Active: {data.Subscriptions.ActiveCount}</p>");