Harden v2 design against the four findings from the 2026-04-17 Codex adversarial review of the db schema and admin UI: (1) DriverInstance.NamespaceId now enforces a same-cluster invariant in three layers (sp_ValidateDraft cross-table check using the new UX_Namespace_Generation_LogicalId_Cluster composite index, server-side namespace-selection API scoping that prevents bypass via crafted requests, and audit-log entries on cross-cluster attempts) so a draft for cluster A can no longer bind to cluster B's namespace and leak its URI into A's endpoint; (2) the Namespace table moves from cluster-level to generation-versioned with append-only logical-ID identity and locked NamespaceUri/Kind across generations so admins can no longer disable a namespace that a published driver depends on outside the publish/diff/rollback flow, the cluster-create workflow opens an initial draft containing the default namespaces instead of writing namespace rows directly, and the Admin UI Namespaces tab becomes hybrid (read-only over published, click-to-edit opens draft) like the UNS Structure tab; (3) ZTag/SAPID fleet-wide uniqueness moves from per-generation indexes (which silently allow rollback or re-enable to reintroduce duplicates) into a new ExternalIdReservation table that sits outside generation versioning, with sp_PublishGeneration reserving atomically via MERGE under transaction lock so a different EquipmentUuid attempting the same active value rolls the whole publish back, an FleetAdmin-only sp_ReleaseExternalIdReservation as the only path to free a value for reuse with audit trail, and a corresponding Release-reservation operator workflow in the Admin UI; (4) Equipment.EquipmentId is now system-generated as 'EQ-' + first 12 hex chars of EquipmentUuid, never operator-supplied or editable, removed from the Equipment CSV import schema entirely (rows match by EquipmentUuid for updates or create new equipment with auto-generated identifiers when no UUID is supplied), with a new Merge-or-Rebind-equipment operator workflow handling the rare case where two UUIDs need to be reconciled — closing the corruption path where typos and bulk-import renames were minting duplicate identities and breaking downstream UUID-keyed lineage. New decisions #122-125 with explicit "supersedes" notes for the earlier #107 (cluster-level namespace) and #116 (operator-set EquipmentId) frames they revise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-17 11:08:58 -04:00
parent 449bc454b7
commit a59ad2e0c6
4 changed files with 674 additions and 65 deletions

View File

@@ -187,13 +187,17 @@ If we discover an Admin-specific component need, add it to our Shared folder rat
```
/ Fleet Overview (default landing)
/clusters Cluster list
/clusters/{ClusterId} Cluster detail
/clusters/{ClusterId}/nodes/{NodeId} Node detail
/clusters/{ClusterId}/draft Draft editor (drivers/devices/tags)
/clusters/{ClusterId}/draft/diff Draft vs current diff viewer
/clusters/{ClusterId}/generations Generation history
/clusters/{ClusterId}/generations/{Id} Generation detail (read-only view of any generation)
/clusters/{ClusterId}/audit Audit log filtered to this cluster
/clusters/{ClusterId} Cluster detail (tabs: Overview / Namespaces / UNS Structure / Drivers / Devices / Equipment / Tags / Generations / Audit)
/clusters/{ClusterId}/nodes/{NodeId} Node detail
/clusters/{ClusterId}/namespaces Namespace management (generation-versioned via draft → publish; same boundary as drivers/tags)
/clusters/{ClusterId}/uns UNS structure management (areas, lines, drag-drop reorganize)
/clusters/{ClusterId}/equipment Equipment list (default sorted by ZTag)
/clusters/{ClusterId}/equipment/{EquipmentId} Equipment detail (5 identifiers, UNS placement, signals, audit)
/clusters/{ClusterId}/draft Draft editor (drivers/devices/equipment/tags)
/clusters/{ClusterId}/draft/diff Draft vs current diff viewer
/clusters/{ClusterId}/generations Generation history
/clusters/{ClusterId}/generations/{Id} Generation detail (read-only view of any generation)
/clusters/{ClusterId}/audit Audit log filtered to this cluster
/credentials Credential management (FleetAdmin only)
/audit Fleet-wide audit log
/admin/users Admin role assignments (FleetAdmin only)
@@ -224,14 +228,49 @@ Tabbed view for one cluster.
**Tabs:**
1. **Overview** — cluster metadata (name, site, redundancy mode, namespace URI), node table with online/offline/role/generation/last-applied-status, current published generation summary, draft status (none / in progress / ready to publish)
2. **Drivers**table of `DriverInstance` rows in the *current published* generation, with per-row navigation to driver-specific config screens. "Edit in draft" button creates or opens the cluster's draft.
3. **Devices** — table of `Device` rows (where applicable), grouped by `DriverInstance`
4. **Tags** — paged, filterable table of all tags. Filters: driver, device, folder path, name pattern, data type. Bulk operations toolbar: export to CSV, import from CSV (validated against active draft).
5. **Generations** — generation history list (see Generation History page)
6. **Audit** — filtered audit log
1. **Overview** — cluster metadata (name, Enterprise, Site, redundancy mode), namespace summary (which kinds are configured + their URIs), node table with online/offline/role/generation/last-applied-status, current published generation summary, draft status (none / in progress / ready to publish)
2. **Namespaces**list of `Namespace` rows for this cluster *in the current published generation* (Kind, NamespaceUri, Enabled). **Namespaces are generation-versioned** (revised after adversarial review finding #2): add / disable / re-enable a namespace by opening a draft, making the change, and publishing. The tab is read-only when no draft is open; "Edit in draft" button opens the cluster's draft scoped to the namespace section. Equipment kind is auto-included in the cluster's first generation; SystemPlatform kind is added when a Galaxy driver is configured. Simulated kind is reserved (operator can add a row of `Kind = 'Simulated'` in a draft but no driver populates it in v2.0; UI shows "Awaiting replay driver — see roadmap" placeholder).
3. **UNS Structure** — tree view of `UnsArea``UnsLine``Equipment` for this cluster's current published generation. Operators can:
- Add/rename/delete areas and lines (changes go into the active draft)
- Bulk-move lines between areas (drag-and-drop in the tree, single edit propagates UNS path changes to all equipment under the moved line)
- Bulk-move equipment between lines
- View live UNS path preview per node (`Enterprise/Site/Area/Line/Equipment`)
- See validation errors inline (segment regex, length cap, _default placeholder rules)
- Counts per node: # lines per area, # equipment per line, # signals per equipment
- Path-rename impact: when renaming an area, UI shows "X lines, Y equipment, Z signals will pick up new path" before commit
4. **Drivers** — table of `DriverInstance` rows in the *current published* generation, with per-row namespace assignment shown. Per-row navigation to driver-specific config screens. "Edit in draft" button creates or opens the cluster's draft.
5. **Devices** — table of `Device` rows (where applicable), grouped by `DriverInstance`
6. **Equipment** — table of `Equipment` rows in the current published generation, scoped to drivers in Equipment-kind namespaces. **Default sort: ZTag ascending** (the primary browse identifier per decision #117). Default columns:
- `ZTag` (primary, bold, copyable)
- `MachineCode` (secondary, e.g. `machine_001`)
- Full UNS path (rendered live from cluster + UnsLine→UnsArea + Equipment.Name)
- `SAPID` (when set)
- `EquipmentUuid` (collapsed badge, copyable on click — "show UUID" toggle to expand)
- `EquipmentClassRef` (placeholder until schemas repo lands)
- DriverInstance, DeviceId, Enabled
The Drivers/Devices/Tags tabs are **read-only views** of the published generation; editing is done in the dedicated draft editor to make the publish boundary explicit.
Search bar supports any of the five identifiers (ZTag, MachineCode, SAPID, EquipmentId, EquipmentUuid) — operator types and the search dispatches across all five with a typeahead that disambiguates ("Found in ZTag" / "Found in MachineCode" labels on each suggestion). Per-row click opens the Equipment Detail page.
7. **Tags** — paged, filterable table of all tags. Filters: namespace kind, equipment (by ZTag/MachineCode/SAPID), driver, device, folder path, name pattern, data type. For Equipment-ns tags the path is shown as the full UNS path; for SystemPlatform-ns tags the v1-style `FolderPath/Name` is shown. Bulk operations toolbar: export to CSV, import from CSV (validated against active draft).
8. **Generations** — generation history list (see Generation History page)
9. **Audit** — filtered audit log
The Drivers/Devices/Equipment/Tags tabs are **read-only views** of the published generation; editing is done in the dedicated draft editor to make the publish boundary explicit. The Namespaces tab and the UNS Structure tab follow the same hybrid pattern: navigation is read-only over the published generation, click-to-edit on any node opens the draft editor scoped to that node. **No table in v2.0 is edited outside the publish boundary** (revised after adversarial review finding #2).
### Equipment Detail (`/clusters/{ClusterId}/equipment/{EquipmentId}`)
Per-equipment view. Form sections:
- **Identifiers panel**: all five identifiers, with explicit purpose labels and copy-to-clipboard buttons
- `ZTag` — editable; live fleet-wide uniqueness check via `ExternalIdReservation` (warns if value is currently held by another EquipmentUuid; cannot save unless reservation is released first)
- `MachineCode` — editable; live within-cluster uniqueness check
- `SAPID` — editable; same reservation-backed check as ZTag
- `EquipmentId`**read-only forever** (revised after adversarial review finding #4). System-generated as `'EQ-' + first 12 hex chars of EquipmentUuid`. Never operator-editable, never present in any input form, never accepted from CSV imports
- `EquipmentUuid` — read-only forever (auto-generated UUIDv4 on creation, never editable; copyable badge with "downstream consumers join on this" tooltip)
- **UNS placement panel**: UnsArea/UnsLine pickers (typeahead from existing structure); `Equipment.Name` field with live segment validation; live full-path preview with character counter
- **Class template panel**: `EquipmentClassRef` — free text in v2.0; becomes a typeahead picker when schemas repo lands
- **Driver source panel**: DriverInstance + DeviceId pickers (filtered to drivers in Equipment-kind namespaces of this cluster)
- **Signals panel**: list of `Tag` rows that belong to this equipment; inline edit not supported here (use Draft Editor's Tags panel for editing); read-only with a "Edit in draft" deep link
- **Audit panel**: filtered audit log scoped to this equipment row across generations
### Node Detail (`/clusters/{ClusterId}/nodes/{NodeId}`)
@@ -248,17 +287,40 @@ Per-node view for `ClusterNode` management.
### Draft Editor (`/clusters/{ClusterId}/draft`)
The primary edit surface. Three-panel layout: tree on the left (drivers → devices → tags), edit form on the right, validation panel at the bottom.
The primary edit surface. Three-panel layout: tree on the left (Drivers → Devices → Equipment → Tags, with Equipment shown only for drivers in Equipment-kind namespaces), edit form on the right, validation panel at the bottom.
- **Drivers panel**: add/edit/remove `DriverInstance` rows in the draft. Each driver type opens a driver-specific config screen (deferred per #27). Generic fields (Name, NamespaceUri, Enabled) are always editable.
- **Drivers panel**: add/edit/remove `DriverInstance` rows in the draft. Each driver type opens a driver-specific config screen (deferred per #27). Generic fields (Name, NamespaceId, Enabled) are always editable. The NamespaceId picker is filtered to namespace kinds that are valid for the chosen driver type (e.g. selecting `DriverType=Galaxy` restricts the picker to SystemPlatform-kind namespaces only).
- **Devices panel**: scoped to the selected driver instance (where applicable)
- **UNS Structure panel** (Equipment-ns drivers only): tree of UnsArea → UnsLine; CRUD on areas and lines; rename and move operations with live impact preview ("renaming bldg-3 → bldg-3a will update 12 lines, 47 equipment, 1,103 signal paths"); validator rejects identity reuse with a different parent
- **Equipment panel** (Equipment-ns drivers only):
- Add/edit/remove `Equipment` rows scoped to the selected driver
- Inline form sections:
- **Identifiers**: `MachineCode` (required, e.g. `machine_001`, validates within-cluster uniqueness live); `ZTag` (optional, ERP id, validates fleet-wide uniqueness via `ExternalIdReservation` lookup live — surfaces "currently reserved by EquipmentUuid X in cluster Y" if collision); `SAPID` (optional, SAP PM id, same reservation-backed check)
- **UNS placement**: `UnsLineId` picker (typeahead from existing structure or "Create new line" inline); `Name` (UNS level 5, live segment validation `^[a-z0-9-]{1,32}$`)
- **Class template**: `EquipmentClassRef` (free text in v2.0; becomes a typeahead picker when schemas repo lands)
- **Source**: `DeviceId` (when driver has multiple devices); `Enabled`
- **`EquipmentUuid` is auto-generated UUIDv4 on creation, displayed read-only as a copyable badge**, never editable. **`EquipmentId` is also auto-generated** (`'EQ-' + first 12 hex chars of EquipmentUuid`) and never editable in any form. Both stay constant across renames, MachineCode/ZTag/SAPID edits, and area/line moves. The validator rejects any draft that tries to change either value on a published equipment.
- **Live UNS path preview** above the form: `{Cluster.Enterprise}/{Cluster.Site}/{UnsArea.Name}/{UnsLine.Name}/{Name}` with character count and ≤200 limit indicator
- Bulk operations:
- Move many equipment from one line to another (UUIDs and identifiers preserved)
- Bulk-edit MachineCode/ZTag/SAPID via inline grid (validation per row)
- Bulk-create equipment from CSV (one row per equipment; UUIDs auto-generated for new rows)
- **Tags panel**:
- Tree view by `FolderPath`
- Tree view: by Equipment when in Equipment-ns; by `FolderPath` when in SystemPlatform-ns
- Inline edit for individual tags (Name, DataType, AccessLevel, WriteIdempotent, PollGroupId, TagConfig JSON in a structured editor)
- **Bulk operations**: select multiple tags → bulk edit (change poll group, access level, etc.)
- **CSV import**: upload a CSV with `(DriverInstanceId, DeviceId?, FolderPath, Name, DataType, AccessLevel, WriteIdempotent, PollGroupId, TagConfig)` columns. Preview shows additions/modifications/removals against current draft, with row-level validation errors. Operator confirms or cancels.
- **CSV export**: emit the same shape from the current published generation, useful as a starting point for bulk edits in Excel
- **Validation panel** runs `sp_ValidateDraft` continuously (debounced) and surfaces FK errors, JSON schema errors, duplicate paths, missing references. Publish button is disabled while errors exist.
- **CSV import** schemas (one per namespace kind):
- Equipment-ns: `(EquipmentId, Name, DataType, AccessLevel, WriteIdempotent, PollGroupId, TagConfig)`
- SystemPlatform-ns: `(DriverInstanceId, DeviceId?, FolderPath, Name, DataType, AccessLevel, WriteIdempotent, PollGroupId, TagConfig)`
- Preview shows additions/modifications/removals against current draft, with row-level validation errors. Operator confirms or cancels.
- **CSV export**: emit the matching shape from the current published generation
- **Equipment CSV import** (separate flow): bulk-create-or-update equipment. Columns: `(EquipmentUuid?, MachineCode, ZTag?, SAPID?, UnsAreaName, UnsLineName, Name, DriverInstanceId, DeviceId?, EquipmentClassRef?)`. **No `EquipmentId` column** (revised after adversarial review finding #4 — operator-supplied EquipmentId would mint duplicate equipment identity on typos):
- **Row with `EquipmentUuid` set**: matches existing equipment by UUID, updates the matched row's editable fields (MachineCode/ZTag/SAPID/UnsLineId/Name/EquipmentClassRef/DeviceId/Enabled). Mismatched UUID = error, abort row.
- **Row without `EquipmentUuid`**: creates new equipment. System generates fresh UUID and `EquipmentId = 'EQ-' + first 12 hex chars`. Cannot be used to update an existing row — operator must include UUID for updates.
- UnsArea/UnsLine resolved by name within the cluster (auto-create if not present, with validation prompt).
- Identifier uniqueness checks run row-by-row with errors surfaced before commit. ZTag/SAPID checked against `ExternalIdReservation` — collisions surface inline with the conflicting EquipmentUuid named.
- Explicit "merge equipment A into B" or "rebind ZTag from A to B" operations are not in the CSV import path — see the Merge / Rebind operator flow below.
- **Validation panel** runs `sp_ValidateDraft` continuously (debounced 500 ms) and surfaces FK errors, JSON schema errors, duplicate paths, missing references, UNS naming-rule violations, UUID-immutability violations, and driver-type-vs-namespace-kind mismatches. Publish button is disabled while errors exist.
- **Diff link** at top: opens the diff viewer comparing the draft against the current published generation
### Diff Viewer (`/clusters/{ClusterId}/draft/diff`)
@@ -325,9 +387,15 @@ The generic JSON editor uses the per-driver JSON schema from `DriverTypeRegistry
### Add a new cluster
1. FleetAdmin: `/clusters` → "New cluster"
2. Form: Name, Site, NodeCount (1 or 2), RedundancyMode (auto-set based on NodeCount), NamespaceUri (auto-suggested from name)
3. Save → cluster row created (`Status = Enabled`, no generations yet)
4. Redirect to Cluster Detail; prompt to add nodes
2. Form: Name, **Enterprise** (UNS level 1, e.g. `ent`, validated `^[a-z0-9-]{1,32}$`), **Site** (UNS level 2, e.g. `warsaw-west`, same validation), NodeCount (1 or 2), RedundancyMode (auto-set based on NodeCount)
3. Save → cluster row created (`Enabled = 1`, no generations yet)
4. **Open initial draft** containing default namespaces:
- Equipment-kind namespace (`NamespaceId = {ClusterName}-equipment`, `NamespaceUri = urn:{Enterprise}:{Site}:equipment`). Operator can edit URI in the draft before publish.
- Prompt: "This cluster will host a Galaxy / System Platform driver?" → if yes, the draft also includes a SystemPlatform-kind namespace (`urn:{Enterprise}:{Site}:system-platform`). If no, skip — operator can add it later via a draft.
5. Operator reviews the initial draft, optionally adds the first nodes' worth of drivers/equipment, then publishes generation 1. The cluster cannot serve any consumer until generation 1 is published (no namespaces exist before that).
6. Redirect to Cluster Detail; prompt to add nodes via the Node tab (cluster topology) — node addition itself remains cluster-level since `ClusterNode` rows are physical-machine topology, not consumer-visible content.
(Revised after adversarial review finding #2 — namespaces must travel through the publish boundary; the cluster-create flow no longer writes namespace rows directly.)
### Add a node to a cluster
@@ -371,6 +439,33 @@ The "no new generation" choice is deliberate: overrides are operationally bound
3. Wait for `LastAppliedAt` on the node to advance (proves the new credential is being used by the node — operator-side work to provision the new credential on the node's machine happens out-of-band)
4. Once verified, disable the old credential → only the new one is valid
### Release an external-ID reservation
When equipment is permanently retired and its `ZTag` or `SAPID` needs to be reusable by a different physical asset (a known-rare event):
1. FleetAdmin: navigate to Equipment Detail of the retired equipment, or to a global "External ID Reservations" view
2. Select the reservation (Kind + Value), click "Release"
3. Modal requires: confirmation of the EquipmentUuid that currently holds the reservation, and a free-text **release reason** (compliance audit trail)
4. Confirm → `sp_ReleaseExternalIdReservation` runs: sets `ReleasedAt`, `ReleasedBy`, `ReleaseReason`. Audit-logged with `EventType = 'ExternalIdReleased'`.
5. The same `(Kind, Value)` can now be reserved by a different EquipmentUuid in a future publish. The released row stays in the table forever for audit.
This is the **only** path that allows ZTag/SAPID reuse — no implicit release on equipment disable, no implicit release on cluster delete. Requires explicit FleetAdmin action with a documented reason.
### Merge or rebind equipment (rare)
When operators discover that two `EquipmentRow`s in different generations actually represent the same physical asset (e.g. a typo created a duplicate) — or when an asset's identity has been incorrectly split across UUIDs — the resolution is **not** an in-place EquipmentId edit (which is now impossible per finding #4). Instead:
1. FleetAdmin: Equipment Detail of the row that should be retained → "Merge from another EquipmentUuid"
2. Pick the source EquipmentUuid (the one to retire); modal shows a side-by-side diff of identifiers and signal counts
3. Confirm → opens a **draft** that:
- Disables the source equipment row (`Enabled = 0`) and adds an `EventType = 'EquipmentMergedAway'` audit entry naming the target UUID
- Re-points any tags currently on the source equipment to the target equipment
- If the source held a ZTag/SAPID reservation that should move to the target: explicit release of the source's reservation followed by re-reservation under the target UUID, both audit-logged
4. Operator reviews the draft diff; publishes
5. Downstream consumers see the source EquipmentUuid disappear (joins on it return historical data only) and the target EquipmentUuid gain the merged tags
Merge is a destructive lineage operation — the source EquipmentUuid is never reused, but its history persists in old generations + audit log. Rare by intent; UI buries the action behind two confirmation prompts.
## Deferred / Out of Scope
- **Cluster-scoped admin grants** (`ConfigEditor` for Cluster X only, not for Cluster Y) — surface in v2.1
@@ -391,6 +486,9 @@ The "no new generation" choice is deliberate: overrides are operationally bound
- Draft → diff → publish is the only edit path; no in-place edits
- Sticky alerts require manual ack
- Per-node overrides are NOT generation-versioned
- **All content edits go through the draft → diff → publish boundary** — Namespaces, UNS Structure, Drivers, Devices, Equipment, Tags. The UNS Structure and Namespaces tabs are hybrid (read-only navigation over the published generation, click-to-edit opens the draft editor scoped to that node). No table is editable outside the publish boundary in v2.0 (revised after adversarial review finding #2 — earlier draft mistakenly treated namespaces as cluster-level)
- **Equipment list defaults to ZTag sort** (primary browse identifier per the 3-year-plan handoff). All five identifiers (ZTag/MachineCode/SAPID/EquipmentId/EquipmentUuid) are searchable; typeahead disambiguates which field matched
- **EquipmentUuid is read-only forever** in the UI; never editable. Auto-generated UUIDv4 on equipment creation, displayed as a copyable badge
**Resolved Defaults**: