(_redundancyConfig.ServerUris)
};
}
///
/// Generates the operator-facing HTML dashboard for the current bridge status.
///
/// An HTML document containing the latest dashboard snapshot.
public string GenerateHtml()
{
var data = GetStatusData();
var sb = new StringBuilder();
sb.AppendLine("");
sb.AppendLine("");
sb.AppendLine($"");
sb.AppendLine("LmxOpcUa Status");
sb.AppendLine("");
sb.AppendLine(
$"LmxOpcUa Status Dashboardv{WebUtility.HtmlEncode(data.Footer.Version)}
");
// Connection panel
var connColor = data.Connection.State == "Connected" ? "green" :
data.Connection.State == "Connecting" ? "yellow" : "red";
sb.AppendLine($"Connection
");
sb.AppendLine(
$"
State: {data.Connection.State} | Reconnects: {data.Connection.ReconnectCount} | Sessions: {data.Connection.ActiveSessions}
");
sb.AppendLine("
");
// Health panel
sb.AppendLine($"Health
");
sb.AppendLine($"
Status: {data.Health.Status} — {data.Health.Message}
");
sb.AppendLine("
");
// Endpoints panel (exposed URLs + security profiles)
var endpointsColor = data.Endpoints.BaseAddresses.Count > 0 ? "green" : "gray";
sb.AppendLine($"Endpoints
");
if (data.Endpoints.BaseAddresses.Count == 0)
{
sb.AppendLine("
No endpoints — OPC UA server not started.
");
}
else
{
sb.AppendLine("
Base Addresses:
");
foreach (var addr in data.Endpoints.BaseAddresses)
sb.AppendLine($"- {WebUtility.HtmlEncode(addr)}
");
sb.AppendLine("
");
sb.AppendLine("
Security Profiles:
");
sb.AppendLine("
| Mode | Policy | Policy URI |
");
foreach (var profile in data.Endpoints.SecurityProfiles)
{
sb.AppendLine(
$"| {WebUtility.HtmlEncode(profile.SecurityMode)} | " +
$"{WebUtility.HtmlEncode(profile.PolicyName)} | " +
$"{WebUtility.HtmlEncode(profile.PolicyUri)} |
");
}
sb.AppendLine("
");
if (data.Endpoints.UserTokenPolicies.Count > 0)
sb.AppendLine(
$"
User Token Policies: {WebUtility.HtmlEncode(string.Join(", ", data.Endpoints.UserTokenPolicies))}
");
}
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}
");
if (data.Subscriptions.ProbeCount > 0)
sb.AppendLine(
$"
Probes: {data.Subscriptions.ProbeCount} (bridge-owned runtime status)
");
sb.AppendLine("
");
// Data Change Dispatch panel
sb.AppendLine("Data Change Dispatch
");
sb.AppendLine(
$"
Events/sec: {data.DataChange.EventsPerSecond:F1} | Avg Batch Size: {data.DataChange.AvgBatchSize:F1} | Pending: {data.DataChange.PendingItems} | Total Events: {data.DataChange.TotalEvents:N0}
");
sb.AppendLine("
");
// Galaxy Info panel
sb.AppendLine("Galaxy Info
");
sb.AppendLine(
$"
Galaxy: {data.Galaxy.GalaxyName} | DB: {(data.Galaxy.DbConnected ? "Connected" : "Disconnected")}
");
sb.AppendLine(
$"
Last Deploy: {data.Galaxy.LastDeployTime:O} | Objects: {data.Galaxy.ObjectCount} | Attributes: {data.Galaxy.AttributeCount}
");
sb.AppendLine($"
Last Rebuild: {data.Galaxy.LastRebuildTime:O}
");
sb.AppendLine("
");
// Galaxy Runtime panel — per-host Platform + AppEngine state
if (data.RuntimeStatus.Total > 0)
{
var rtColor = data.RuntimeStatus.StoppedCount > 0 ? "red"
: data.RuntimeStatus.UnknownCount > 0 ? "yellow"
: "green";
sb.AppendLine($"Galaxy Runtime
");
sb.AppendLine(
$"
{data.RuntimeStatus.RunningCount} of {data.RuntimeStatus.Total} hosts running" +
$" ({data.RuntimeStatus.StoppedCount} stopped, {data.RuntimeStatus.UnknownCount} unknown)
");
sb.AppendLine("
| Name | Kind | State | Since | Last Error |
");
foreach (var host in data.RuntimeStatus.Hosts)
{
var since = host.LastStateChangeTime?.ToString("O") ?? "-";
var err = WebUtility.HtmlEncode(host.LastError ?? "");
sb.AppendLine(
$"| {WebUtility.HtmlEncode(host.ObjectName)} | " +
$"{WebUtility.HtmlEncode(host.Kind)} | " +
$"{host.State} | " +
$"{since} | " +
$"{err} |
");
}
sb.AppendLine("
");
sb.AppendLine("
");
}
// Historian panel
var anyClusterNodeFailed =
data.Historian.NodeCount > 0 && data.Historian.HealthyNodeCount < data.Historian.NodeCount;
var allClusterNodesFailed =
data.Historian.NodeCount > 0 && data.Historian.HealthyNodeCount == 0;
var histColor = !data.Historian.Enabled ? "gray"
: data.Historian.PluginStatus != "Loaded" ? "red"
: allClusterNodesFailed ? "red"
: data.Historian.ConsecutiveFailures >= 5 ? "red"
: anyClusterNodeFailed || data.Historian.ConsecutiveFailures > 0 ? "yellow"
: "green";
sb.AppendLine($"Historian
");
sb.AppendLine(
$"
Enabled: {data.Historian.Enabled} | Plugin: {data.Historian.PluginStatus} | Port: {data.Historian.Port}
");
if (!string.IsNullOrEmpty(data.Historian.PluginError))
sb.AppendLine($"
Plugin Error: {WebUtility.HtmlEncode(data.Historian.PluginError)}
");
if (data.Historian.PluginStatus == "Loaded")
{
sb.AppendLine(
$"
Queries: {data.Historian.QueryTotal:N0} " +
$"(Success: {data.Historian.QuerySuccesses:N0}, Failure: {data.Historian.QueryFailures:N0}) " +
$"| Consecutive Failures: {data.Historian.ConsecutiveFailures}
");
var procBadge = data.Historian.ProcessConnectionOpen
? $"open ({WebUtility.HtmlEncode(data.Historian.ActiveProcessNode ?? "?")})"
: "closed";
var evtBadge = data.Historian.EventConnectionOpen
? $"open ({WebUtility.HtmlEncode(data.Historian.ActiveEventNode ?? "?")})"
: "closed";
sb.AppendLine(
$"
Process Conn: {procBadge} | Event Conn: {evtBadge}
");
if (data.Historian.LastSuccessTime.HasValue)
sb.AppendLine($"
Last Success: {data.Historian.LastSuccessTime:O}
");
if (data.Historian.LastFailureTime.HasValue)
sb.AppendLine($"
Last Failure: {data.Historian.LastFailureTime:O}
");
if (!string.IsNullOrEmpty(data.Historian.LastQueryError))
sb.AppendLine(
$"
Last Error: {WebUtility.HtmlEncode(data.Historian.LastQueryError)}
");
// Cluster table: only when a true multi-node cluster is configured.
if (data.Historian.NodeCount > 1)
{
sb.AppendLine(
$"
Cluster: {data.Historian.HealthyNodeCount} of {data.Historian.NodeCount} nodes healthy
");
sb.AppendLine(
"
| Node | State | Cooldown Until | Failures | Last Error |
");
foreach (var node in data.Historian.Nodes)
{
var state = node.IsHealthy ? "healthy" : "cooldown";
var cooldown = node.CooldownUntil?.ToString("O") ?? "-";
var lastErr = WebUtility.HtmlEncode(node.LastError ?? "");
sb.AppendLine(
$"| {WebUtility.HtmlEncode(node.Name)} | {state} | " +
$"{cooldown} | {node.FailureCount} | {lastErr} |
");
}
sb.AppendLine("
");
}
else if (data.Historian.NodeCount == 1)
{
sb.AppendLine($"
Node: {WebUtility.HtmlEncode(data.Historian.Nodes[0].Name)}
");
}
}
sb.AppendLine("
");
// Alarms panel
var alarmPanelColor = !data.Alarms.TrackingEnabled ? "gray"
: data.Alarms.AckWriteFailures > 0 ? "yellow" : "green";
sb.AppendLine($"Alarms
");
sb.AppendLine(
$"
Tracking: {data.Alarms.TrackingEnabled} | Conditions: {data.Alarms.ConditionCount} | Active: {data.Alarms.ActiveAlarmCount}
");
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
sb.AppendLine("Operations
");
sb.AppendLine(
"
| Operation | Count | Success Rate | Avg (ms) | Min (ms) | Max (ms) | P95 (ms) |
");
foreach (var kvp in data.Operations)
{
var s = kvp.Value;
sb.AppendLine($"| {kvp.Key} | {s.TotalCount} | {s.SuccessRate:P1} | " +
$"{s.AverageMilliseconds:F1} | {s.MinMilliseconds:F1} | {s.MaxMilliseconds:F1} | {s.Percentile95Milliseconds:F1} |
");
}
sb.AppendLine("
");
sb.AppendLine("");
return sb.ToString();
}
///
/// Generates an indented JSON status payload for API consumers.
///
/// A JSON representation of the current dashboard snapshot.
public string GenerateJson()
{
var data = GetStatusData();
return JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
}
///
/// Determines whether the bridge should currently be considered healthy for the dashboard health endpoint.
///
/// when the bridge meets the health policy; otherwise, .
public bool IsHealthy()
{
var state = _mxAccessClient?.State ?? ConnectionState.Disconnected;
return _healthCheck.IsHealthy(state, _metrics);
}
///
/// Builds the rich health endpoint data including component health, ServiceLevel, and redundancy state.
///
public HealthEndpointData GetHealthData()
{
var connectionState = _mxAccessClient?.State ?? ConnectionState.Disconnected;
var mxConnected = connectionState == ConnectionState.Connected;
var dbConnected = _galaxyStats?.DbConnected ?? false;
var historianInfo = BuildHistorianStatusInfo();
var alarmInfo = BuildAlarmStatusInfo();
var health = _healthCheck.CheckHealth(connectionState, _metrics, historianInfo, alarmInfo);
var uptime = DateTime.UtcNow - _startTime;
var data = new HealthEndpointData
{
Status = health.Status,
RedundancyEnabled = _redundancyConfig?.Enabled ?? false,
Components = new ComponentHealth
{
MxAccess = connectionState.ToString(),
Database = dbConnected ? "Connected" : "Disconnected",
OpcUaServer = _serverHost?.IsRunning ?? false ? "Running" : "Stopped",
Historian = historianInfo.PluginStatus,
Alarms = alarmInfo.TrackingEnabled ? "Enabled" : "Disabled"
},
Uptime = FormatUptime(uptime),
Timestamp = DateTime.UtcNow
};
if (_redundancyConfig != null && _redundancyConfig.Enabled)
{
var isPrimary = string.Equals(_redundancyConfig.Role, "Primary", StringComparison.OrdinalIgnoreCase);
var baseLevel = isPrimary
? _redundancyConfig.ServiceLevelBase
: Math.Max(0, _redundancyConfig.ServiceLevelBase - 50);
var calculator = new ServiceLevelCalculator();
data.ServiceLevel = calculator.Calculate(baseLevel, mxConnected, dbConnected);
data.RedundancyRole = _redundancyConfig.Role;
data.RedundancyMode = _redundancyConfig.Mode;
}
else
{
// Non-redundant: 255 when healthy, 0 when both down
data.ServiceLevel = mxConnected ? (byte)255 : (byte)0;
}
return data;
}
///
/// Generates the JSON payload for the /api/health endpoint.
///
public string GenerateHealthJson()
{
var data = GetHealthData();
return JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
}
///
/// Generates a focused health status HTML page for operators and monitoring dashboards.
///
public string GenerateHealthHtml()
{
var data = GetHealthData();
var sb = new StringBuilder();
var statusColor = data.Status == "Healthy" ? "#00cc66" : data.Status == "Degraded" ? "#cccc33" : "#cc3333";
var mxColor = data.Components.MxAccess == "Connected" ? "#00cc66" : "#cc3333";
var dbColor = data.Components.Database == "Connected" ? "#00cc66" : "#cc3333";
var uaColor = data.Components.OpcUaServer == "Running" ? "#00cc66" : "#cc3333";
sb.AppendLine("");
sb.AppendLine("");
sb.AppendLine($"");
sb.AppendLine("LmxOpcUa Health");
sb.AppendLine("");
// Status badge
sb.AppendLine("");
// Service Level
sb.AppendLine($"");
sb.AppendLine("SERVICE LEVEL");
sb.AppendLine($"{data.ServiceLevel}");
sb.AppendLine("
");
// Redundancy info
if (data.RedundancyEnabled)
sb.AppendLine(
$"Role: {data.RedundancyRole} | Mode: {data.RedundancyMode}
");
var historianColor = data.Components.Historian == "Loaded" ? "#00cc66"
: data.Components.Historian == "Disabled" ? "#666" : "#cc3333";
var alarmColor = data.Components.Alarms == "Enabled" ? "#00cc66" : "#666";
// Component health cards
sb.AppendLine("");
sb.AppendLine(
$"
MXAccess
{data.Components.MxAccess}
");
sb.AppendLine(
$"
Galaxy Database
{data.Components.Database}
");
sb.AppendLine(
$"
OPC UA Server
{data.Components.OpcUaServer}
");
sb.AppendLine(
$"
Historian
{data.Components.Historian}
");
sb.AppendLine(
$"
Alarm Tracking
{data.Components.Alarms}
");
sb.AppendLine("
");
// Footer
sb.AppendLine($"Uptime: {data.Uptime} | {data.Timestamp:O}
");
sb.AppendLine("");
return sb.ToString();
}
private static string FormatUptime(TimeSpan ts)
{
if (ts.TotalDays >= 1)
return $"{(int)ts.TotalDays}d {ts.Hours}h {ts.Minutes}m";
if (ts.TotalHours >= 1)
return $"{(int)ts.TotalHours}h {ts.Minutes}m";
return $"{(int)ts.TotalMinutes}m {ts.Seconds}s";
}
}
}