diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUaService.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUaService.cs index ed24b52..ec3955e 100644 --- a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUaService.cs +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUaService.cs @@ -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) { diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusData.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusData.cs index 0e8db72..3f4ad4d 100644 --- a/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusData.cs +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusData.cs @@ -39,6 +39,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status /// public Dictionary Operations { get; set; } = new(); + /// + /// Gets or sets the redundancy state when redundancy is enabled. + /// + public RedundancyInfo? Redundancy { get; set; } + /// /// Gets or sets footer details such as the snapshot timestamp and service version. /// @@ -160,6 +165,42 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status public long TotalEvents { get; set; } } + /// + /// Dashboard model for redundancy state. Only populated when redundancy is enabled. + /// + public class RedundancyInfo + { + /// + /// Gets or sets whether redundancy is enabled. + /// + public bool Enabled { get; set; } + + /// + /// Gets or sets the redundancy mode (e.g., "Warm", "Hot"). + /// + public string Mode { get; set; } = ""; + + /// + /// Gets or sets this instance's role ("Primary" or "Secondary"). + /// + public string Role { get; set; } = ""; + + /// + /// Gets or sets the current ServiceLevel byte. + /// + public byte ServiceLevel { get; set; } + + /// + /// Gets or sets this instance's ApplicationUri. + /// + public string ApplicationUri { get; set; } = ""; + + /// + /// Gets or sets the list of all server URIs in the redundant set. + /// + public List ServerUris { get; set; } = new(); + } + /// /// Dashboard model for the status page footer. /// diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusReportService.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusReportService.cs index cc4219c..42c5e82 100644 --- a/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusReportService.cs +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusReportService.cs @@ -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; /// /// 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 /// The node manager whose queue depth and MXAccess event throughput should be surfaced on the dashboard. 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; } /// @@ -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(_redundancyConfig.ServerUris) + }; + } + /// /// Generates the operator-facing HTML dashboard for the current bridge status. /// @@ -131,6 +162,17 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status sb.AppendLine($"

Status: {data.Health.Status} — {data.Health.Message}

"); sb.AppendLine(""); + // Redundancy panel (only when enabled) + if (data.Redundancy != null) + { + var roleColor = data.Redundancy.Role == "Primary" ? "green" : "yellow"; + sb.AppendLine($"

Redundancy

"); + sb.AppendLine($"

Mode: {data.Redundancy.Mode} | Role: {data.Redundancy.Role} | Service Level: {data.Redundancy.ServiceLevel}

"); + sb.AppendLine($"

Application URI: {data.Redundancy.ApplicationUri}

"); + sb.AppendLine($"

Redundant Set: {string.Join(", ", data.Redundancy.ServerUris)}

"); + sb.AppendLine("
"); + } + // Subscriptions panel sb.AppendLine("

Subscriptions

"); sb.AppendLine($"

Active: {data.Subscriptions.ActiveCount}

");