Promote service version into the dashboard title and surface the active alarm filter patterns in the Alarms panel so operators can verify scope at a glance without reading logs or the footer block
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -93,6 +93,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
|||||||
public IReadOnlyList<string> UnmatchedPatterns =>
|
public IReadOnlyList<string> UnmatchedPatterns =>
|
||||||
_rawPatterns.Where(p => !_matchedRawPatterns.Contains(p)).ToList();
|
_rawPatterns.Where(p => !_matchedRawPatterns.Contains(p)).ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> RawPatterns => _rawPatterns;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns <see langword="true"/> when any template name in <paramref name="chain"/> matches any
|
/// Returns <see langword="true"/> when any template name in <paramref name="chain"/> matches any
|
||||||
/// compiled pattern. An empty chain never matches unless the operator explicitly supplied a pattern
|
/// compiled pattern. An empty chain never matches unless the operator explicitly supplied a pattern
|
||||||
|
|||||||
@@ -183,6 +183,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int AlarmFilterIncludedObjectCount => _alarmFilterIncludedObjectCount;
|
public int AlarmFilterIncludedObjectCount => _alarmFilterIncludedObjectCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the raw alarm filter patterns exactly as configured, for display on the status dashboard.
|
||||||
|
/// Returns an empty list when no filter is active.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> AlarmFilterPatterns =>
|
||||||
|
_alarmObjectFilter?.RawPatterns ?? Array.Empty<string>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the number of distinct alarm conditions currently tracked (one per alarm attribute).
|
/// Gets the number of distinct alarm conditions currently tracked (one per alarm attribute).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -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.
|
/// Gets or sets the number of Galaxy objects included by the alarm filter during the most recent build.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int FilterIncludedObjectCount { get; set; }
|
public int FilterIncludedObjectCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the raw alarm filter patterns exactly as configured, for dashboard display.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> FilterPatterns { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
|||||||
AckWriteFailures = _nodeManager?.AlarmAckWriteFailures ?? 0,
|
AckWriteFailures = _nodeManager?.AlarmAckWriteFailures ?? 0,
|
||||||
FilterEnabled = _nodeManager?.AlarmFilterEnabled ?? false,
|
FilterEnabled = _nodeManager?.AlarmFilterEnabled ?? false,
|
||||||
FilterPatternCount = _nodeManager?.AlarmFilterPatternCount ?? 0,
|
FilterPatternCount = _nodeManager?.AlarmFilterPatternCount ?? 0,
|
||||||
FilterIncludedObjectCount = _nodeManager?.AlarmFilterIncludedObjectCount ?? 0
|
FilterIncludedObjectCount = _nodeManager?.AlarmFilterIncludedObjectCount ?? 0,
|
||||||
|
FilterPatterns = _nodeManager?.AlarmFilterPatterns?.ToList() ?? new List<string>()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,8 +222,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
|||||||
sb.AppendLine(
|
sb.AppendLine(
|
||||||
"table { width: 100%; border-collapse: collapse; } th, td { text-align: left; padding: 4px 8px; border-bottom: 1px solid #333; }");
|
"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("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("</style></head><body>");
|
sb.AppendLine("</style></head><body>");
|
||||||
sb.AppendLine("<h1>LmxOpcUa Status Dashboard</h1>");
|
sb.AppendLine(
|
||||||
|
$"<h1>LmxOpcUa Status Dashboard<span class='version'>v{WebUtility.HtmlEncode(data.Footer.Version)}</span></h1>");
|
||||||
|
|
||||||
// Connection panel
|
// Connection panel
|
||||||
var connColor = data.Connection.State == "Connected" ? "green" :
|
var connColor = data.Connection.State == "Connected" ? "green" :
|
||||||
@@ -319,8 +322,21 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
|||||||
sb.AppendLine(
|
sb.AppendLine(
|
||||||
$"<p>Transitions: {data.Alarms.TransitionCount:N0} | Ack Events: {data.Alarms.AckEventCount:N0} | Ack Write Failures: {data.Alarms.AckWriteFailures}</p>");
|
$"<p>Transitions: {data.Alarms.TransitionCount:N0} | Ack Events: {data.Alarms.AckEventCount:N0} | Ack Write Failures: {data.Alarms.AckWriteFailures}</p>");
|
||||||
if (data.Alarms.FilterEnabled)
|
if (data.Alarms.FilterEnabled)
|
||||||
|
{
|
||||||
sb.AppendLine(
|
sb.AppendLine(
|
||||||
$"<p>Filter: <b>{data.Alarms.FilterPatternCount}</b> pattern(s), <b>{data.Alarms.FilterIncludedObjectCount}</b> object(s) included</p>");
|
$"<p>Filter: <b>{data.Alarms.FilterPatternCount}</b> pattern(s), <b>{data.Alarms.FilterIncludedObjectCount}</b> object(s) included</p>");
|
||||||
|
if (data.Alarms.FilterPatterns.Count > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine("<ul>");
|
||||||
|
foreach (var pattern in data.Alarms.FilterPatterns)
|
||||||
|
sb.AppendLine($"<li><code>{WebUtility.HtmlEncode(pattern)}</code></li>");
|
||||||
|
sb.AppendLine("</ul>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.AppendLine("<p>Filter: <b>disabled</b> (all alarm-bearing objects tracked)</p>");
|
||||||
|
}
|
||||||
sb.AppendLine("</div>");
|
sb.AppendLine("</div>");
|
||||||
|
|
||||||
// Operations table
|
// Operations table
|
||||||
@@ -336,11 +352,6 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
|||||||
|
|
||||||
sb.AppendLine("</table></div>");
|
sb.AppendLine("</table></div>");
|
||||||
|
|
||||||
// Footer
|
|
||||||
sb.AppendLine("<div class='panel gray'><h2>Footer</h2>");
|
|
||||||
sb.AppendLine($"<p>Generated: {data.Footer.Timestamp:O} | Version: {data.Footer.Version}</p>");
|
|
||||||
sb.AppendLine("</div>");
|
|
||||||
|
|
||||||
sb.AppendLine("</body></html>");
|
sb.AppendLine("</body></html>");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
|||||||
html.ShouldContain("Subscriptions");
|
html.ShouldContain("Subscriptions");
|
||||||
html.ShouldContain("Galaxy Info");
|
html.ShouldContain("Galaxy Info");
|
||||||
html.ShouldContain("Operations");
|
html.ShouldContain("Operations");
|
||||||
html.ShouldContain("Footer");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -82,15 +81,19 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GenerateHtml_Footer_ContainsTimestampAndVersion()
|
public void GenerateHtml_Title_ShowsVersion_NoFooter()
|
||||||
{
|
{
|
||||||
var sut = CreateService();
|
var sut = CreateService();
|
||||||
var html = sut.GenerateHtml();
|
var html = sut.GenerateHtml();
|
||||||
html.ShouldContain("Generated:");
|
|
||||||
html.ShouldContain("Version:");
|
html.ShouldContain("<h1>LmxOpcUa Status Dashboard");
|
||||||
|
html.ShouldContain("class='version'");
|
||||||
|
html.ShouldNotContain("<h2>Footer</h2>");
|
||||||
|
html.ShouldNotContain("Generated:");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -180,6 +183,20 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
|||||||
json.ShouldContain("FilterEnabled");
|
json.ShouldContain("FilterEnabled");
|
||||||
json.ShouldContain("FilterPatternCount");
|
json.ShouldContain("FilterPatternCount");
|
||||||
json.ShouldContain("FilterIncludedObjectCount");
|
json.ShouldContain("FilterIncludedObjectCount");
|
||||||
|
json.ShouldContain("FilterPatterns");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// With no filter configured, the Alarms panel renders an explicit "disabled" line so operators
|
||||||
|
/// know all alarm-bearing objects are being tracked.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void GenerateHtml_AlarmsPanel_FilterDisabled_ShowsDisabledLine()
|
||||||
|
{
|
||||||
|
var sut = CreateService();
|
||||||
|
var html = sut.GenerateHtml();
|
||||||
|
|
||||||
|
html.ShouldContain("Filter: <b>disabled</b>");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user