From 56c136b0fd71722c0905bf39731dea0d8eb67d35 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 18 Jun 2026 11:29:58 -0400 Subject: [PATCH] =?UTF-8?q?docs(plans):=20implementation=20plan=20+=20task?= =?UTF-8?q?s=20=E2=80=94=20Hosts=20rows=20+=20AbCip=20nested=20+=20Galaxy?= =?UTF-8?q?=20hygiene?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...6-06-18-hosts-rows-abcip-nested-hygiene.md | 415 ++++++++++++++++++ ...ts-rows-abcip-nested-hygiene.md.tasks.json | 17 + 2 files changed, 432 insertions(+) create mode 100644 docs/plans/2026-06-18-hosts-rows-abcip-nested-hygiene.md create mode 100644 docs/plans/2026-06-18-hosts-rows-abcip-nested-hygiene.md.tasks.json diff --git a/docs/plans/2026-06-18-hosts-rows-abcip-nested-hygiene.md b/docs/plans/2026-06-18-hosts-rows-abcip-nested-hygiene.md new file mode 100644 index 00000000..39ce5da3 --- /dev/null +++ b/docs/plans/2026-06-18-hosts-rows-abcip-nested-hygiene.md @@ -0,0 +1,415 @@ +# Hosts per-driver rows + AbCip nested-struct + Galaxy hygiene — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to implement this plan task-by-task. + +**Goal:** Ship three disjoint backlog items — a `/hosts` cluster-grouped Driver Instances section (#8), +AbCip nested-struct member expansion (#6), and Galaxy stale-comment hygiene + reconcile (#3/#13/#10). + +**Architecture:** Reuse the existing driver-health DPS pipeline (add only an AdminUI-internal store +`GetAll()` + a pure view-model builder) so no Commons/interface/EF change is needed. AbCip stops discarding +the nested template id the member block already carries. Galaxy comments rewritten to shipped reality. + +**Tech Stack:** .NET 10, Blazor Server (AdminUI), EF Core (ConfigDB read), xUnit + Shouldly. No bUnit. + +**Base:** branch `feat/hosts-rows-abcip-nested-hygiene` off master `f59680fa`; design committed `fec08915`. + +**Execution note:** implementers dispatched **serially** on this branch (avoids the shared-tree git-race +lesson); per-task classification-driven review. T3 depends on T2. + +--- + +### Task 1: AbCip nested-struct template-id threading + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** none (serial dispatch) + +**Files:** +- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTemplateCache.cs:57` (`AbCipUdtMember` record) +- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/CipTemplateObjectDecoder.cs:85-96` +- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs:~1179` (the nested recursion call) +- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipFetchUdtShapeTests.cs` (or a new + `AbCipNestedTemplateTests.cs` in the same project) + +**Context:** Top-level controller-discovered UDT members are addressable (`4e141402`/`4a7b0fde`), but a +nested struct member's sub-shape can't be fetched because the prior fix passed `templateInstanceId: null` +when recursing. The member-info `u16` actually carries the nested template id (bit 15 = struct flag, lower +12 bits = template instance id — see the decoder's own remarks block, lines 26-27). Stop discarding it. + +**Step 1: Extend `AbCipUdtMember` (optional trailing param — preserves existing 4-arg call sites)** + +```csharp +/// One member of a Logix UDT. +/// Member name. +/// Byte offset from the struct start. +/// Member CIP data type (Structure for a nested UDT). +/// Element count (1 for a scalar member). +/// For a member, the nested UDT's +/// template instance id decoded from the member-info low 12 bits; null for a scalar member. +public sealed record AbCipUdtMember( + string Name, + int Offset, + AbCipDataType DataType, + int ArrayLength, + uint? NestedTemplateId = null); +``` + +**Step 2: Decode the nested template id in `CipTemplateObjectDecoder.Decode` (struct members only)** + +In the member loop (after computing `isStruct`/`typeCode`, ~line 85), use the **full 12-bit mask** (not the +byte-cast `typeCode`) for the nested id, and thread it into the member: + +```csharp +var isStruct = (info & MemberInfoStructFlag) != 0; +var typeCode = (byte)(info & MemberInfoTypeCodeMask); +var dataType = isStruct + ? AbCipDataType.Structure + : (CipSymbolObjectDecoder.MapTypeCode(typeCode) ?? AbCipDataType.Structure); +// For a struct member the low 12 bits are the nested UDT's template instance id (same encoding as +// the Symbol Object), NOT a primitive type code — capture it so the nested shape can be fetched. +var nestedTemplateId = isStruct ? (uint?)(info & MemberInfoTypeCodeMask) : null; + +var memberName = i < memberNames.Length ? memberNames[i] : $""; +members.Add(new AbCipUdtMember( + Name: memberName, + Offset: offset, + DataType: dataType, + ArrayLength: arraySize == 0 ? 1 : arraySize, + NestedTemplateId: nestedTemplateId)); +``` + +**Step 3: Thread the id at the recursion site (`AbCipDriver.cs:~1179`)** + +Change `templateInstanceId: null` → `templateInstanceId: member.NestedTemplateId`: + +```csharp +var nested = await ResolveDiscoveredUdtShapeAsync( + deviceHostAddress, member.Name, templateInstanceId: member.NestedTemplateId, cancellationToken) + .ConfigureAwait(false); +``` + +`ResolveDiscoveredUdtShapeAsync` still consults the name-keyed seeded shapes (the `SeedDiscoveredUdtShapeForTest` +seam) first, then falls back to `FetchUdtShapeAsync(deviceHostAddress, id, ct)` — which reads `@udt/{id}` and +decodes — when an id is now present. Deep nesting works recursively (each level decodes its own members' +`NestedTemplateId`), bounded by the existing `MaxUdtDepth` + `visited` cycle guard. + +**Step 4: Tests (offline, via `FakeTemplateReader`)** + +- **Decoder test:** build a minimal Template Object blob (header + one struct member with + `info = 0x8000 | 0x123`, plus a scalar member) using the same byte layout the existing tests use; assert + the struct member decodes `DataType == Structure` AND `NestedTemplateId == 0x123u`, and the scalar member + decodes `NestedTemplateId == null`. +- **Threading test:** drive the discovery/fan-out path for a top-level UDT whose member is a nested struct; + configure the `FakeTemplateReader` to return the nested template's blob for the nested id; assert the nested + struct's leaf members become addressable (emitted) — i.e. the nested id now drives a real fetch rather than + yielding `null`. Reuse the existing discovery test harness pattern in this project; do NOT use + `SeedDiscoveredUdtShapeForTest` for the nested shape (that would bypass what we're testing). + +**Step 5: Run + commit** + +```bash +dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests --filter "FullyQualifiedName~Udt" -v minimal +git add src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTemplateCache.cs \ + src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/CipTemplateObjectDecoder.cs \ + src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs \ + tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/.cs +git commit -m "feat(abcip): thread nested-struct template id so nested UDT members are addressable (#6)" +``` + +--- + +### Task 2: `/hosts` driver-snapshot store enumeration + pure grouping view-model + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** none (serial; T3 depends on this) + +**Files:** +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/IDriverStatusSnapshotStore.cs` +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/InMemoryDriverStatusSnapshotStore.cs` +- Create: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hosts/HostsDriverView.cs` (pure builder + view-model records) +- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Hosts/HostsDriverViewTests.cs` (confirm the AdminUI + test project path; create the file) + +**Step 1: Add `GetAll()` to the store interface** + +```csharp +/// Returns a point-in-time snapshot of every driver instance's last-known health. +IReadOnlyCollection GetAll(); +``` + +**Step 2: Implement in `InMemoryDriverStatusSnapshotStore`** + +```csharp +/// +public IReadOnlyCollection GetAll() => _byInstance.Values.ToArray(); +``` + +**Step 3: Pure grouping view-model `HostsDriverView`** + +Define plain input/output records (no EF types) so it unit-tests offline. Group by `ClusterId`, union of +cluster ids from nodes + snapshots; enrich each snapshot with `Name`/`DriverType` by joining instances on +`DriverInstanceId`; unknown driver (snapshot with no matching instance) → null Name/DriverType fallback. + +```csharp +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hosts; + +using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers; + +/// A cluster node as needed for the Hosts driver view (subset of the ConfigDB ClusterNode row). +public sealed record HostsNodeInfo(string ClusterId, string NodeId, string Host, int OpcUaPort); + +/// A driver instance as needed for enrichment (subset of the ConfigDB DriverInstance row). +public sealed record HostsDriverInstanceInfo(string DriverInstanceId, string ClusterId, string Name, string DriverType); + +/// One driver row in a cluster group: live health enriched with the configured name + type. +public sealed record HostsDriverRow( + string DriverInstanceId, + string? Name, + string? DriverType, + string State, + DateTime? LastSuccessfulReadUtc, + string? LastError, + int ErrorCount5Min, + DateTime PublishedUtc); + +/// One cluster group: the cluster's nodes + the cluster's live driver rows. +public sealed record HostsClusterGroup( + string ClusterId, + IReadOnlyList Nodes, + IReadOnlyList Drivers); + +/// Pure builder for the Hosts page Driver-Instances section — DB-agnostic + unit-testable. +public static class HostsDriverView +{ + /// Builds the cluster-grouped driver view from live snapshots + cached ConfigDB rows. + /// Live driver-health snapshots from the snapshot store. + /// ConfigDB cluster nodes (for the per-cluster node list). + /// ConfigDB driver instances (for Name/DriverType enrichment). + /// Cluster groups ordered by ClusterId; drivers ordered by Name ?? DriverInstanceId. + public static IReadOnlyList Build( + IEnumerable snapshots, + IEnumerable nodes, + IEnumerable instances) + { + var snapList = snapshots?.ToList() ?? new(); + var nodeList = nodes?.ToList() ?? new(); + var instById = (instances ?? Enumerable.Empty()) + .GroupBy(i => i.DriverInstanceId) + .ToDictionary(g => g.Key, g => g.First()); + + var clusterIds = snapList.Select(s => s.ClusterId) + .Concat(nodeList.Select(n => n.ClusterId)) + .Where(id => !string.IsNullOrEmpty(id)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(id => id, StringComparer.OrdinalIgnoreCase); + + var groups = new List(); + foreach (var clusterId in clusterIds) + { + var clusterNodes = nodeList + .Where(n => string.Equals(n.ClusterId, clusterId, StringComparison.OrdinalIgnoreCase)) + .OrderBy(n => n.NodeId, StringComparer.OrdinalIgnoreCase) + .ToList(); + + var drivers = snapList + .Where(s => string.Equals(s.ClusterId, clusterId, StringComparison.OrdinalIgnoreCase)) + .Select(s => + { + instById.TryGetValue(s.DriverInstanceId, out var inst); + return new HostsDriverRow( + s.DriverInstanceId, inst?.Name, inst?.DriverType, s.State, + s.LastSuccessfulReadUtc, s.LastError, s.ErrorCount5Min, s.PublishedUtc); + }) + .OrderBy(d => d.Name ?? d.DriverInstanceId, StringComparer.OrdinalIgnoreCase) + .ToList(); + + groups.Add(new HostsClusterGroup(clusterId, clusterNodes, drivers)); + } + return groups; + } +} +``` + +**Step 4: Tests (xUnit + Shouldly)** in `HostsDriverViewTests.cs`: +- two clusters with nodes + snapshots → two groups, correct nodes + drivers per cluster; +- enrichment: a snapshot whose `DriverInstanceId` matches an instance → Name + DriverType filled; +- unknown-driver fallback: a snapshot with no matching instance → row present with null Name/DriverType; +- a cluster with nodes but no snapshots → group present, empty Drivers; +- empty inputs → empty list; +- driver ordering by Name then DriverInstanceId. + +**Step 5: Run + commit** + +```bash +dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests --filter "FullyQualifiedName~HostsDriverView" -v minimal +git add src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/IDriverStatusSnapshotStore.cs \ + src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/InMemoryDriverStatusSnapshotStore.cs \ + src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hosts/HostsDriverView.cs \ + tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Hosts/HostsDriverViewTests.cs +git commit -m "feat(adminui): driver-snapshot GetAll() + pure Hosts driver-view builder (#8)" +``` + +--- + +### Task 3: `/hosts` "Driver Instances" Razor section + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** none (depends on Task 2) + +**Files:** +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Hosts.razor` + +**Context:** Add a new section below the existing Members section. Keep the Akka member rows untouched. +Pattern after `DriverStatusPanel` for the live subscription (in-process store read, marshal via InvokeAsync — +dodges the Traefik self-hub trap) and after `Fleet.razor` for the ConfigDB read. + +**Step 1: Injects + usings** (top of Hosts.razor) + +```razor +@using ZB.MOM.WW.OtOpcUa.AdminUI.Hosts +@using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs +@using ZB.MOM.WW.OtOpcUa.Configuration +@using Microsoft.EntityFrameworkCore +@inject IDriverStatusSnapshotStore DriverStore +@inject IDbContextFactory DbFactory +``` +(Confirm the ConfigDbContext type name + namespace from `Fleet.razor`.) + +**Step 2: State + lifecycle (in `@code`)** + +- Fields: `IReadOnlyList? _driverGroups;` plus cached + `List _nodes`, `List _instances`. +- `OnInitializedAsync`: `await LoadConfigAsync();` then `RebuildDriverGroups();` then + `DriverStore.SnapshotChanged += OnSnapshotChanged;` +- `LoadConfigAsync()`: `await using var db = await DbFactory.CreateDbContextAsync();` read `ClusterNodes` + → `HostsNodeInfo(ClusterId, NodeId, Host, OpcUaPort)`; read `DriverInstances` → + `HostsDriverInstanceInfo(DriverInstanceId, ClusterId, Name, DriverType)`. (Confirm DbSet + property names.) +- `RebuildDriverGroups()`: `_driverGroups = HostsDriverView.Build(DriverStore.GetAll(), _nodes, _instances);` +- `OnSnapshotChanged(DriverHealthChanged _)`: `InvokeAsync(() => { RebuildDriverGroups(); StateHasChanged(); });` + (cheap — reuses cached config; a brand-new driver shows by id until the 5 s timer/Refresh re-reads config). +- Extend the existing `Refresh`/timer + `RefreshAsync` to also `await LoadConfigAsync(); RebuildDriverGroups();` + so configured names stay current. (The timer callback is sync — make a small async path or call + `LoadConfigAsync()` fire-and-forget-with-InvokeAsync; keep it simple and correct.) +- `Dispose()`: add `DriverStore.SnapshotChanged -= OnSnapshotChanged;` alongside the existing `_timer?.Dispose()`. + +**Step 3: Markup — new section below the Members ``** + +A panel "Driver instances" with a small notice that rows are **cluster-scoped** (health is per driver +instance across the cluster, not per Akka member). For each `HostsClusterGroup`: a sub-head +`Cluster ` + node chips (` (:)`) + a table of drivers: +columns Driver (Name ?? DriverInstanceId, with the id in `.mono` small if a name exists), Type, Status +(chip via the mapping below), Last read, Last error, Errors/5 min. Handle `_driverGroups is null` (Loading) +and empty (a notice "No driver instances reporting yet"). + +**Step 4: Local chip mapping (mirror `DriverStatusPanel.ChipClass`)** + +```csharp +private static string DriverChipClass(string? state) => state switch +{ + "Healthy" => "chip-ok", + "Degraded" => "chip-warn", + "Connecting" => "chip-warn", + "Reconnecting" => "chip-warn", + "Faulted" => "chip-bad", + _ => "chip-idle", +}; +``` + +**Step 5: Build + commit** (no bUnit — proven live in Task 6) + +```bash +dotnet build src/Server/ZB.MOM.WW.OtOpcUa.AdminUI -v minimal +git add src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Hosts.razor +git commit -m "feat(adminui): /hosts cluster-grouped Driver Instances section (#8)" +``` +Also update the page's stale top-of-file comment (lines 2-5: "there are no per-driver host rows yet") to +reflect the shipped section. + +--- + +### Task 4: Galaxy stale-comment hygiene + +**Classification:** trivial +**Estimated implement time:** ~4 min +**Parallelizable with:** none (serial) + +**Files:** +- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs` (~lines 52, 92, 669) +- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverFactoryExtensions.cs` (~line 19) +- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/HostStatusAggregator.cs` (~line 21) + +**Comment-only.** Verify each claim against the current code BEFORE rewriting (e.g. confirm `_ownedMxSession` +is built in `InitializeAsync`, the probe-watcher membership refresh after `DiscoverAsync` is wired, +`HostStatusAggregator.OnHostStatusChanged` is re-raised by the driver). Rewrite to shipped reality: +- Drop "PR 4.W" / "PR 4.4 supplies the production implementation; until then…" / "legacy-host backend + handles reads in production" — Galaxy is the standard Equipment-kind driver now; reads ARE supported in + production; the legacy Galaxy.Host/Proxy/Shared were retired in PR 7.2. +- `GalaxyDriverFactoryExtensions`: drop "PR 4.W will add a server-side `Galaxy:Backend` switch … parity + testing (Phase 5)" — never landed and won't (only `GalaxyMxGateway` exists; the legacy `Galaxy` proxy + type is retired). State the shipped reality (the distinct type name is historical; no backend switch). +- Convert any genuinely-still-future note to a real `TODO`, else delete the forward-ref. + +Do NOT change any code — comments/XML-doc only. + +**Build + commit:** +```bash +dotnet build src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy -v minimal +git add src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs \ + src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverFactoryExtensions.cs \ + src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/HostStatusAggregator.cs +git commit -m "docs(galaxy): rewrite stale PR-4.W/legacy-host forward-ref comments to shipped reality (#13)" +``` + +--- + +### Task 5: Reconcile stillpending + memory (never-staged for stillpending) + +**Classification:** trivial +**Estimated implement time:** ~3 min +**Parallelizable with:** none + +**Files:** +- Modify: `stillpending.md` (**never staged** — local working copy only) +- Modify: `/Users/dohertj2/.claude/projects/-Users-dohertj2-Desktop-OtOpcUa/memory/MEMORY.md` +- Modify: `/Users/dohertj2/.claude/projects/-Users-dohertj2-Desktop-OtOpcUa/memory/project_stillpending_backlog.md` + +- `stillpending.md`: mark item #10 SHIPPED (the `ctx`-receiver guard is in `ScriptAnalysisService.cs:224`, + `70e1bde9`); strike #8 (this phase) + #6 (this phase) SHIPPED with the commit refs once known; note the + AbCip finding (id was already in the member block, not a new query) + the Hosts cluster-scoped framing + + the deferred per-member-nesting follow-up. +- Memory: update `project_stillpending_backlog.md` + the `MEMORY.md` one-liner. Keep `MEMORY.md` entries + short (the index is already near its size cap). + +**Commit (memory files only — stillpending.md stays unstaged):** +```bash +git add docs/plans/2026-06-18-hosts-rows-abcip-nested-hygiene.md.tasks.json +git commit -m "docs(plans): mark hosts/abcip/hygiene tasks complete + reconcile" +``` +(Memory files live outside the repo; they are written, not committed.) + +--- + +### Task 6: Build + tests + live `/run` + finish + +**Classification:** small +**Estimated implement time:** ~5 min (+ live-verify) +**Parallelizable with:** none + +**Step 1: Full build + the three affected test projects** +```bash +dotnet build ZB.MOM.WW.OtOpcUa.slnx -v minimal +dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests -v minimal +dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests -v minimal +dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests -v minimal +``` +Expected: build clean; all green (AbCip + AdminUI + Galaxy). Note any pre-existing unrelated failures. + +**Step 2: Component A live `/run` (docker-dev)** — rebuild **BOTH** central-1 AND central-2 (the `:9200` +AdminUI round-robins across both; a half-deploy serves stale code). Deploy a Modbus driver, open +`http://localhost:9200/hosts`, confirm the Driver Instances section lists the deployed driver(s) grouped by +cluster with a live status chip; drive a Reconnect from a driver page and confirm the chip updates. Record +the result honestly (login is disabled on the local rig — drive it yourself; do not defer to user sign-in). + +**Step 3: Finish** — superpowers-extended-cc:finishing-a-development-branch → merge to master + push. diff --git a/docs/plans/2026-06-18-hosts-rows-abcip-nested-hygiene.md.tasks.json b/docs/plans/2026-06-18-hosts-rows-abcip-nested-hygiene.md.tasks.json new file mode 100644 index 00000000..a3ec684b --- /dev/null +++ b/docs/plans/2026-06-18-hosts-rows-abcip-nested-hygiene.md.tasks.json @@ -0,0 +1,17 @@ +{ + "planPath": "docs/plans/2026-06-18-hosts-rows-abcip-nested-hygiene.md", + "designPath": "docs/plans/2026-06-18-hosts-rows-abcip-nested-hygiene-design.md", + "branch": "feat/hosts-rows-abcip-nested-hygiene", + "baseSha": "f59680fa", + "designCommit": "fec08915", + "executionState": "IN_PROGRESS", + "tasks": [ + {"id": 1, "subject": "Task 1: AbCip nested-struct template-id threading (decoder + record + driver + tests)", "classification": "standard", "status": "pending"}, + {"id": 2, "subject": "Task 2: /hosts store GetAll() + pure HostsDriverView grouping builder + tests", "classification": "standard", "status": "pending"}, + {"id": 3, "subject": "Task 3: /hosts cluster-grouped Driver Instances Razor section", "classification": "standard", "status": "pending", "blockedBy": [2]}, + {"id": 4, "subject": "Task 4: Galaxy stale-comment hygiene (PR-4.W / legacy-host forward-refs)", "classification": "trivial", "status": "pending"}, + {"id": 5, "subject": "Task 5: Reconcile stillpending #10/#6/#8 + memory", "classification": "trivial", "status": "pending"}, + {"id": 6, "subject": "Task 6: Build + AbCip/AdminUI/Galaxy tests + Component A live /run + finish (merge+push)", "classification": "small", "status": "pending"} + ], + "lastUpdated": "2026-06-18" +}