@page "/alarms-historian" @* Live status of the local node's IAlarmHistorianSink (queue depth, drain state) via the HistorianAdapterActor.GetStatus query landed in F11. *@ @attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)] @rendermode RenderMode.InteractiveServer @using Akka.Actor @using Akka.Hosting @using ZB.MOM.WW.OtOpcUa.AdminUI.Clients @using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian @using ZB.MOM.WW.OtOpcUa.Runtime @using ZB.MOM.WW.OtOpcUa.Runtime.Historian @inject IRequiredActor HistorianActor @implements IAsyncDisposable

Alarms historian sink

Polling every @((int)(_interval == TimeSpan.Zero ? PollSeconds : _interval.TotalSeconds)) s.
@if (_reading is { Availability: HistorianSinkAvailability.NotOnThisRole }) {
@_reading.Message Checking again every @((int)HistorianSinkStatusReader.NotRegisteredPollInterval.TotalSeconds) s in case the node is still starting.
} else if (_status is null && _reading is { Availability: HistorianSinkAvailability.Unavailable }) {
@_reading.Message Retrying every @PollSeconds s.
} else if (_status is null) {

Loading…

} else { @if (_reading is { Availability: HistorianSinkAvailability.Unavailable }) {
@_reading.Message Showing the last status read at @_statusReadUtc?.ToString("u").
}
Queue
Depth@_status.QueueDepth
Dead-lettered@_status.DeadLetterDepth
Evicted (lifetime)@_status.EvictedCount
Drain state
State@_status.DrainState
Last drain@(_status.LastDrainUtc?.ToString("u") ?? "—")
Last success@(_status.LastSuccessUtc?.ToString("u") ?? "—")
@if (!string.IsNullOrWhiteSpace(_status.LastError)) {
Last error@DisplayText.Abbreviate(_status.LastError, 80)
}
}
@code { private const int PollSeconds = 5; // The Fleet.razor lifecycle (R04 L3): IAsyncDisposable, a _disposed flag, an overlap guard and a // page-lifetime CTS, so a queued tick after teardown neither Asks nor renders. private HistorianSinkStatus? _status; private DateTime? _statusReadUtc; private HistorianSinkReading? _reading; private Timer? _timer; private int _tickRunning; private TimeSpan _interval; private readonly CancellationTokenSource _cts = new(); private volatile bool _disposed; protected override async Task OnInitializedAsync() { await RefreshAsync(); // "Not registered" is usually permanent (an admin-only node) but can also be a driver node still // starting up, so it keeps a slow re-poll instead of stopping for good (review r0 M6). _interval = HistorianSinkStatusReader.PollIntervalFor(_reading?.Availability); _timer = new Timer(_ => _ = InvokeAsync(RefreshAsync), null, _interval, _interval); } private async Task RefreshAsync() { if (_disposed || Interlocked.Exchange(ref _tickRunning, 1) == 1) return; try { _reading = await HistorianSinkStatusReader.ReadAsync( () => HistorianActor.ActorRef, HistorianSinkStatusReader.AskTimeout, _cts.Token); if (_reading.Status is not null) { _status = _reading.Status; _statusReadUtc = DateTime.UtcNow; } } catch (Exception ex) when (ex is OperationCanceledException or ObjectDisposedException) { return; // the page went away mid-Ask } finally { Volatile.Write(ref _tickRunning, 0); } if (_disposed) return; var interval = HistorianSinkStatusReader.PollIntervalFor(_reading?.Availability); if (_timer is not null && interval != _interval) { _interval = interval; try { _timer.Change(interval, interval); } catch (ObjectDisposedException) { return; } } StateHasChanged(); } private static string StateChipClass(HistorianDrainState state) => state switch { HistorianDrainState.Disabled => "chip chip-idle", HistorianDrainState.Idle => "chip chip-idle", HistorianDrainState.Draining => "chip chip-ok", HistorianDrainState.BackingOff => "chip chip-caution", _ => "chip chip-idle", }; public async ValueTask DisposeAsync() { _disposed = true; _cts.Cancel(); // Drains an in-flight callback (System.Threading.Timer async dispose, .NET 6+). The callback only // schedules the tick, so a tick may still hold _cts — skip the dispose then (see Hosts.razor). if (_timer is not null) await _timer.DisposeAsync(); if (Volatile.Read(ref _tickRunning) == 0) _cts.Dispose(); } }