{ "planPath": "docs/plans/2026-05-29-adminui-followups.md", "branch": "feat/adminui-followups", "lastUpdated": "2026-08-07", "closureNote": "CLOSED 2026-08-07 by the overlap audit deferment.md §6.2 asked for before executing. This file recorded 19 pending / 0 completed and was called 'the largest unexecuted plan in the tree' and 'the only substantial engineering left' — that was bookkeeping drift, not backlog. Every task is shipped, superseded by v3, or moot; per-task evidence is in each closureNote below. The plan's own premise died with v3: WS1 rests on 'driver-embedded tag/device lists in DriverConfig JSON are the runtime source of truth', but v3 authors tags as Tag rows in /raw and the deploy artifact populates the driver's RawTags (ModbusDriverFactoryExtensions.cs:98-102), so hand-editing a list on the driver form no longer decides what the driver polls. The capability WS1 wanted was delivered in the v3 shape instead: per-driver typed tag editors via TagConfigEditorMap (11 editors, dispatched from RawTagModal.razor:79 + RawManualTagEntryModal.razor:92) and per-driver device forms via DeviceFormMap (9 forms, dispatched from DeviceModal.razor:45), both guarded by DriverFormMapParityTests + DriverDispatchMapParityTests. Verified reachable, not merely present — a supersession claim naming an unreachable replacement would repeat the dead-surface class this repo has hit repeatedly.", "tasks": [ {"id": 11, "plan": 1, "subject": "Task 1: Generic CollectionEditor component", "status": "completed", "closureNote": "SHIPPED — Components/Shared/Drivers/CollectionEditor.razor exists with the designed shape (working-copy modal, commit-on-save, NewRow factory, Validate delegate)."}, {"id": 12, "plan": 2, "subject": "Task 2: Modbus tag editor (proof) + tests", "status": "completed", "closureNote": "SUPERSEDED — delivered as Uns/TagEditors/ModbusTagConfigEditor.razor via TagConfigEditorMap, not as a CollectionEditor over an embedded JSON list. The embedded list it would have edited is no longer the runtime source of truth."}, {"id": 13, "plan": 3, "subject": "Task 3: AbCip device+tag editors + tests", "status": "completed", "closureNote": "SUPERSEDED — DeviceForms/AbCipDeviceForm.razor + Uns/TagEditors/AbCipTagConfigEditor.razor."}, {"id": 14, "plan": 4, "subject": "Task 4: AbLegacy device+tag editors + tests", "status": "completed", "closureNote": "SUPERSEDED — DeviceForms/AbLegacyDeviceForm.razor + Uns/TagEditors/AbLegacyTagConfigEditor.razor."}, {"id": 15, "plan": 5, "subject": "Task 5: TwinCAT device+tag editors + tests", "status": "completed", "closureNote": "SUPERSEDED — DeviceForms/TwinCATDeviceForm.razor + Uns/TagEditors/TwinCATTagConfigEditor.razor."}, {"id": 16, "plan": 6, "subject": "Task 6: FOCAS device+tag editors + tests", "status": "completed", "closureNote": "SUPERSEDED — DeviceForms/FocasDeviceForm.razor + Uns/TagEditors/FocasTagConfigEditor.razor."}, {"id": 17, "plan": 7, "subject": "Task 7: S7 tag editor + tests", "status": "completed", "closureNote": "SUPERSEDED — Uns/TagEditors/S7TagConfigEditor.razor (plus DeviceForms/S7DeviceForm.razor, which this task did not ask for)."}, {"id": 18, "plan": 8, "subject": "Task 8: OpcUaClient endpoint-URL editor + tests", "status": "completed", "closureNote": "SHIPPED — and it is the one task delivered in the plan's own shape: Forms/OpcUaClientDriverForm.razor is the sole production consumer of CollectionEditor. Endpoints genuinely still live in the driver config, which is why this one survived the v3 change and Tasks 2-7 did not."}, {"id": 19, "plan": 9, "subject": "Task 9: ResilienceFormModel + tests", "status": "completed", "closureNote": "SHIPPED — Components/Shared/Drivers/ResilienceFormModel.cs + tests/Server/...AdminUI.Tests/ResilienceFormModelTests.cs."}, {"id": 20, "plan": 10, "subject": "Task 10: Typed resilience form in DriverResilienceSection", "status": "completed", "closureNote": "SHIPPED — DriverResilienceSection.razor binds ResilienceFormModel and keeps the raw-JSON escape hatch at :67. NARROWER than designed, deliberately: two of the designed fields no longer exist. RecycleIntervalSeconds was deleted with the scheduled-recycle machinery (Gitea #522; see the tombstone comments at DriverResilienceOptions.cs:22 and ResilienceFormModel.cs:26) and the bulkhead knobs were deleted end-to-end by archreview R2-02 because Build never added a concurrency limiter. Authoring either today would re-create an inert knob."}, {"id": 21, "plan": 11, "subject": "Task 11: RoleMapper.Merge overload + tests", "status": "completed", "closureNote": "SHIPPED — RoleMapper.Merge(baseline, dbRows) consumed by OtOpcUaGroupRoleMapper.MapAsync; covered by OtOpcUaGroupRoleMapperTests + CanonicalAdminRolesTests."}, {"id": 22, "plan": 12, "subject": "Task 12: Register ILdapGroupRoleMappingService in DI", "status": "completed", "closureNote": "SHIPPED — Configuration/ServiceCollectionExtensions.cs:34 (EF-backed, Scoped). A driver-only node additionally binds NullLdapGroupRoleMappingService (Host/Program.cs:351) because mesh Phase 4 left it with no ConfigDb."}, {"id": 23, "plan": 13, "subject": "Task 13: Wire DB merge into AuthEndpoints.LoginAsync", "status": "completed", "closureNote": "SHIPPED — AuthEndpoints.cs:100 calls roleMapper.MapAsync behind the shared IGroupRoleMapper seam, which applies the appsettings GroupToRole baseline and merges system-wide DB grants."}, {"id": 24, "plan": 14, "subject": "Task 14: Add FleetAdmin authorization policy", "status": "completed", "closureNote": "SHIPPED — AdminUiPolicies.FleetAdmin registered at Security/ServiceCollectionExtensions.cs:160 (RequireRole Administrator; the policy NAME was kept stable across the AdminRole rename)."}, {"id": 25, "plan": 15, "subject": "Task 15: RoleGrants.razor global CRUD (FleetAdmin-gated)", "status": "completed", "closureNote": "SHIPPED — RoleGrants.razor:2 carries [Authorize(Policy = AdminUiPolicies.FleetAdmin)]; Add (:132 CreateAsync, IsSystemWide=true/ClusterId=null as designed) + Delete (:148) + the read-only appsettings fallback panel. No in-place Update, which is correct rather than missing: a row is only (LdapGroup, Role), so editing either field IS a different grant — delete-then-add is the whole of 'update' here."}, {"id": 26, "plan": 16, "subject": "Task 16: LdapGroupRoleMapping service tests (global CRUD)", "status": "completed", "closureNote": "SHIPPED — tests/Core/...Configuration.Tests/LdapGroupRoleMappingServiceTests.cs."}, {"id": 27, "plan": 17, "subject": "Task 17: Delete stale source comments", "status": "completed", "closureNote": "MOOT — the comments went with their files. FleetStatusHub.cs was deleted with the whole zero-client fleet-status channel (Gitea #542); DriverEditRouter.razor, DriverTypePicker.razor and DriverEdit.razor were deleted by v3 Batch 2, which retired the routed /clusters/{id}/drivers flow. Greps for the named comments (TODO(3.3/3.4), 'legacy DriverEdit', 'until the bridge lands') return nothing outside bin/obj."}, {"id": 28, "plan": 18, "subject": "Task 18: Strip now-true rendered notes", "status": "completed", "closureNote": "MOOT — same cause: the driver pages that rendered 'list-editor coming in a follow-up phase' and 'typed-form-ifying Polly is a follow-up' no longer exist. Neither string survives anywhere in src/."}, {"id": 29, "plan": 19, "subject": "Task 19: Full verification (build + test + /run)", "status": "completed", "closureNote": "DONE 2026-08-07 as part of the same session's live-gate run: solution build 0 errors, and the AdminUI surfaces this plan targets were driven on the docker-dev rig alongside the four live gates. See deferment.md §9."} ] }