diff --git a/gateway.md b/gateway.md
index 0665864..087b77f 100644
--- a/gateway.md
+++ b/gateway.md
@@ -188,8 +188,12 @@ used as before.
`OnAlarmTransitionEvent` and `ActiveAlarmSnapshot` proto fields. The
`AlarmFeedMessage` feed emits an `AlarmProviderStatus` message (the
`provider_status` oneof case) on stream open and on every switch. The
-dashboard shows a Bootstrap badge (green for alarm manager, amber when
-degraded). Metrics: `mxgateway.alarms.provider_mode` gauge (1 = alarmmgr,
+dashboard shows a Bootstrap badge: green ("Alarm Manager") when healthy, amber
+("Subtag monitoring (degraded)") on an unexpected failover, and cyan ("Subtag
+monitoring (forced)") when subtag mode is the configured `Fallback:Mode=ForceSubtag`
+— the latter distinguished by the well-known `AlarmProviderStatus.reason`
+(`AlarmProviderReasons.ForcedSubtag`) so an intentional configuration is not shown
+as a fault. Metrics: `mxgateway.alarms.provider_mode` gauge (1 = alarmmgr,
2 = subtag) and `mxgateway.alarms.provider_switches` counter.
Forced modes are available via `MxGateway:Alarms:Fallback:Mode`:
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Alarms/AlarmProviderReasons.cs b/src/ZB.MOM.WW.MxGateway.Server/Alarms/AlarmProviderReasons.cs
new file mode 100644
index 0000000..045f3aa
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Server/Alarms/AlarmProviderReasons.cs
@@ -0,0 +1,18 @@
+namespace ZB.MOM.WW.MxGateway.Server.Alarms;
+
+///
+/// Well-known reason strings carried on the alarm feed's
+/// AlarmProviderStatus message. Shared between the producer
+/// () and consumers (e.g. the dashboard
+/// provider badge) so the two cannot drift on a magic string.
+///
+public static class AlarmProviderReasons
+{
+ ///
+ /// Reason set when the monitor starts in subtag mode because
+ /// MxGateway:Alarms:Fallback:Mode is ForceSubtag — a
+ /// deliberate configuration, not a runtime failover. Lets the dashboard
+ /// distinguish a forced subtag mode from an unexpected degraded failover.
+ ///
+ public const string ForcedSubtag = "Forced subtag mode (configuration)";
+}
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs b/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs
index d4e6253..ef07c9d 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs
@@ -170,7 +170,7 @@ public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmServic
case AlarmProviderMode.Subtag:
initialMode = AlarmProviderMode.Subtag;
initialDegraded = true;
- initialReason = "Forced subtag mode (configuration)";
+ initialReason = AlarmProviderReasons.ForcedSubtag;
break;
case AlarmProviderMode.Alarmmgr:
initialMode = AlarmProviderMode.Alarmmgr;
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAlarmProviderStatus.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAlarmProviderStatus.cs
index 37251b0..de035b1 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAlarmProviderStatus.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAlarmProviderStatus.cs
@@ -1,4 +1,5 @@
using ZB.MOM.WW.MxGateway.Contracts.Proto;
+using ZB.MOM.WW.MxGateway.Server.Alarms;
namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
@@ -22,9 +23,20 @@ public sealed record DashboardAlarmProviderStatus(
/// Badge label shown when the feed has fallen back to subtag monitoring.
public const string DegradedLabel = "Subtag monitoring (degraded)";
+ ///
+ /// Badge label shown when the feed is in subtag monitoring because it was
+ /// deliberately configured (Fallback:Mode=ForceSubtag), as opposed
+ /// to an unexpected failover. A stable, intended state rather than a fault.
+ ///
+ public const string ForcedSubtagLabel = "Subtag monitoring (forced)";
+
private const string HealthyBadge = "bg-success";
private const string DegradedBadge = "bg-warning text-dark";
+ // Cyan/info badge: visually distinct from the amber failover-degraded badge —
+ // forced subtag is an intentional configuration, not an alarm-manager fault.
+ private const string ForcedSubtagBadge = "bg-info text-dark";
+
///
/// The default status assumed before the first provider-status message
/// arrives: healthy alarm-manager mode.
@@ -48,13 +60,24 @@ public sealed record DashboardAlarmProviderStatus(
// the contract sets degraded=true whenever mode == SUBTAG, but guard
// against either being set independently.
bool degraded = status.Degraded || status.Mode == AlarmProviderMode.Subtag;
+ string reason = status.Reason ?? string.Empty;
+
+ // A configured ForceSubtag start carries the well-known forced reason and
+ // is a deliberate mode, not a failover — render it distinctly so an
+ // operator isn't alarmed by a "(degraded)" badge for an intended config.
+ bool forced = degraded
+ && status.Mode == AlarmProviderMode.Subtag
+ && string.Equals(reason, AlarmProviderReasons.ForcedSubtag, StringComparison.Ordinal);
+
+ string label = !degraded ? AlarmManagerLabel : forced ? ForcedSubtagLabel : DegradedLabel;
+ string badge = !degraded ? HealthyBadge : forced ? ForcedSubtagBadge : DegradedBadge;
return new DashboardAlarmProviderStatus(
Mode: status.Mode,
IsDegraded: degraded,
- Label: degraded ? DegradedLabel : AlarmManagerLabel,
- BadgeCssClass: degraded ? DegradedBadge : HealthyBadge,
- Reason: status.Reason ?? string.Empty,
+ Label: label,
+ BadgeCssClass: badge,
+ Reason: reason,
SinceUtc: status.Since?.ToDateTimeOffset());
}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs
index 46a7d29..1709cc8 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs
@@ -172,6 +172,28 @@ public sealed class DashboardBrowseAndAlarmModelTests
Assert.Equal("x", model.Reason);
}
+ ///
+ /// Verifies that a configured forced-subtag provider status renders the
+ /// distinct "forced" badge (cyan/info), not the amber failover-degraded one.
+ ///
+ [Fact]
+ public void FromProviderStatus_Subtag_ForcedReason_ForcedBadge()
+ {
+ AlarmProviderStatus status = new()
+ {
+ Mode = AlarmProviderMode.Subtag,
+ Degraded = true,
+ Reason = ZB.MOM.WW.MxGateway.Server.Alarms.AlarmProviderReasons.ForcedSubtag,
+ };
+
+ DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status);
+
+ Assert.True(model.IsDegraded);
+ Assert.Equal(DashboardAlarmProviderStatus.ForcedSubtagLabel, model.Label);
+ Assert.Contains("bg-info", model.BadgeCssClass, StringComparison.Ordinal);
+ Assert.DoesNotContain("bg-warning", model.BadgeCssClass, StringComparison.Ordinal);
+ }
+
/// Verifies that the formatter renders array elements and element type correctly.
[Fact]
public void FormatValue_AndDataType_RenderArrayElementsAndElementType()