14 KiB
14 KiB
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-completionEstimated 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:
- 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. - Equipment CSV import + 5-identifier search — decision #95 + #117. Current state: basic form; no CSV parser; search indexes only ZTag.
- Draft-generation diff viewer — enhance existing
DiffViewer.razorto show generation-diff not just staged-edit diff; highlight ACL grant changes (lands after Phase 6.2). _baseequipment-class Identification fields exposure — decision #138–139. Columns exist onEquipment; 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 itemsadmin-ui.mdre-skimmed for screen layouts- Existing
EquipmentTab.razor/UnsTab.razor/DraftEditor.razordiff'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)
- A.1
UnsImpactAnalyzerservice. Inputs:(DraftGenerationId, MoveOperation). Outputs:ImpactPreview { AffectedEquipmentCount, AffectedTagCount, CascadeWarnings[] }. Unit tests cover line move / area rename / line merge. - A.2 HTML5 DnD on a tree component. No JS interop beyond
ondragstart/ondragover/ondrop— keeps build + testability simple. - A.3 Modal preview wired to
UnsImpactAnalyzeroutput; "Confirm" commits a draft edit viaDraftService. - 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)
- B.1
EquipmentCsvImporterwith a documented header row (ZTag, SAPID, UniqueId, Alias1, Alias2, Name, UnsAreaName, UnsLineName, Manufacturer, Model, SerialNumber, …). Parser rejects unknown columns + blank required fields + duplicate ZTags. - B.2
ImportPreviewUI: 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. - 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.
- 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)
- C.1 Refactor
DiffViewer.razorinto 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). - C.2 Each section renders collapsed by default; counts + top-line summary always visible.
- 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)
- D.1
IdentificationFields.razorcomponent — labelled inputs; nullable columns show empty input; required field validation only on commit. - D.2
DriverNodeManagerequipment-folder builder — after building the equipment node, inspect the Identification columns; if any non-null, add anIdentificationsub-folder with variable-per-field. - D.3 Address-space smoke test via Client.CLI: browse an equipment node, assert
Identificationsub-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
Identificationsub-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:
DiffViewerrefactor + 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 testpasses;phase-6-4-compliance.ps1exits 0; exit-gate doc
Adversarial Review — 2026-04-19 (Codex, via codex-rescue subagent)
- Crit · ACCEPT — Stale UNS impact preview can overwrite concurrent draft edits. Change: each preview carries a
DraftRevisionToken;Confirmcompares against the current draft + rejects with a409 Conflict / refresh-requiredmodal if any draft edit landed since the preview was generated. Stream A.3 updated. - 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; finalFinaliseImportBatchis atomic overEquipment+ExternalIdReservation. Rollback is "drop the batch row" — the real Equipment table is never partially mutated. - 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 v1so future shape changes are unambiguous. - 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. - 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). - 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.
- 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 / MonthOfConstructiondropped from Phase 6.4 — they belong to a future OPC 40010 widening decision. - High · ACCEPT —
Identificationsubtree 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/Xas inheriting theEquipmentscope's grants. Documented in Phase 6.2'sacl-design.mdcross-reference update. - Low · ACCEPT — Visual-review gate names nonexistent reviewer role. Change: rubric defined — a named "Admin UX reviewer" (role
FleetAdminuser, not the implementation lead) compares side-by-side screenshots against theadmin-ui.md§Visual-Design reference panels; signoff artefact is a checked-in screenshot set underdocs/v2/visual-compliance/phase-6-4/. - 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.