74161f9460
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been cancelled
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been cancelled
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been cancelled
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been cancelled
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been cancelled
v2-ci / integration (push) Has been cancelled
v2-ci / build (push) Has been cancelled
- ClusterAudit (/clusters/{id}/audit) — reads ConfigAuditLog with the
EventId/CorrelationId columns added in F3; shown as a Cluster tab
- VirtualTags (/virtual-tags) — fleet-wide read view
- ScriptedAlarms (/scripted-alarms) — fleet-wide read view
- Scripts (/scripts) — fleet-wide; expandable code preview
- RoleGrants (/role-grants) — per Q4, surfaces the fleet-wide
LDAP-group → role mapping from
Authentication:Ldap:GroupToRole
(read-only; reload via host restart)
- Certificates (/certificates) — own/trusted/issuer/rejected store
contents resolved against
OpcUa:PkiStoreRoot config (F13a)
- Reservations (/reservations) — ExternalIdReservation table
- AlarmsHistorian (/alarms-historian) — live HistorianAdapterActor sink
status via the F11 GetStatus query;
5s polling
ScriptLog deferred (needs the F16-deferred ScriptLogHub bridge).
ClusterNav extended with the Audit tab.
Adds an AdminUI → Runtime project reference so the historian status page can
inject IRequiredActor<HistorianAdapterActorKey>. NuGet audit suppression for
the transitive Opc.Ua.Core advisory mirrored from the Runtime project.
All 104 v2 tests still green.
92 lines
3.7 KiB
Plaintext
92 lines
3.7 KiB
Plaintext
@page "/alarms-historian"
|
|
@* Live status of the local node's IAlarmHistorianSink (queue depth, drain state) via the
|
|
HistorianAdapterActor.GetStatus query landed in F11. *@
|
|
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
|
@rendermode RenderMode.InteractiveServer
|
|
@using Akka.Actor
|
|
@using Akka.Hosting
|
|
@using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian
|
|
@using ZB.MOM.WW.OtOpcUa.Runtime
|
|
@using ZB.MOM.WW.OtOpcUa.Runtime.Historian
|
|
@inject IRequiredActor<HistorianAdapterActorKey> HistorianActor
|
|
@implements IDisposable
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h4 class="mb-0">Alarms historian sink</h4>
|
|
</div>
|
|
|
|
<section class="panel notice rise" style="animation-delay:.02s">
|
|
Snapshot from the local node's <span class="mono">HistorianAdapterActor</span>. Default sink
|
|
is a no-op (<span class="mono">NullAlarmHistorianSink</span>); production wires
|
|
<span class="mono">SqliteStoreAndForwardSink</span> with the Wonderware historian sidecar
|
|
behind it. Polling every @PollSeconds s.
|
|
</section>
|
|
|
|
@if (_status is null)
|
|
{
|
|
<p class="mt-3">Loading…</p>
|
|
}
|
|
else
|
|
{
|
|
<section class="card-grid rise mt-3" style="animation-delay:.08s">
|
|
<div class="metric-card">
|
|
<div class="panel-head">Queue</div>
|
|
<div class="kv"><span class="k">Depth</span><span class="v numeric">@_status.QueueDepth</span></div>
|
|
<div class="kv"><span class="k">Dead-lettered</span><span class="v numeric">@_status.DeadLetterDepth</span></div>
|
|
<div class="kv"><span class="k">Evicted (lifetime)</span><span class="v numeric">@_status.EvictedCount</span></div>
|
|
</div>
|
|
|
|
<div class="metric-card">
|
|
<div class="panel-head">Drain state</div>
|
|
<div class="kv"><span class="k">State</span><span class="v"><span class="@StateChipClass(_status.DrainState)">@_status.DrainState</span></span></div>
|
|
<div class="kv"><span class="k">Last drain</span><span class="v">@(_status.LastDrainUtc?.ToString("u") ?? "—")</span></div>
|
|
<div class="kv"><span class="k">Last success</span><span class="v">@(_status.LastSuccessUtc?.ToString("u") ?? "—")</span></div>
|
|
@if (!string.IsNullOrWhiteSpace(_status.LastError))
|
|
{
|
|
<div class="kv"><span class="k">Last error</span><span class="v text-danger small">@_status.LastError</span></div>
|
|
}
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
@code {
|
|
private const int PollSeconds = 5;
|
|
|
|
private HistorianSinkStatus? _status;
|
|
private Timer? _timer;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await RefreshAsync();
|
|
_timer = new Timer(_ => _ = InvokeAsync(RefreshAsync), null,
|
|
TimeSpan.FromSeconds(PollSeconds), TimeSpan.FromSeconds(PollSeconds));
|
|
}
|
|
|
|
private async Task RefreshAsync()
|
|
{
|
|
try
|
|
{
|
|
_status = await HistorianActor.ActorRef.Ask<HistorianSinkStatus>(
|
|
HistorianAdapterActor.GetStatus.Instance, TimeSpan.FromSeconds(2));
|
|
StateHasChanged();
|
|
}
|
|
catch
|
|
{
|
|
// Actor unavailable (admin-only node, not driver-role) — leave _status null and let
|
|
// the page show "Loading…". A dedicated "this role doesn't run a historian" message
|
|
// would be nicer; lands when we add role gating to the UI.
|
|
}
|
|
}
|
|
|
|
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 void Dispose() => _timer?.Dispose();
|
|
}
|