using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Options; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Server.Alarms; using ZB.MOM.WW.MxGateway.Server.Configuration; namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs; /// /// Background service that subscribes to /// (no filter) and re-broadcasts /// every to every /// client. The hub itself is session-less; clients filter / route messages /// in the browser. /// /// /// When MxGateway:Dashboard:ShowTagValues is false (the default), the /// alarm value fields are stripped from a redacted copy before the message /// reaches any browser client — the same rule /// applies to the events-hub mirror, so /// the two SignalR seams cannot disagree about whether values leave the gateway. /// The source message is never mutated: it fans out from one feed to the gRPC /// StreamAlarms subscribers and the alarms page as well, and none of /// those audiences is subject to this dashboard-display flag. /// /// The broadcast stays deliberately unfiltered. Audience is decided once, at hub /// admission — carries /// , so only an authenticated caller /// with a real dashboard role is ever in — and the alarm /// monitor is gateway-central with no per-session provenance, so there is no per-connection /// narrowing this loop could apply even in principle. Adding a filter here would imply a /// per-caller scope the feed does not carry. /// /// /// The gateway's central alarm feed. /// Hub context used to broadcast to the alarms group. /// Gateway options supplying Dashboard:ShowTagValues. /// Logger for best-effort broadcast failures. public sealed class AlarmsHubPublisher( IGatewayAlarmService alarmService, IHubContext hubContext, IOptions options, ILogger logger) : BackgroundService { private readonly bool _showTagValues = options.Value.Dashboard.ShowTagValues; /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Loop forever — when StreamAsync completes (monitor restart, etc.) // reopen the subscription. The hosted-service lifetime ends only // when the host stops. while (!stoppingToken.IsCancellationRequested) { try { await foreach (AlarmFeedMessage message in alarmService .StreamAsync(alarmFilterPrefix: null, stoppingToken) .ConfigureAwait(false)) { if (stoppingToken.IsCancellationRequested) { break; } try { AlarmFeedMessage outbound = _showTagValues ? message : RedactValues(message); await hubContext.Clients .Group(AlarmsHub.AllAlarmsGroup) .SendAsync(AlarmsHub.AlarmMessage, outbound, stoppingToken) .ConfigureAwait(false); } catch (Exception ex) when (ex is not OperationCanceledException) { logger.LogWarning(ex, "Alarm broadcast failed; continuing."); } } } catch (OperationCanceledException) { return; } catch (Exception ex) { logger.LogWarning(ex, "Alarm subscription faulted; reconnecting in 5s."); try { await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken).ConfigureAwait(false); } catch (OperationCanceledException) { return; } } } } /// /// Produces a copy of with the alarm value fields /// cleared, leaving every other field — reference, severity, state, category, /// operator, timestamps — intact so the alarms page still renders the row. /// /// The message as the alarm feed produced it. /// /// A redacted deep clone for the two value-bearing payload arms; the source /// instance itself for every other arm, which carries no value to strip. /// New payload arms therefore pass through unchanged by default — the switch /// names only the arms that have something to redact, so adding a valueless /// arm to the contract needs no change here. /// private static AlarmFeedMessage RedactValues(AlarmFeedMessage source) { switch (source.PayloadCase) { case AlarmFeedMessage.PayloadOneofCase.Transition: { AlarmFeedMessage redacted = source.Clone(); redacted.Transition.CurrentValue = null; redacted.Transition.LimitValue = null; return redacted; } case AlarmFeedMessage.PayloadOneofCase.ActiveAlarm: { AlarmFeedMessage redacted = source.Clone(); redacted.ActiveAlarm.CurrentValue = null; redacted.ActiveAlarm.LimitValue = null; return redacted; } default: return source; } } }