diff --git a/docs/plans/2026-05-26-akka-hosting-alignment-plan.md b/docs/plans/2026-05-26-akka-hosting-alignment-plan.md index 1359be5..55a60ed 100644 --- a/docs/plans/2026-05-26-akka-hosting-alignment-plan.md +++ b/docs/plans/2026-05-26-akka-hosting-alignment-plan.md @@ -464,56 +464,173 @@ Commit: `feat(configdb): persist DataProtection keys in ConfigDb`. --- -### Task 14: EF migration — drop `ConfigGeneration` and `ClusterNode.RedundancyRole`, add new tables +### Tasks 14a-14f: Entity-model rewrite + V2HostingAlignment migration + +> **Plan rewrite, 2026-05-26**: the original single Task 14 (5-min EF migration) was +> under-scoped — it only listed the schema drops/adds without addressing the 13+ entities +> whose foreign keys + indexes are keyed on `GenerationId`. The design doc (§ live-edit +> model) requires removing `GenerationId` from `Equipment`, `Driver`, `DriverInstance`, +> `Namespace`, `UnsArea`, `UnsLine`, `Device`, `Tag`, `PollGroup`, `NodeAcl`, `Script`, +> `VirtualTag`, `ScriptedAlarm` and adding `RowVersion` columns for last-write-wins +> stale-write detection. That cascades into `GenerationApplier`/`GenerationDiff`/ +> `GenerationSealedCache` and the legacy Server/Admin CRUD services. Policy decision +> (recorded with the user): the legacy `OtOpcUa.Server` + `OtOpcUa.Admin` projects are +> allowed to fail-to-compile between Task 14c and Task 56 — only the new v2 projects need +> to stay green. + +#### Task 14a: Add `RowVersion` to live-edit entities + +**Classification:** standard +**Estimated implement time:** ~10 min +**Parallelizable with:** none (foundation for 14b) + +**Files:** every live-edit entity class — `Equipment`, `DriverInstance`, `Device`, `Tag`, +`PollGroup`, `Namespace`, `UnsArea`, `UnsLine`, `NodeAcl`, `Script`, `VirtualTag`, +`ScriptedAlarm`. Add `public byte[] RowVersion { get; set; } = Array.Empty();` and a +`e.Property(x => x.RowVersion).IsRowVersion();` mapping in `OtOpcUaConfigDbContext`. + +Commit: `feat(configdb): add RowVersion to live-edit entities for last-write-wins detection`. + +--- + +#### Task 14b: Decouple live-edit entities from `ConfigGeneration` **Classification:** high-risk +**Estimated implement time:** ~30 min +**Parallelizable with:** none + +Remove `GenerationId` property, `Generation` navigation property, and the +`HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId)` mapping from each +of the 13 live-edit entities listed above. Rewrite the `UX__Generation_LogicalId` +indexes to drop the `GenerationId` column (logical IDs become globally unique). Drop +`UX_*_Generation_*` filtered indexes where the filter referenced generation scope. + +Will break `OtOpcUa.Server` + `OtOpcUa.Admin` compilation — that is accepted (Task 56 +deletes them). + +Commit: `refactor(configdb): drop GenerationId FK from live-edit entities`. + +--- + +#### Task 14c: Mark `GenerationApplier` / `GenerationDiff` / `GenerationSealedCache` obsolete + +**Classification:** high-risk +**Estimated implement time:** ~20 min +**Parallelizable with:** none + +`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/` contains `GenerationApplier.cs`, +`GenerationDiff.cs`, `ApplyCallbacks.cs`, `ChangeKind.cs`, `IGenerationApplier.cs`. These +implement the v1 draft/publish lifecycle that v2 replaces with `AdminOperationsActor` + +`ConfigComposer`. + +Inventory callers via `grep -rln 'GenerationApplier\|GenerationDiff' src tests`. Either: +- Mark types `[Obsolete("Replaced by AdminOperationsActor in v2", error: true)]` so + surviving call sites become hard build errors (cleaner; surfaces the Server-breakage), +- Or delete the files and accept the Server-side build break. + +Sweep `GenerationSealedCache` similarly. Keep the LiteDb cache concept (it's repurposed +in Task 39 for stale-config fallback) but rename references to use `DeploymentArtifact`. + +Commit: `refactor(configdb): obsolete GenerationApplier/Diff/SealedCache (replaced by AdminOperationsActor)`. + +--- + +#### Task 14d: Drop `RedundancyRole` from `ClusterNode` + +**Classification:** standard **Estimated implement time:** ~5 min -**Parallelizable with:** none (depends on Tasks 10-13) +**Parallelizable with:** none + +Remove `ClusterNode.RedundancyRole` property + the +`e.Property(x => x.RedundancyRole).HasConversion()` mapping + the +`UX_ClusterNode_Primary_Per_Cluster` filtered unique index from +`OtOpcUaConfigDbContext.ConfigureClusterNode`. Akka cluster leader-of-driver-role becomes +the source of truth (Phase 5, Task 35). + +Commit: `refactor(configdb): drop ClusterNode.RedundancyRole (replaced by Akka leader)`. + +--- + +#### Task 14e: Delete `ConfigGeneration` + `ClusterNodeGenerationState` + +**Classification:** small +**Estimated implement time:** ~5 min +**Parallelizable with:** none (depends on 14b clearing the FKs) + +Delete `Entities/ConfigGeneration.cs` and `Entities/ClusterNodeGenerationState.cs`. Remove +the corresponding `DbSet<>` entries and `Configure*` methods from +`OtOpcUaConfigDbContext`. Drop `GenerationStatus` and `NodeApplyStatus` enums. + +Commit: `refactor(configdb): delete ConfigGeneration + ClusterNodeGenerationState`. + +--- + +#### Task 14f: Generate `V2HostingAlignment` EF migration + +**Classification:** high-risk +**Estimated implement time:** ~15 min +**Parallelizable with:** none (consolidates 14a-14e) **Files:** - Create: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/_V2HostingAlignment.cs` - Create: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/_V2HostingAlignment.Designer.cs` -- Modify: `OtOpcUaConfigDbContext.cs` — remove `DbSet` and `DbSet`; remove `ClusterNode.RedundancyRole` property -- Delete: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ConfigGeneration.cs` -- Delete: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNodeGenerationState.cs` +- Modify: `tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/SchemaComplianceTests.cs` — update + the `expected` table list (remove ConfigGeneration + ClusterNodeGenerationState; add + Deployment + NodeDeploymentState + ConfigEdit + DataProtectionKeys). **Step 1: Generate migration** -Run: `dotnet ef migrations add V2HostingAlignment --project src/Core/ZB.MOM.WW.OtOpcUa.Configuration --startup-project src/Server/ZB.MOM.WW.OtOpcUa.Host` +```bash +dotnet ef migrations add V2HostingAlignment \ + --project src/Core/ZB.MOM.WW.OtOpcUa.Configuration \ + --startup-project src/Server/ZB.MOM.WW.OtOpcUa.Host +``` If `dotnet-ef` isn't installed: `dotnet tool install --global dotnet-ef --version 10.0.7`. **Step 2: Audit the generated migration** — it should: -- `DropTable("ConfigGeneration")` -- `DropTable("ClusterNodeGenerationState")` +- `DropTable("ConfigGeneration")` and `DropTable("ClusterNodeGenerationState")` - `DropColumn("RedundancyRole", "ClusterNode")` -- `CreateTable("Deployment", ...)` -- `CreateTable("NodeDeploymentState", ...)` -- `CreateTable("ConfigEdit", ...)` -- `CreateTable("DataProtectionKeys", ...)` +- For each of the 13 live-edit tables: `DropForeignKey` on `GenerationId`, + `DropIndex` on `UX_*_Generation_LogicalId` (and any `UX_*_Generation_*`), `DropColumn` on + `GenerationId`, `AddColumn("RowVersion", "rowversion")`, `CreateIndex` on the new + globally-unique logical-id pattern. +- `CreateTable("Deployment", ...)`, `CreateTable("NodeDeploymentState", ...)`, + `CreateTable("ConfigEdit", ...)`, `CreateTable("DataProtectionKeys", ...)`. -If extra changes appear (e.g., column-type drift), reconcile by editing the entity classes — do not edit the migration directly. +If extra changes appear (e.g., column-type drift), reconcile by editing the entity classes +— do not edit the migration directly. -**Step 3: Verify on a scratch SQL Server** +**Step 3: Verify on a scratch SQL Server** (per CLAUDE.md, Docker is on the shared host +`10.100.0.35`, not local). ```bash -docker run --rm -d --name v2-migration-test -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=Pass@word123" -p 14333:1433 mcr.microsoft.com/mssql/server:2022-latest +# from this Mac dev: +ssh dohertj2@10.100.0.35 'docker run --rm -d --name v2-migration-test \ + -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=Pass@word123" \ + -p 14333:1433 mcr.microsoft.com/mssql/server:2022-latest' # Wait ~10s for SQL Server to start -ConnectionStrings__ConfigDb="Server=localhost,14333;Database=OtOpcUaV2Test;User Id=sa;Password=Pass@word123;TrustServerCertificate=true" \ - dotnet ef database update --project src/Core/ZB.MOM.WW.OtOpcUa.Configuration --startup-project src/Server/ZB.MOM.WW.OtOpcUa.Host +ConnectionStrings__ConfigDb="Server=10.100.0.35,14333;Database=OtOpcUaV2Test;User Id=sa;Password=Pass@word123;TrustServerCertificate=true" \ + dotnet ef database update \ + --project src/Core/ZB.MOM.WW.OtOpcUa.Configuration \ + --startup-project src/Server/ZB.MOM.WW.OtOpcUa.Host +ssh dohertj2@10.100.0.35 'docker exec v2-migration-test /opt/mssql-tools/bin/sqlcmd \ + -S localhost -U sa -P Pass@word123 -d OtOpcUaV2Test \ + -Q "SELECT name FROM sys.tables ORDER BY name"' +ssh dohertj2@10.100.0.35 'docker stop v2-migration-test' ``` -Expected: completes without error. Verify with `docker exec v2-migration-test /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Pass@word123 -d OtOpcUaV2Test -Q "SELECT name FROM sys.tables ORDER BY name"`. +Expected: migration completes; sys.tables contains the 4 new tables and not the 2 dropped +ones; live-edit tables have `RowVersion` column. -**Step 4: Tear down** - -`docker stop v2-migration-test`. +**Step 4: Update `SchemaComplianceTests`** so its `expected` array matches the new schema. **Step 5: Commit** ```bash -git add src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ -git commit -m "feat(configdb): V2HostingAlignment migration — drop ConfigGeneration, add Deployment+NodeDeploymentState+ConfigEdit" +git add src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ \ + tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/SchemaComplianceTests.cs +git commit -m "feat(configdb): V2HostingAlignment migration — drop generation lifecycle, add deploy tables" ``` --- @@ -1888,7 +2005,12 @@ After Task 65: | 11 | NodeDeploymentState entity | standard | 5m | 10,12,13 | | 12 | ConfigEdit entity | small | 4m | 10,11,13 | | 13 | DataProtection keys | small | 3m | 10-12 | -| 14 | V2 migration | high-risk | 5m | — | +| 14a | RowVersion on live-edit entities | standard | 10m | — | +| 14b | Drop GenerationId FK from entities | high-risk | 30m | — | +| 14c | Obsolete GenerationApplier/Diff/Cache | high-risk | 20m | — | +| 14d | Drop ClusterNode.RedundancyRole | standard | 5m | — | +| 14e | Delete ConfigGeneration + ClusterNodeGenerationState | small | 5m | — | +| 14f | V2HostingAlignment migration (consolidator) | high-risk | 15m | — | | 15 | Migrate-To-V2.ps1 | standard | 5m | 16-18 | | 16 | Common types | standard | 5m | 17,18 | | 17 | Message contracts | standard | 5m | 16,18 | diff --git a/docs/plans/2026-05-26-akka-hosting-alignment-plan.md.tasks.json b/docs/plans/2026-05-26-akka-hosting-alignment-plan.md.tasks.json index f5f6a1e..ff11dcd 100644 --- a/docs/plans/2026-05-26-akka-hosting-alignment-plan.md.tasks.json +++ b/docs/plans/2026-05-26-akka-hosting-alignment-plan.md.tasks.json @@ -4,22 +4,27 @@ "designDoc": "docs/plans/2026-05-26-akka-hosting-alignment-design.md", "lastUpdated": "2026-05-26T00:00:00Z", "tasks": [ - {"id": 0, "subject": "Task 0: Create branch and central package management", "status": "pending", "classification": "small", "estMinutes": 3, "parallelizableWith": []}, - {"id": 1, "subject": "Task 1: Create OtOpcUa.Commons project", "status": "pending", "classification": "small", "estMinutes": 3, "parallelizableWith": [2,3,4,5,6,7,8], "blockedBy": [0]}, - {"id": 2, "subject": "Task 2: Create OtOpcUa.Cluster project", "status": "pending", "classification": "small", "estMinutes": 3, "parallelizableWith": [1,3,4,5,6,7,8], "blockedBy": [0]}, - {"id": 3, "subject": "Task 3: Create OtOpcUa.Security project", "status": "pending", "classification": "small", "estMinutes": 3, "parallelizableWith": [1,2,4,5,6,7,8], "blockedBy": [0]}, - {"id": 4, "subject": "Task 4: Create OtOpcUa.ControlPlane project", "status": "pending", "classification": "small", "estMinutes": 3, "parallelizableWith": [1,2,3,5,6,7,8], "blockedBy": [0]}, - {"id": 5, "subject": "Task 5: Create OtOpcUa.Runtime project", "status": "pending", "classification": "small", "estMinutes": 3, "parallelizableWith": [1,2,3,4,6,7,8], "blockedBy": [0]}, - {"id": 6, "subject": "Task 6: Create OtOpcUa.OpcUaServer project", "status": "pending", "classification": "small", "estMinutes": 3, "parallelizableWith": [1,2,3,4,5,7,8], "blockedBy": [0]}, - {"id": 7, "subject": "Task 7: Create OtOpcUa.AdminUI Razor class library", "status": "pending", "classification": "small", "estMinutes": 3, "parallelizableWith": [1,2,3,4,5,6,8], "blockedBy": [0]}, - {"id": 8, "subject": "Task 8: Create OtOpcUa.Host Web SDK project", "status": "pending", "classification": "small", "estMinutes": 5, "parallelizableWith": [1,2,3,4,5,6,7], "blockedBy": [0]}, - {"id": 9, "subject": "Task 9: Build green smoke check", "status": "pending", "classification": "trivial", "estMinutes": 2, "parallelizableWith": [], "blockedBy": [1,2,3,4,5,6,7,8]}, - {"id": 10, "subject": "Task 10: Add Deployment entity", "status": "pending", "classification": "standard", "estMinutes": 5, "parallelizableWith": [11,12,13], "blockedBy": [9]}, - {"id": 11, "subject": "Task 11: Add NodeDeploymentState entity", "status": "pending", "classification": "standard", "estMinutes": 5, "parallelizableWith": [10,12,13], "blockedBy": [9]}, - {"id": 12, "subject": "Task 12: Add ConfigEdit audit entity", "status": "pending", "classification": "small", "estMinutes": 4, "parallelizableWith": [10,11,13], "blockedBy": [9]}, - {"id": 13, "subject": "Task 13: Add DataProtection keys table", "status": "pending", "classification": "small", "estMinutes": 3, "parallelizableWith": [10,11,12], "blockedBy": [9]}, - {"id": 14, "subject": "Task 14: EF migration V2HostingAlignment", "status": "pending", "classification": "high-risk", "estMinutes": 5, "parallelizableWith": [], "blockedBy": [10,11,12,13]}, - {"id": 15, "subject": "Task 15: Migrate-To-V2.ps1 idempotent script", "status": "pending", "classification": "standard", "estMinutes": 5, "parallelizableWith": [16,17,18], "blockedBy": [14]}, + {"id": 0, "subject": "Task 0: Create branch and central package management", "status": "completed", "classification": "small", "estMinutes": 3, "parallelizableWith": [], "commit": "2b81147"}, + {"id": 1, "subject": "Task 1: Create OtOpcUa.Commons project", "status": "completed", "classification": "small", "estMinutes": 3, "parallelizableWith": [2,3,4,5,6,7,8], "blockedBy": [0], "commit": "30a2104"}, + {"id": 2, "subject": "Task 2: Create OtOpcUa.Cluster project", "status": "completed", "classification": "small", "estMinutes": 3, "parallelizableWith": [1,3,4,5,6,7,8], "blockedBy": [0], "commit": "30a2104"}, + {"id": 3, "subject": "Task 3: Create OtOpcUa.Security project", "status": "completed", "classification": "small", "estMinutes": 3, "parallelizableWith": [1,2,4,5,6,7,8], "blockedBy": [0], "commit": "30a2104"}, + {"id": 4, "subject": "Task 4: Create OtOpcUa.ControlPlane project", "status": "completed", "classification": "small", "estMinutes": 3, "parallelizableWith": [1,2,3,5,6,7,8], "blockedBy": [0], "commit": "30a2104"}, + {"id": 5, "subject": "Task 5: Create OtOpcUa.Runtime project", "status": "completed", "classification": "small", "estMinutes": 3, "parallelizableWith": [1,2,3,4,6,7,8], "blockedBy": [0], "commit": "30a2104"}, + {"id": 6, "subject": "Task 6: Create OtOpcUa.OpcUaServer project", "status": "completed", "classification": "small", "estMinutes": 3, "parallelizableWith": [1,2,3,4,5,7,8], "blockedBy": [0], "commit": "30a2104"}, + {"id": 7, "subject": "Task 7: Create OtOpcUa.AdminUI Razor class library", "status": "completed", "classification": "small", "estMinutes": 3, "parallelizableWith": [1,2,3,4,5,6,8], "blockedBy": [0], "commit": "30a2104"}, + {"id": 8, "subject": "Task 8: Create OtOpcUa.Host Web SDK project", "status": "completed", "classification": "small", "estMinutes": 5, "parallelizableWith": [1,2,3,4,5,6,7], "blockedBy": [0], "commit": "30a2104"}, + {"id": 9, "subject": "Task 9: Build green smoke check", "status": "completed", "classification": "trivial", "estMinutes": 2, "parallelizableWith": [], "blockedBy": [1,2,3,4,5,6,7,8], "commit": "30a2104"}, + {"id": 10, "subject": "Task 10: Add Deployment entity", "status": "completed", "classification": "standard", "estMinutes": 5, "parallelizableWith": [11,12,13], "blockedBy": [9], "commit": "8e2c4f2"}, + {"id": 11, "subject": "Task 11: Add NodeDeploymentState entity", "status": "completed", "classification": "standard", "estMinutes": 5, "parallelizableWith": [10,12,13], "blockedBy": [9], "commit": "8e2c4f2"}, + {"id": 12, "subject": "Task 12: Add ConfigEdit audit entity", "status": "completed", "classification": "small", "estMinutes": 4, "parallelizableWith": [10,11,13], "blockedBy": [9], "commit": "8e2c4f2"}, + {"id": 13, "subject": "Task 13: Add DataProtection keys table", "status": "completed", "classification": "small", "estMinutes": 3, "parallelizableWith": [10,11,12], "blockedBy": [9], "commit": "8e2c4f2"}, + {"id": "14a", "subject": "Task 14a: Add RowVersion to live-edit entities", "status": "pending", "classification": "standard", "estMinutes": 10, "parallelizableWith": [], "blockedBy": [13]}, + {"id": "14b", "subject": "Task 14b: Decouple live-edit entities from ConfigGeneration", "status": "pending", "classification": "high-risk", "estMinutes": 30, "parallelizableWith": [], "blockedBy": ["14a"]}, + {"id": "14c", "subject": "Task 14c: Obsolete GenerationApplier/Diff/SealedCache", "status": "pending", "classification": "high-risk", "estMinutes": 20, "parallelizableWith": [], "blockedBy": ["14b"]}, + {"id": "14d", "subject": "Task 14d: Drop ClusterNode.RedundancyRole", "status": "pending", "classification": "standard", "estMinutes": 5, "parallelizableWith": ["14a","14b","14c"], "blockedBy": [13]}, + {"id": "14e", "subject": "Task 14e: Delete ConfigGeneration + ClusterNodeGenerationState", "status": "pending", "classification": "small", "estMinutes": 5, "parallelizableWith": [], "blockedBy": ["14b","14c"]}, + {"id": "14f", "subject": "Task 14f: V2HostingAlignment EF migration (consolidator)", "status": "pending", "classification": "high-risk", "estMinutes": 15, "parallelizableWith": [], "blockedBy": ["14a","14b","14c","14d","14e"]}, + {"id": 15, "subject": "Task 15: Migrate-To-V2.ps1 idempotent script", "status": "pending", "classification": "standard", "estMinutes": 5, "parallelizableWith": [16,17,18], "blockedBy": ["14f"]}, {"id": 16, "subject": "Task 16: Common types (CorrelationId, ExecutionId, NodeId, ...)", "status": "pending", "classification": "standard", "estMinutes": 5, "parallelizableWith": [17,18], "blockedBy": [9]}, {"id": 17, "subject": "Task 17: Akka message contracts", "status": "pending", "classification": "standard", "estMinutes": 5, "parallelizableWith": [16,18], "blockedBy": [16]}, {"id": 18, "subject": "Task 18: Common interfaces", "status": "pending", "classification": "small", "estMinutes": 4, "parallelizableWith": [16,17], "blockedBy": [16]},