61193629b6
v2-ci / build (push) Failing after 36s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Both bugs surfaced only on split-role deployments (the MAIN cluster's admin-only nodes), where the AdminUI runs without the driver role. - Test Connect returned "No probe registered" for every driver: the IDriverProbe set was registered only under the driver role, but the admin-operations singleton that consumes it is pinned to admin. Extract AddOtOpcUaDriverProbes() (idempotent via TryAddEnumerable) and call it in the hasAdmin path too. - Live driver-status/alerts/script-log panels showed "SignalR error: Connection refused": these Blazor Server components opened a HubConnection to their own hub via the browser's public URL, which server-side code can't reach behind Traefik (host :9200 -> container :9000). Read the in-process source directly instead -- DriverStatus via IDriverStatusSnapshotStore.SnapshotChanged, Alerts/ScriptLog via a new IInProcessBroadcaster<T>. Fleet status was unaffected (reads DB/ActorSystem). Adds unit tests for probe registration, the snapshot-store event, and the broadcaster.
113 lines
4.4 KiB
Plaintext
113 lines
4.4 KiB
Plaintext
@page "/alerts"
|
|
@* Live alarm tail via SignalR. Subscribes to /hubs/alerts and shows the most-recent
|
|
AlarmTransitionEvent entries published by ScriptedAlarmActor (Runtime/ScriptedAlarms)
|
|
and the AB CIP ALMD bridge. *@
|
|
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
|
@rendermode RenderMode.InteractiveServer
|
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs
|
|
@using ZB.MOM.WW.OtOpcUa.Commons.Messages.Alerts
|
|
@inject IInProcessBroadcaster<AlarmTransitionEvent> Alarms
|
|
@implements IDisposable
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h4 class="mb-0">Alerts</h4>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<span class="conn-pill" data-state="@(_connected ? "connected" : "disconnected")">
|
|
<span class="dot"></span><span>@(_connected ? "live" : "disconnected")</span>
|
|
</span>
|
|
<button class="btn btn-sm btn-outline-secondary" @onclick="ClearAsync">Clear</button>
|
|
</div>
|
|
</div>
|
|
|
|
<section class="panel notice rise" style="animation-delay:.02s">
|
|
Live alarm transitions from the cluster's <span class="mono">alerts</span> DPS topic. Shows
|
|
the most-recent @Capacity entries since the page opened; reload for a fresh window. Sources:
|
|
ScriptedAlarmActor, native driver alarm bridges (AB CIP ALMD, Galaxy where wired).
|
|
</section>
|
|
|
|
@if (_rows.Count == 0)
|
|
{
|
|
<section class="panel notice rise mt-3" style="animation-delay:.08s">
|
|
No alarms in the current window. The table will populate as soon as a
|
|
ScriptedAlarmActor or driver alarm bridge publishes a transition.
|
|
</section>
|
|
}
|
|
else
|
|
{
|
|
<section class="panel rise mt-3" style="animation-delay:.08s">
|
|
<div class="panel-head">Recent transitions (@_rows.Count)</div>
|
|
<div class="table-wrap">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Time</th>
|
|
<th>Alarm</th>
|
|
<th>Equipment</th>
|
|
<th>Kind</th>
|
|
<th class="num">Severity</th>
|
|
<th>User</th>
|
|
<th>Message</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var e in _rows)
|
|
{
|
|
<tr>
|
|
<td><span class="mono small">@e.TimestampUtc.ToString("HH:mm:ss.fff")</span></td>
|
|
<td><span class="mono">@e.AlarmId</span><div class="text-muted small">@e.AlarmName</div></td>
|
|
<td><span class="mono small">@e.EquipmentPath</span></td>
|
|
<td><span class="chip @KindChipClass(e.TransitionKind)">@e.TransitionKind</span></td>
|
|
<td class="num">@e.Severity</td>
|
|
<td>@e.User</td>
|
|
<td>@e.Message</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
@code {
|
|
private const int Capacity = 200;
|
|
|
|
private readonly List<AlarmTransitionEvent> _rows = new();
|
|
private bool _connected;
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
// Live alarm tail straight from the in-process broadcaster (fed by AlertSignalRBridge off the
|
|
// 'alerts' DPS topic). A Blazor Server component can't self-connect a SignalR HubConnection
|
|
// behind a reverse proxy — see IInProcessBroadcaster — so we subscribe in-process instead.
|
|
Alarms.Received += OnAlarm;
|
|
_connected = true;
|
|
}
|
|
|
|
private void OnAlarm(AlarmTransitionEvent evt) =>
|
|
// Marshal both the mutation and the re-render onto the circuit sync context so this can't
|
|
// race ClearAsync (which runs there) over the shared _rows list.
|
|
InvokeAsync(() =>
|
|
{
|
|
_rows.Insert(0, evt);
|
|
if (_rows.Count > Capacity) _rows.RemoveAt(_rows.Count - 1);
|
|
StateHasChanged();
|
|
});
|
|
|
|
private async Task ClearAsync()
|
|
{
|
|
_rows.Clear();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private static string KindChipClass(string kind) => kind switch
|
|
{
|
|
"Activated" => "chip-alert",
|
|
"Cleared" => "chip-ok",
|
|
"Acknowledged" or "Confirmed" => "chip-caution",
|
|
"Shelved" or "Disabled" => "chip-idle",
|
|
_ => "chip-idle",
|
|
};
|
|
|
|
public void Dispose() => Alarms.Received -= OnAlarm;
|
|
}
|