Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/AlarmsHistorian.razor
T
Joseph Doherty 2e4ccf7fe9 chore(localdb): phase-2 DoD sweep
Build: 0 errors solution-wide, and 0 warnings from every project this branch
touches. The ~816 solution-wide warnings are pre-existing xUnit1051 /
OTOPCUA0001 / CS86xx in untouched driver + client test projects.

Tests: full solution run compared against a full run on a detached worktree at
the pre-branch baseline 2e46d054. The two failure SETS are identical -- all 13
tests, same names, zero new failures. Net +26 tests: +3 Core.AlarmHistorian
(drain gate), +6 Runtime (role view), +17 Host.IntegrationTests (migrator +
convergence). Set comparison rather than counts, because the suite carries
standing environment- and load-dependent failures a count would hide.

The greps found real drift Task 6 missed -- eight live sites still naming the
deleted SqliteStoreAndForwardSink, including the AdminUI /alarms/historian
panel text, which is user-visible, and a <see cref> in HistorianAdapterActor
that resolved to nothing without warning. All repointed at
LocalDbStoreAndForwardSink; CLAUDE.md's alarm-history paragraph now also
records the LocalDb buffer and the primary-gated drain, and drops DatabasePath
from the knob list. docs/AlarmTracking.md still promised an
AlarmHistorianOptions.Validate() startup warning for a relative DatabasePath
and an empty SharedSecret; both branches are gone, so it now says so.

Code references to AlarmHistorian:DatabasePath reduce to exactly two intentional
ones: AlarmSfLegacyMigrator.LegacyPathKey and its test. No `new SqliteConnection`
remains anywhere in Core.AlarmHistorian.

Recon doc gains the durable verification record: guard-deletion evidence for
both vacuous passes, the two exact-set replicated-table pins (both assert set
equality, so an added or a dropped registration fails), and the baseline test
comparison with a per-failure account of why each of the 13 is not this
branch's.

Stops here per the plan. Task 8's live gate needs explicit go-ahead; nothing on
this branch is to be merged.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-21 05:03:57 -04:00

93 lines
3.8 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 [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
@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">LocalDbStoreAndForwardSink</span> — buffering into this node's LocalDb, and
draining to the HistorianGateway (<span class="mono">SendEvent</span>) only while this node holds
the Primary role. 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();
}