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;
}
}
}