diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/AlarmObjectFilter.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/AlarmObjectFilter.cs index 680bbea..0f0f080 100644 --- a/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/AlarmObjectFilter.cs +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Domain/AlarmObjectFilter.cs @@ -93,6 +93,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain public IReadOnlyList UnmatchedPatterns => _rawPatterns.Where(p => !_matchedRawPatterns.Contains(p)).ToList(); + /// + /// Gets the raw pattern strings exactly as supplied by the operator after comma-splitting + /// and trimming. Surfaced on the status dashboard so operators can confirm the active filter. + /// + public IReadOnlyList RawPatterns => _rawPatterns; + /// /// Returns when any template name in matches any /// compiled pattern. An empty chain never matches unless the operator explicitly supplied a pattern diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs index 32c380d..c3a997d 100644 --- a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs @@ -183,6 +183,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa /// public int AlarmFilterIncludedObjectCount => _alarmFilterIncludedObjectCount; + /// + /// Gets the raw alarm filter patterns exactly as configured, for display on the status dashboard. + /// Returns an empty list when no filter is active. + /// + public IReadOnlyList AlarmFilterPatterns => + _alarmObjectFilter?.RawPatterns ?? Array.Empty(); + /// /// Gets the number of distinct alarm conditions currently tracked (one per alarm attribute). /// diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusData.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusData.cs index 91add15..4ed362b 100644 --- a/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusData.cs +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusData.cs @@ -308,6 +308,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status /// Gets or sets the number of Galaxy objects included by the alarm filter during the most recent build. /// public int FilterIncludedObjectCount { get; set; } + + /// + /// Gets or sets the raw alarm filter patterns exactly as configured, for dashboard display. + /// + public List FilterPatterns { get; set; } = new(); } /// diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusReportService.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusReportService.cs index a3f34c7..6d06925 100644 --- a/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusReportService.cs +++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusReportService.cs @@ -148,7 +148,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status AckWriteFailures = _nodeManager?.AlarmAckWriteFailures ?? 0, FilterEnabled = _nodeManager?.AlarmFilterEnabled ?? false, FilterPatternCount = _nodeManager?.AlarmFilterPatternCount ?? 0, - FilterIncludedObjectCount = _nodeManager?.AlarmFilterIncludedObjectCount ?? 0 + FilterIncludedObjectCount = _nodeManager?.AlarmFilterIncludedObjectCount ?? 0, + FilterPatterns = _nodeManager?.AlarmFilterPatterns?.ToList() ?? new List() }; } @@ -221,8 +222,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status sb.AppendLine( "table { width: 100%; border-collapse: collapse; } th, td { text-align: left; padding: 4px 8px; border-bottom: 1px solid #333; }"); sb.AppendLine("h2 { margin: 0 0 10px 0; } h1 { color: #66ccff; }"); + sb.AppendLine("h1 .version { color: #888; font-size: 0.5em; font-weight: normal; margin-left: 12px; }"); sb.AppendLine(""); - sb.AppendLine("

LmxOpcUa Status Dashboard

"); + sb.AppendLine( + $"

LmxOpcUa Status Dashboardv{WebUtility.HtmlEncode(data.Footer.Version)}

"); // Connection panel var connColor = data.Connection.State == "Connected" ? "green" : @@ -319,8 +322,21 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status sb.AppendLine( $"

Transitions: {data.Alarms.TransitionCount:N0} | Ack Events: {data.Alarms.AckEventCount:N0} | Ack Write Failures: {data.Alarms.AckWriteFailures}

"); if (data.Alarms.FilterEnabled) + { sb.AppendLine( $"

Filter: {data.Alarms.FilterPatternCount} pattern(s), {data.Alarms.FilterIncludedObjectCount} object(s) included

"); + if (data.Alarms.FilterPatterns.Count > 0) + { + sb.AppendLine("
    "); + foreach (var pattern in data.Alarms.FilterPatterns) + sb.AppendLine($"
  • {WebUtility.HtmlEncode(pattern)}
  • "); + sb.AppendLine("
"); + } + } + else + { + sb.AppendLine("

Filter: disabled (all alarm-bearing objects tracked)

"); + } sb.AppendLine(""); // Operations table @@ -336,11 +352,6 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status sb.AppendLine(""); - // Footer - sb.AppendLine("

Footer

"); - sb.AppendLine($"

Generated: {data.Footer.Timestamp:O} | Version: {data.Footer.Version}

"); - sb.AppendLine("
"); - sb.AppendLine(""); return sb.ToString(); } diff --git a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/StatusReportServiceTests.cs b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/StatusReportServiceTests.cs index e55ea59..9ac81b8 100644 --- a/tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/StatusReportServiceTests.cs +++ b/tests/ZB.MOM.WW.LmxOpcUa.Tests/Status/StatusReportServiceTests.cs @@ -29,7 +29,6 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status html.ShouldContain("Subscriptions"); html.ShouldContain("Galaxy Info"); html.ShouldContain("Operations"); - html.ShouldContain("Footer"); } /// @@ -82,15 +81,19 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status } /// - /// Confirms that the footer renders timestamp and version information. + /// The dashboard title shows the service version inline so operators can identify the deployed + /// build without scrolling, and the standalone footer panel is gone. /// [Fact] - public void GenerateHtml_Footer_ContainsTimestampAndVersion() + public void GenerateHtml_Title_ShowsVersion_NoFooter() { var sut = CreateService(); var html = sut.GenerateHtml(); - html.ShouldContain("Generated:"); - html.ShouldContain("Version:"); + + html.ShouldContain("

LmxOpcUa Status Dashboard"); + html.ShouldContain("class='version'"); + html.ShouldNotContain("

Footer

"); + html.ShouldNotContain("Generated:"); } /// @@ -180,6 +183,20 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status json.ShouldContain("FilterEnabled"); json.ShouldContain("FilterPatternCount"); json.ShouldContain("FilterIncludedObjectCount"); + json.ShouldContain("FilterPatterns"); + } + + /// + /// With no filter configured, the Alarms panel renders an explicit "disabled" line so operators + /// know all alarm-bearing objects are being tracked. + /// + [Fact] + public void GenerateHtml_AlarmsPanel_FilterDisabled_ShowsDisabledLine() + { + var sut = CreateService(); + var html = sut.GenerateHtml(); + + html.ShouldContain("Filter: disabled"); } ///