Implement LmxOpcUa server — all 6 phases complete

Full OPC UA server on .NET Framework 4.8 (x86) exposing AVEVA System
Platform Galaxy tags via MXAccess. Mirrors Galaxy object hierarchy as
OPC UA address space, translating contained-name browse paths to
tag-name runtime references.

Components implemented:
- Configuration: AppConfiguration with 4 sections, validator
- Domain: ConnectionState, Quality, Vtq, MxDataTypeMapper, error codes
- MxAccess: StaComThread, MxAccessClient (partial classes), MxProxyAdapter
  using strongly-typed ArchestrA.MxAccess COM interop
- Galaxy Repository: SQL queries (hierarchy, attributes, change detection),
  ChangeDetectionService with auto-rebuild on deploy
- OPC UA Server: LmxNodeManager (CustomNodeManager2), LmxOpcUaServer,
  OpcUaServerHost with programmatic config, SecurityPolicy None
- Status Dashboard: HTTP server with HTML/JSON/health endpoints
- Integration: Full 14-step startup, graceful shutdown, component wiring

175 tests (174 unit + 1 integration), all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-25 05:55:27 -04:00
commit a7576ffb38
283 changed files with 16493 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
using System;
using System.Text;
using System.Text.Json;
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository;
using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
namespace ZB.MOM.WW.LmxOpcUa.Host.Status
{
/// <summary>
/// Aggregates status from all components and generates HTML/JSON reports. (DASH-001 through DASH-009)
/// </summary>
public class StatusReportService
{
private readonly HealthCheckService _healthCheck;
private readonly int _refreshIntervalSeconds;
private IMxAccessClient? _mxAccessClient;
private PerformanceMetrics? _metrics;
private GalaxyRepositoryStats? _galaxyStats;
private OpcUaServerHost? _serverHost;
public StatusReportService(HealthCheckService healthCheck, int refreshIntervalSeconds)
{
_healthCheck = healthCheck;
_refreshIntervalSeconds = refreshIntervalSeconds;
}
public void SetComponents(IMxAccessClient? mxAccessClient, PerformanceMetrics? metrics,
GalaxyRepositoryStats? galaxyStats, OpcUaServerHost? serverHost)
{
_mxAccessClient = mxAccessClient;
_metrics = metrics;
_galaxyStats = galaxyStats;
_serverHost = serverHost;
}
public StatusData GetStatusData()
{
var connectionState = _mxAccessClient?.State ?? ConnectionState.Disconnected;
return new StatusData
{
Connection = new ConnectionInfo
{
State = connectionState.ToString(),
ReconnectCount = _mxAccessClient?.ReconnectCount ?? 0,
ActiveSessions = _serverHost?.ActiveSessionCount ?? 0
},
Health = _healthCheck.CheckHealth(connectionState, _metrics),
Subscriptions = new SubscriptionInfo
{
ActiveCount = _mxAccessClient?.ActiveSubscriptionCount ?? 0
},
Galaxy = new GalaxyInfo
{
GalaxyName = _galaxyStats?.GalaxyName ?? "",
DbConnected = _galaxyStats?.DbConnected ?? false,
LastDeployTime = _galaxyStats?.LastDeployTime,
ObjectCount = _galaxyStats?.ObjectCount ?? 0,
AttributeCount = _galaxyStats?.AttributeCount ?? 0,
LastRebuildTime = _galaxyStats?.LastRebuildTime
},
Operations = _metrics?.GetStatistics() ?? new(),
Footer = new FooterInfo
{
Timestamp = DateTime.UtcNow,
Version = typeof(StatusReportService).Assembly.GetName().Version?.ToString() ?? "1.0.0"
}
};
}
public string GenerateHtml()
{
var data = GetStatusData();
var sb = new StringBuilder();
sb.AppendLine("<!DOCTYPE html><html><head>");
sb.AppendLine($"<meta http-equiv='refresh' content='{_refreshIntervalSeconds}'>");
sb.AppendLine("<title>LmxOpcUa Status</title>");
sb.AppendLine("<style>");
sb.AppendLine("body { font-family: monospace; background: #1a1a2e; color: #eee; padding: 20px; }");
sb.AppendLine(".panel { border: 2px solid #444; border-radius: 8px; padding: 15px; margin: 10px 0; }");
sb.AppendLine(".green { border-color: #00cc66; } .red { border-color: #cc3333; } .yellow { border-color: #cccc33; } .gray { border-color: #666; }");
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("</style></head><body>");
sb.AppendLine("<h1>LmxOpcUa Status Dashboard</h1>");
// Connection panel
var connColor = data.Connection.State == "Connected" ? "green" : data.Connection.State == "Connecting" ? "yellow" : "red";
sb.AppendLine($"<div class='panel {connColor}'><h2>Connection</h2>");
sb.AppendLine($"<p>State: <b>{data.Connection.State}</b> | Reconnects: {data.Connection.ReconnectCount} | Sessions: {data.Connection.ActiveSessions}</p>");
sb.AppendLine("</div>");
// Health panel
sb.AppendLine($"<div class='panel {data.Health.Color}'><h2>Health</h2>");
sb.AppendLine($"<p>Status: <b>{data.Health.Status}</b> — {data.Health.Message}</p>");
sb.AppendLine("</div>");
// Subscriptions panel
sb.AppendLine("<div class='panel gray'><h2>Subscriptions</h2>");
sb.AppendLine($"<p>Active: {data.Subscriptions.ActiveCount}</p>");
sb.AppendLine("</div>");
// Galaxy Info panel
sb.AppendLine("<div class='panel gray'><h2>Galaxy Info</h2>");
sb.AppendLine($"<p>Galaxy: <b>{data.Galaxy.GalaxyName}</b> | DB: {(data.Galaxy.DbConnected ? "Connected" : "Disconnected")}</p>");
sb.AppendLine($"<p>Last Deploy: {data.Galaxy.LastDeployTime:O} | Objects: {data.Galaxy.ObjectCount} | Attributes: {data.Galaxy.AttributeCount}</p>");
sb.AppendLine($"<p>Last Rebuild: {data.Galaxy.LastRebuildTime:O}</p>");
sb.AppendLine("</div>");
// Operations table
sb.AppendLine("<div class='panel gray'><h2>Operations</h2>");
sb.AppendLine("<table><tr><th>Operation</th><th>Count</th><th>Success Rate</th><th>Avg (ms)</th><th>Min (ms)</th><th>Max (ms)</th><th>P95 (ms)</th></tr>");
foreach (var kvp in data.Operations)
{
var s = kvp.Value;
sb.AppendLine($"<tr><td>{kvp.Key}</td><td>{s.TotalCount}</td><td>{s.SuccessRate:P1}</td>" +
$"<td>{s.AverageMilliseconds:F1}</td><td>{s.MinMilliseconds:F1}</td><td>{s.MaxMilliseconds:F1}</td><td>{s.Percentile95Milliseconds:F1}</td></tr>");
}
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>");
return sb.ToString();
}
public string GenerateJson()
{
var data = GetStatusData();
return JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
}
public bool IsHealthy()
{
var state = _mxAccessClient?.State ?? ConnectionState.Disconnected;
return _healthCheck.IsHealthy(state, _metrics);
}
}
}