feat(adminui): #546 wire DriverStatusPanel into /hosts driver rows (collapsed expander)

DriverStatusPanel was orphaned — zero references repo-wide — so the Restart and
Reconnect driver commands had NO reachable UI at all, despite working end to end
over the new gRPC mesh transport. Nothing failed to build, test or deploy while
that was true, which is why it went unnoticed.

The panel is now reachable per driver row on /hosts, behind a per-row "Controls"
expander that is collapsed by default: the table stays scannable, and only the
opened panels hold snapshot-store subscriptions. Expansion state is a HashSet
keyed by DriverInstanceId held on the page, so it survives the 5 s refresh that
replaces every row record. Both rows are @key'd by instance id so the panel —
which binds its DriverInstanceId once in OnInitializedAsync — can never be diffed
onto a different driver.

The panel's Enabled parameter is fed from real config rather than defaulted:
HostsDriverInstanceInfo/HostsDriverRow now carry DriverInstance.Enabled, which
LoadConfigAsync already had in hand. A snapshot with no matching configured
instance is treated as enabled — the row exists only because live health arrived,
so "Disabled — not deployed" would contradict the same row.

The expander is visible to every AuthenticatedRead viewer; the panel self-gates
only its Reconnect/Restart buttons on DriverOperator.

A source-scan pin (HostsPanelWiringTests) asserts /hosts still renders the panel
and still binds it to the row's own driver, cluster and enabled state — removing
the tag would silently restore the orphaned state.

Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
This commit is contained in:
Joseph Doherty
2026-08-06 17:18:37 -04:00
parent 97854a074e
commit d33e4e20b5
4 changed files with 162 additions and 10 deletions
@@ -22,7 +22,11 @@ public sealed record HostsNodeInfo(string ClusterId, string NodeId, string Host,
/// <param name="ClusterId">Cluster the instance is configured under.</param>
/// <param name="Name">Authored display name.</param>
/// <param name="DriverType">Driver type string (Modbus, S7, Galaxy, ...).</param>
public sealed record HostsDriverInstanceInfo(string DriverInstanceId, string ClusterId, string Name, string DriverType);
/// <param name="Enabled">Whether the instance is authored enabled — forwarded to the row's
/// <c>DriverStatusPanel</c>, which renders a "Disabled — not deployed" notice and opens no
/// subscription when false.</param>
public sealed record HostsDriverInstanceInfo(
string DriverInstanceId, string ClusterId, string Name, string DriverType, bool Enabled = true);
/// <summary>
/// One row in the <c>/hosts</c> "Driver Instances" table: a live health snapshot enriched with
@@ -48,12 +52,17 @@ public sealed record HostsDriverInstanceInfo(string DriverInstanceId, string Clu
/// is no answer — the driver cannot report skips, or it faulted before building a tag table. Lets
/// "ignoring 20 of 22 tags" show even while every other column on the row — state, last read, error
/// count, last error — reads perfectly healthy (Gitea #541).</param>
/// <param name="Enabled">Whether the matching configured instance is authored enabled; <see langword="true"/>
/// when no instance matched. Fed to the row's expandable <c>DriverStatusPanel</c> (Gitea #546). An
/// unmatched row defaults to enabled on purpose: the row only exists because a live snapshot arrived,
/// so telling the operator it is "not deployed" would contradict the evidence in front of them.</param>
public sealed record HostsDriverRow(
string DriverInstanceId, string? Name, string? DriverType, string State,
DateTime? LastSuccessfulReadUtc, string? LastError, int ErrorCount5Min, DateTime PublishedUtc,
DateTime? RediscoveryNeededUtc = null, string? RediscoveryReason = null,
IReadOnlyList<HostConnectivityStatus>? HostStatuses = null,
SkippedTagReport? SkippedTags = null)
SkippedTagReport? SkippedTags = null,
bool Enabled = true)
{
/// <summary>Whether this driver is ignoring at least one authored tag — the condition the whole
/// column exists to make visible.</summary>
@@ -114,7 +123,7 @@ public static class HostsDriverView
/// seen across <paramref name="snapshots"/> and <paramref name="nodes"/>, ordered
/// case-insensitively. Each group carries its nodes (ordered by node id) and its driver rows
/// (snapshots enriched by joining <paramref name="instances"/> on driver instance id;
/// unmatched ids yield null name/type), ordered by name-then-instance-id.
/// unmatched ids yield null name/type and an enabled row), ordered by name-then-instance-id.
/// </summary>
/// <param name="snapshots">Live health snapshots; null treated as empty.</param>
/// <param name="nodes">Configured cluster nodes; null treated as empty.</param>
@@ -167,7 +176,8 @@ public static class HostsDriverView
s.RediscoveryNeededUtc,
s.RediscoveryReason,
s.HostStatuses,
s.SkippedTags);
s.SkippedTags,
instance?.Enabled ?? true);
})
.OrderBy(d => d.Name ?? d.DriverInstanceId, StringComparer.OrdinalIgnoreCase)
.ThenBy(d => d.DriverInstanceId, StringComparer.OrdinalIgnoreCase)