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:
@@ -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**:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user