Files
lmxopcua/docs/v2/implementation/phase-6-4-admin-ui-completion.md
Joseph Doherty 4695a5c88e Phase 6 — Draft 4 implementation plans covering v2 unimplemented features + adversarial review + adjustments. After drivers were paused per user direction, audited the v2 plan for features documented-but-unshipped and identified four coherent tracks that had no implementation plan at all. Each plan follows the docs/v2/implementation/phase-*.md template (DRAFT status, branch name, Stream A-E task breakdown, Compliance Checks, Risks, Completion Checklist). docs/v2/implementation/phase-6-1-resilience-and-observability.md (243 lines) covers Polly resilience pipelines wired to every capability interface, Tier A/B/C runtime enforcement (memory watchdog generalized beyond Galaxy, scheduled recycle per decision #67, wedge detection), health endpoints on :4841, structured Serilog with correlation IDs, LiteDB local-cache fallback per decision #36. phase-6-2-authorization-runtime.md (145 lines) wires ACL enforcement on every OPC UA Read/Write/Subscribe/Call path + LDAP-group-to-admin-role grants per decisions #105 and #129 -- runtime permission-trie evaluator over the 6-level Cluster/Namespace/UnsArea/UnsLine/Equipment/Tag hierarchy, per-session cache invalidated on generation-apply + LDAP-cache expiry. phase-6-3-redundancy-runtime.md (165 lines) lands the non-transparent warm/hot redundancy runtime per decisions #79-85: dynamic ServiceLevel node, ServerUriArray peer broadcast, mid-apply dip via sp_PublishGeneration hook, operator-driven role transition (no auto-election -- plan remains explicit about what's out of scope). phase-6-4-admin-ui-completion.md (178 lines) closes Phase 1 Stream E completion-checklist items that never landed: UNS drag-reorder + impact preview, Equipment CSV import, 5-identifier search, draft-diff viewer enhancements, OPC 40010 _base Identification field exposure per decisions #138-139. Each plan then got a Codex adversarial-review pass (codex mcp tool, read-only sandbox, synchronous). Reviews explicitly targeted decision-log conflicts, API-shape assumptions, unbounded blast radius, under-specified state transitions, and testing holes. Appended 'Adversarial Review — 2026-04-19' section to each plan with numbered findings (severity / finding / why-it-matters / adjustment accepted). Review surfaced real substantive issues that the initial drafts glossed over: Phase 6.1 auto-retry conflicting with decisions #44-45 no-auto-write-retry rule; Phase 6.1 per-driver-instance pipeline breaking decision #35's per-device isolation; Phase 6.1 recycle/watchdog at Tier A/B breaching decisions #73-74 Tier-C-only constraint; Phase 6.2 conflating control-plane LdapGroupRoleMapping with data-plane ACL grants; Phase 6.2 missing Browse enforcement entirely; Phase 6.2 subscription re-authorization policy unresolved between create-time-only and per-publish; Phase 6.3 ServiceLevel=0 colliding with OPC UA Part 5 Maintenance semantics; Phase 6.3 ServerUriArray excluding self (spec-bug); Phase 6.3 apply-window counter race on cancellation; Phase 6.3 client cutover for Kepware/Aveva OI Gateway is unverified hearsay; Phase 6.4 stale UNS impact preview overwriting concurrent draft edits; Phase 6.4 identifier contract drifting from admin-ui.md canonical set (ZTag/MachineCode/SAPID/EquipmentId/EquipmentUuid, not ZTag/SAPID/UniqueId/Alias1/Alias2); Phase 6.4 CSV import atomicity internally contradictory (single txn vs chunked inserts); Phase 6.4 OPC 40010 field list not matching decision #139. Every finding has an adjustment in the plan doc -- plans are meant to be executable from the next session with the critique already baked in rather than a clean draft that would run into the same issues at implementation time. Codex thread IDs cited in each plan's review section for reproducibility. Pure documentation PR -- no code changes. Plans are DRAFT status; each becomes its own implementation phase with its own entry-gate + exit-gate when business prioritizes.
2026-04-19 03:15:00 -04:00

14 KiB
Raw Blame History

Phase 6.4 — Admin UI Completion

Status: DRAFT — Phase 1 Stream E shipped the Admin scaffold + core pages; several feature-completeness items from its completion checklist (phase-1-configuration-and-admin-scaffold.md §Stream E) never landed. This phase closes them.

Branch: v2/phase-6-4-admin-ui-completion Estimated duration: 2 weeks Predecessor: Phase 6.3 (Redundancy runtime) — reuses the /cluster/{id} page layout for the new tabs Successor: v2 release-readiness capstone (Task #121)

Phase Objective

Close the Admin UI feature-completeness checklist that Phase 1 Stream E exit gate left open. Each item below is an existing phase-1-configuration-and-admin-scaffold.md completion-checklist entry that is currently unchecked.

Gaps to close:

  1. UNS Structure tab drag/move with impact preview — decision #115 + admin-ui.md §"UNS". Current state: list-only render; no drag reorder; no "X lines / Y equipment impacted" preview.
  2. Equipment CSV import + 5-identifier search — decision #95 + #117. Current state: basic form; no CSV parser; search indexes only ZTag.
  3. Draft-generation diff viewer — enhance existing DiffViewer.razor to show generation-diff not just staged-edit diff; highlight ACL grant changes (lands after Phase 6.2).
  4. _base equipment-class Identification fields exposure — decision #138139. Columns exist on Equipment; no Admin UI field group; no address-space exposure of the OPC 40010 sub-folder.

Scope — What Changes

Concern Change
Admin/Pages/UnsTab.razor Rewrite as a tree component with drag-drop (Blazor-native HTML5 DnD; no third-party dep). Each drag fires a "Compute Impact" call against the draft-generation state + renders a modal preview ("Moving Line 'Oven-2' from 'Packaging' to 'Assembly' will re-home 14 equipment + re-parent 237 tags"). Confirmation commits the draft edit.
Admin/Services/UnsImpactAnalyzer.cs New service. Given a move-operation (line move, area rename, line merge), computes cascade counts by walking the draft-generation Equipment + Tag tables. Pure-function shape; testable in isolation.
Admin/Pages/EquipmentTab.razor Add CSV-import button → modal with file picker + dry-run preview. Add multi-identifier search bar (ZTag / SAPID / UniqueId / Alias1 / Alias2) per decision #95 — parses any of the five, shows matches across draft + published generations.
Admin/Services/EquipmentCsvImporter.cs New service. Parses CSV with documented header row; validates each row against the Equipment schema (required fields + ExternalIdReservation freshness); returns ImportPreview DTO with per-row accept/reject + reason; commit step wraps in a single EF transaction.
Admin/Pages/DraftEditor.razor + DiffViewer.razor Diff viewer expanded: adds sections for ACL grants (from Phase 6.2 LdapGroupRoleMapping + NodeAcl), redundancy-role changes (from Phase 6.3), equipment-class _base Identification fields. Render each section collapsible.
Admin/Components/IdentificationFields.razor New component. Renders the OPC 40010 nullable columns (Manufacturer, Model, SerialNumber, ProductInstanceUri, HardwareRevision, SoftwareRevision, DeviceRevision, YearOfConstruction, MonthOfConstruction) as a labelled field group on the EquipmentTab detail view.
OtOpcUa.Server/OpcUa/DriverNodeManager — Equipment folder build When an Equipment row has non-null Identification fields, the server adds an Identification sub-folder under the Equipment node containing one variable per non-null field. Matches OPC 40010 companion spec.

Scope — What Does NOT Change

Item Reason
Admin UI visual language Bootstrap 5 / cookie auth / sidebar layout unchanged — consistency with ScadaLink design reference.
LDAP auth flow Already shipped in Phase 1. Phase 6.4 is additive UI only.
Core abstractions / driver layer Admin UI changes don't touch drivers.
Equipment-class template schema validation Still deferred (decision #112 — schemas repo not landed). We expose the Identification fields but don't validate against a template hierarchy.
Drag/move to other clusters Out of scope — equipment is cluster-scoped per decision #82. Cross-cluster migration is a different workflow.

Entry Gate Checklist

  • Phase 6.2 merged (ACL grants are part of the new diff viewer sections)
  • Phase 6.3 merged (redundancy-role changes are part of the diff viewer)
  • phase-1-configuration-and-admin-scaffold.md §Stream E completion checklist re-read — confirm these are the remaining items
  • admin-ui.md re-skimmed for screen layouts
  • Existing EquipmentTab.razor / UnsTab.razor / DraftEditor.razor diff'd against what ships today so the edits are additive not destructive
  • Dev Galaxy available for OPC 40010 exposure smoke testing

Task Breakdown

Stream A — UNS drag/reorder + impact preview (4 days)

  1. A.1 UnsImpactAnalyzer service. Inputs: (DraftGenerationId, MoveOperation). Outputs: ImpactPreview { AffectedEquipmentCount, AffectedTagCount, CascadeWarnings[] }. Unit tests cover line move / area rename / line merge.
  2. A.2 HTML5 DnD on a tree component. No JS interop beyond ondragstart/ondragover/ondrop — keeps build + testability simple.
  3. A.3 Modal preview wired to UnsImpactAnalyzer output; "Confirm" commits a draft edit via DraftService.
  4. A.4 Playwright smoke test (or equivalent): drag a line across areas, assert modal shows the right counts, assert draft row reflects the move.

Stream B — Equipment CSV import + 5-identifier search (4 days)

  1. B.1 EquipmentCsvImporter with a documented header row (ZTag, SAPID, UniqueId, Alias1, Alias2, Name, UnsAreaName, UnsLineName, Manufacturer, Model, SerialNumber, …). Parser rejects unknown columns + blank required fields + duplicate ZTags.
  2. B.2 ImportPreview UI: per-row accept/reject table. Reject reasons: "ZTag already exists in draft", "ExternalIdReservation conflict with Cluster X", "UnsLineName not found in draft UNS tree", etc. Operator reviews then clicks "Commit" → single EF transaction.
  3. B.3 Multi-identifier search — bar accepts any of the 5 identifiers, probes each column in parallel, returns first-match-wins + disambiguation list if multiple match.
  4. B.4 Smoke tests: 100-row CSV with 10 intentional conflicts (5 ZTag dupes, 3 reservation clashes, 2 missing UnsLines); assert preview flags each; assert commit rolls back cleanly when a conflict surfaces post-preview.

Stream C — Diff viewer enhancements (3 days)

  1. C.1 Refactor DiffViewer.razor into a base component + section plugins. Section plugins: StructuralDiffSection (UNS tree), EquipmentDiffSection (Equipment rows), TagDiffSection (Tag rows), AclDiffSection (ACL grants — depends on Phase 6.2), RedundancyDiffSection (role changes — depends on Phase 6.3), IdentificationDiffSection (OPC 40010 fields).
  2. C.2 Each section renders collapsed by default; counts + top-line summary always visible.
  3. C.3 Tests: seed two generations with deliberate diffs, assert every section reports the right counts + top-line summary.

Stream D — OPC 40010 Identification exposure (3 days)

  1. D.1 IdentificationFields.razor component — labelled inputs; nullable columns show empty input; required field validation only on commit.
  2. D.2 DriverNodeManager equipment-folder builder — after building the equipment node, inspect the Identification columns; if any non-null, add an Identification sub-folder with variable-per-field.
  3. D.3 Address-space smoke test via Client.CLI: browse an equipment node, assert Identification sub-folder present when columns are set, absent when all null.

Compliance Checks (run at exit gate)

  • UNS drag/move: drag a line across areas; modal preview shows correct impacted-equipment + impacted-tag counts.
  • Equipment CSV: 100-row CSV with 10 conflicts imports cleanly (preview flags each, commit rolls back mid-conflict).
  • 5-identifier search: querying any of the 5 IDs returns the matching row; ambiguous searches list options.
  • Diff viewer: every section renders for a 2-generation diff with deliberate changes in every category.
  • OPC 40010 exposure: Client.CLI browse shows Identification sub-folder when equipment has non-null columns; folder absent when all null.
  • ScadaLink visual parity: operator-equivalence reviewer signs off that the new tabs feel consistent with existing Admin UI pages.

Risks and Mitigations

Risk Likelihood Impact Mitigation
UNS drag-drop janky on large trees (>500 nodes) Medium Medium Virtualize the tree component; default-collapse nested areas; test with a synthetic 1000-equipment seed
CSV import performance on 10k-row imports Medium Medium Stream-parse rather than load-into-memory; preview renders in batches of 100; commit is chunked-EF-insert with progress bar
Diff viewer becomes unwieldy with many sections Low Medium Each section collapsed by default; top-line summary row always shown; Phase 6.4 caps at 6 sections
OPC 40010 sub-folder accidentally exposes NULL/empty identification columns as empty-string variables Low Low Column null-check in the builder; drop variables whose DB value is null
5-identifier search pulls full table Medium Medium Indexes on each of ZTag/SAPID/UniqueId/Alias1/Alias2; search query uses a UNION of 5 indexed lookups; falls back to LIKE only on explicit operator opt-in

Completion Checklist

  • Stream A: UnsImpactAnalyzer + drag-drop tree + modal preview + Playwright smoke
  • Stream B: EquipmentCsvImporter + preview modal + 5-identifier search + conflict-rollback test
  • Stream C: DiffViewer refactor + 6 section plugins + 2-generation diff test
  • Stream D: IdentificationFields.razor + address-space builder change + Client.CLI browse test
  • Visual-compliance reviewer signoff
  • Full solution dotnet test passes; phase-6-4-compliance.ps1 exits 0; exit-gate doc

Adversarial Review — 2026-04-19 (Codex, via codex-rescue subagent)

  1. Crit · ACCEPT — Stale UNS impact preview can overwrite concurrent draft edits. Change: each preview carries a DraftRevisionToken; Confirm compares against the current draft + rejects with a 409 Conflict / refresh-required modal if any draft edit landed since the preview was generated. Stream A.3 updated.
  2. High · ACCEPT — CSV import atomicity is internally contradictory (single EF transaction vs. chunked inserts). Change: one explicit model — staged-import table (EquipmentImportBatch { Id, CreatedAtUtc, RowsStaged, RowsAccepted, RowsRejected }) receives rows in chunks; final FinaliseImportBatch is atomic over Equipment + ExternalIdReservation. Rollback is "drop the batch row" — the real Equipment table is never partially mutated.
  3. Crit · ACCEPT — Identifier contract rewrite mis-cites decisions. Change: revert to the admin-ui.md + decision #117 canonical set — ZTag / MachineCode / SAPID / EquipmentId / EquipmentUuid. CSV header follows that set verbatim. Introduce a separate decision entry for versioned CSV header shape before adding any new column; CSV header row must start with # OtOpcUaCsv v1 so future shape changes are unambiguous.
  4. Med · ACCEPT — Search ordering undefined. Change: rank SQL — exact match on any identifier scores 100; prefix match 50; LIKE-fuzzy 20; published > draft tie-breaker; ORDER BY score DESC, RowVersion DESC. Typeahead shows which field matched via trailing badge.
  5. High · ACCEPT — HTML5 DnD on virtualized tree is aspirational. Change: Stream A.2 rewritten — commits to MudBlazor.TreeView + MudBlazor.DropTarget (already a transitive dep via the existing Admin UI). Build a 1000-node synthetic seed in A.1 + validate drag-latency budget before implementing impact preview. If MudBlazor can't hit the budget, fall back to a flat-list reorder UI with Area/Line dropdowns (loss of visual drag affordance but unblocks the feature).
  6. Med · ACCEPT — Collapsed-by-default doesn't handle generation-sized diffs. Change: each diff section has a hard row cap (1000 by default). Over-cap sections render an aggregate summary + "Load full diff" button that streams via SignalR in 500-row pages. Decision #115 subtree renames surface as a "N equipment re-parented under X → Y" summary instead of row-by-row.
  7. High · ACCEPT — OPC 40010 field list doesn't match decision #139. Change: field group realigned to Manufacturer, Model, SerialNumber, HardwareRevision, SoftwareRevision, YearOfConstruction, AssetLocation, ManufacturerUri, DeviceManualUri. ProductInstanceUri / DeviceRevision / MonthOfConstruction dropped from Phase 6.4 — they belong to a future OPC 40010 widening decision.
  8. High · ACCEPTIdentification subtree unreconciled with ACL hierarchy (Phase 6.2 6-level scope). Change: address-space builder creates the Identification sub-folder under the Equipment node with the same ScopeId as Equipment — no new scope level. ACL evaluator treats …/Equipment/Identification/X as inheriting the Equipment scope's grants. Documented in Phase 6.2's acl-design.md cross-reference update.
  9. Low · ACCEPT — Visual-review gate names nonexistent reviewer role. Change: rubric defined — a named "Admin UX reviewer" (role FleetAdmin user, not the implementation lead) compares side-by-side screenshots against the admin-ui.md §Visual-Design reference panels; signoff artefact is a checked-in screenshot set under docs/v2/visual-compliance/phase-6-4/.
  10. Med · ACCEPT — Cross-cluster drag/drop lacks loud failure path. Change: on drop across cluster boundary, disable the drop target + show a toast "Equipment is cluster-scoped (decision #82). To move across clusters, use the Export → Import workflow on the Cluster detail page." Plus a help link. Tested in Stream A.4.