From 2b5949320cb100301ad2007907407a33f7ae4dc2 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 24 Jun 2026 15:51:26 -0400 Subject: [PATCH] =?UTF-8?q?feat(templateengine+centralui):=20resolve=20fol?= =?UTF-8?q?low-ups=20#1/#2=20=E2=80=94=20inherited-member=20propagation=20?= =?UTF-8?q?&=20resync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Derived templates store IsInherited placeholder rows mirroring inherited members, but a base member added/changed/removed AFTER a child was derived never reached the child — leaving the editor's editable tabs incomplete (#1) and stored rows drifted from the resolved set (#2). Fix (one order-independent reconcile, two entry points): - Auto-propagation: every attribute/alarm/script add/update/delete now reconciles the template's derived subtree (TemplateService.ReconcileDescendantsAsync), hooked into all member-mutating paths incl. native-alarm-source CRUD in the ManagementActor. - Resync: ResyncInheritedMembersAsync repairs a template + its subtree on demand — materialize missing placeholders, re-sync drifted ones, remove orphans, across attributes/alarms/scripts/native sources. Exposed as management ResyncInheritedMembersCommand (Designer-gated, audited) → CLI `template resync-members` → a Resync button on the editor's staleness banner. Reconcile drives off TemplateInheritanceResolver (same precedence + HiLo merge as deploy), only ever touches IsInherited placeholders (never an authored override), and matches the staleness comparison keys so the banner clears. BuildDerivedTemplate now also materializes native-source placeholders at compose time (previously omitted → any inherited native source was perpetually stale). Tests: +8 TemplateServiceTests (materialize / drift-update / orphan-remove / override-untouched / base-cascade / multi-type / direct-propagate / end-to-end add) + 1 ManagementService test fix (native-source add resolves TemplateService). Affected suites green: TemplateEngine 446, ManagementService 230, CentralUI 866, CLI 333, Transport 127, ConfigurationDatabase 307; full solution builds 0/0. Docs: Component-TemplateEngine.md "Inherited-Member Propagation & Resync"; CLI README `template resync-members`; known-issues tracker #1/#2 resolved. --- ...mplate-inheritance-ui-and-cli-followups.md | 12 +- docs/requirements/Component-TemplateEngine.md | 9 + .../Commands/TemplateCommands.cs | 19 + src/ZB.MOM.WW.ScadaBridge.CLI/README.md | 12 + .../Pages/Design/TemplateEdit.razor | 63 ++- .../Management/ResolvedTemplateMembers.cs | 17 + .../Messages/Management/TemplateCommands.cs | 12 + .../ManagementActor.cs | 38 +- .../TemplateService.cs | 469 ++++++++++++++++++ .../ManagementActorTests.cs | 4 + .../TemplateServiceTests.cs | 237 +++++++++ 11 files changed, 873 insertions(+), 19 deletions(-) diff --git a/docs/known-issues/2026-06-24-template-inheritance-ui-and-cli-followups.md b/docs/known-issues/2026-06-24-template-inheritance-ui-and-cli-followups.md index b07f7198..4197fcfa 100644 --- a/docs/known-issues/2026-06-24-template-inheritance-ui-and-cli-followups.md +++ b/docs/known-issues/2026-06-24-template-inheritance-ui-and-cli-followups.md @@ -3,14 +3,17 @@ **Status:** PARTIALLY RESOLVED · **Found:** 2026-06-24 · **Context:** live ops session on `wonder-app-vd03` (CvdReactor / Z28061 / Z28061Sim) — renaming the template, adding the LeakTest module, and adding MoveInType to the MESReceiver children. **Components:** Central UI (#9), Template Engine (#1), CLI (#19), Configuration Database (#17) -**Resolved:** #3 (collision detector) and #7 (sandbox compile surface) fixed on branch `fix/followups-3-7` (2026-06-24). Open: #1, #2, #4, #5, #6, #8. +**Resolved:** #3 (collision detector) and #7 (sandbox compile surface) on branch `fix/followups-3-7`; #1 + #2 (inherited-member propagation & resync) on branch `fix/followups-1-2` (2026-06-24). Open: #4, #5, #6, #8. Issues are listed worst-first. Severities are author estimates. None caused data loss; the runtime/flattened config and deployed instances are correct. --- ## 1. Template editor omits inherited-but-unmaterialized base attributes (user-reported) -**Severity:** Medium · **Components:** Central UI (#9), Template Engine (#1) +**Severity:** Medium · **Components:** Central UI (#9), Template Engine (#1) · **✅ RESOLVED 2026-06-24 (branch `fix/followups-1-2`)** + +**Fix:** shared root cause with #2 — see #2's fix. Once a template's inherited rows are materialized (auto-propagation going forward, or the Resync action for already-stale templates), the editor's editable Attributes/Alarms/Scripts tabs list them. The "base changed" banner is now actionable: it carries a **Resync inherited members** button (`TemplateEdit.razor`) that calls `TemplateService.ResyncInheritedMembersAsync` and reloads. The read-only "Effective inherited set" preview is retained. + **Symptom:** On `/design/templates` → `LeftMESReceiver`, the **Attributes** tab does not list `MoveInType`. Same for `RightMESReceiver`. (Also missing from the list: all `MoveOut*` and `ScanStateCmd`.) @@ -23,7 +26,10 @@ Issues are listed worst-first. Severities are author estimates. None caused data --- ## 2. Derived templates carry incomplete/stale `IsInherited` row sets -**Severity:** Medium · **Components:** Template Engine (#1), Configuration Database (#17) +**Severity:** Medium · **Components:** Template Engine (#1), Configuration Database (#17) · **✅ RESOLVED 2026-06-24 (branch `fix/followups-1-2`)** + +**Fix:** root-cause fix — derived templates' stored inherited rows are now kept in sync two ways. (1) **Auto-propagation:** adding/updating/removing a member on a template now reconciles its entire derived subtree (`TemplateService.ReconcileDescendantsAsync`, called from every member-mutating path incl. native-alarm-source CRUD in the ManagementActor). (2) **Resync:** `ResyncInheritedMembersAsync` (CLI `template resync-members`, management `ResyncInheritedMembersCommand`, Designer-gated, audited; UI banner button) repairs a template + its subtree on demand — materializing missing placeholders, re-syncing drifted ones, removing orphans, across attributes/alarms/scripts/native sources. `BuildDerivedTemplate` also now materializes native-source placeholders at compose time (previously omitted, which made any inherited native source perpetually stale). Authored overrides are never touched. Covered by `TemplateServiceTests` (materialize / drift-update / orphan-remove / override-untouched / base-cascade / multi-type / propagation / end-to-end add). Documented in `Component-TemplateEngine.md` → "Inherited-Member Propagation & Resync". + **Symptom:** `LeftMESReceiver`/`RightMESReceiver` (parent=3) have 12 stored attribute rows vs the base's 26. By contrast `LeftReactorSide`/`RightReactorSide` (parent=7) mirror the full 61. So derived row-sets are inconsistent. diff --git a/docs/requirements/Component-TemplateEngine.md b/docs/requirements/Component-TemplateEngine.md index 33b07de1..b2bd9835 100644 --- a/docs/requirements/Component-TemplateEngine.md +++ b/docs/requirements/Component-TemplateEngine.md @@ -158,6 +158,15 @@ The resolved result carries, per member: The resolver is consumed only by the Central UI `TemplateEdit` page. It is not part of the flattening pipeline and is not called during deployment. +## Inherited-Member Propagation & Resync + +A derived template stores `IsInherited` **placeholder rows** mirroring every member it inherits (attributes, alarms, scripts, native alarm sources). These placeholders are what the editor's editable member tabs render and what the staleness summary above compares against. They are materialized when the derived template is created (compose / inherit), and kept in sync by two mechanisms so the stored rows never drift incomplete: + +- **Auto-propagation.** Whenever a member is **added, updated, or removed** on a template, the change is propagated to that template's entire derived subtree: a missing inherited placeholder is materialized, a drifted one is re-synced to the live effective value, and an orphaned one (its base member removed) is deleted. This runs automatically as part of the member-mutating commands, so children stay complete going forward. +- **Resync (operator repair).** `ResyncInheritedMembersAsync` (CLI `template resync-members`, and a **Resync** button on the editor's "base changed" banner) reconciles a template **and its subtree** on demand — repairing templates that drifted before auto-propagation existed (e.g. base members added after a child was derived). Resyncing a base repairs every derivation; resyncing a leaf repairs just it. + +Both delegate to one **order-independent reconcile** that compares a template's stored inherited rows against the inheritance resolver's effective set (the same precedence + HiLo merge the editor preview and deploy use) and only ever touches `IsInherited` placeholder rows — never an authored override (`IsInherited == false`). Because the resolver ignores placeholder rows when picking winners, reconciling one template never changes what another resolves, so the operation needs no particular ordering. The effective value mirrored into each placeholder matches the staleness comparison key per member type, so after a reconcile the "base changed" banner clears. Reconcile is best-effort housekeeping for the *stored authoring rows*: a deploy always re-resolves the chain fresh regardless, so a not-yet-resynced template still deploys correctly. + ## Diff Calculation The Template Engine can compare: diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs index b9a1d1c3..c1819e39 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs @@ -29,10 +29,29 @@ public static class TemplateCommands command.Add(BuildNativeAlarmSource(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildScript(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildComposition(urlOption, formatOption, usernameOption, passwordOption)); + command.Add(BuildResyncMembers(urlOption, formatOption, usernameOption, passwordOption)); return command; } + private static Command BuildResyncMembers(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) + { + var idOption = new Option("--id") { Description = "Template ID (its derived subtree is included)", Required = true }; + var cmd = new Command("resync-members") + { + Description = "Resync inherited members onto a template and its derived subtree " + + "(materialize missing, re-sync drifted, remove orphaned)" + }; + cmd.Add(idOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + return await CommandHelpers.ExecuteCommandAsync( + result, urlOption, formatOption, usernameOption, passwordOption, new ResyncInheritedMembersCommand(id)); + }); + return cmd; + } + private static Command BuildList(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var cmd = new Command("list") { Description = "List all templates" }; diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/README.md b/src/ZB.MOM.WW.ScadaBridge.CLI/README.md index 4547724e..1aa44e44 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/README.md +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/README.md @@ -403,6 +403,18 @@ scadabridge --url template composition delete --id |--------|----------|-------------| | `--id` | yes | Composition ID to remove | +#### `template resync-members` + +Reconcile a template's stored *inherited* member rows (and those of its whole derived subtree) with the resolved effective inherited set: materialize missing inherited placeholders (e.g. base members added after a derived template was created), re-sync drifted ones to the live base value, and remove orphaned ones. After a resync the template editor's member tabs are complete and the "Base template changed" banner clears. Authored overrides are never touched. Base member changes auto-propagate going forward; this command repairs templates that drifted before that was in place. Returns the per-subtree change counts (templates changed, members added/updated/removed). + +```sh +scadabridge --url template resync-members --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Template ID (its derived subtree is included) | + --- ### `instance` — Manage instances diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TemplateEdit.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TemplateEdit.razor index a618375a..74933952 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TemplateEdit.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TemplateEdit.razor @@ -288,18 +288,29 @@ } - @* M9-T26b: read-only base-changed banner. Informational only — surfaced - when the freshly-resolved inherited set differs from this template's - stored copy (a multi-level inherited member, or a base member added - after this template was created). No action button: a deploy already - resolves fresh, so this is purely an authoring heads-up. *@ + @* Base-changed banner (follow-up #1/#2). Surfaced when the freshly-resolved + inherited set differs from this template's stored copy (a base member + added/changed/removed after this template was created, or a multi-level + inherited member). The Resync action materializes the missing inherited + rows, re-syncs drifted ones, and removes orphans on this template and its + derived subtree — so the editable tabs below become complete and the + banner clears. (A deploy already resolves fresh regardless.) *@ @if (_resolved?.Staleness.IsStale == true) { -
- - Base template changed — - @_resolved.Staleness.DifferingMemberCount inherited member(s) differ from this template's stored copy. - The effective set above reflects the live base; a deploy resolves fresh. +
+
+ + Base template changed — + @_resolved.Staleness.DifferingMemberCount inherited member(s) differ from this template's stored copy. + Resync to bring this template's stored members in line with the base. +
+
} @@ -944,6 +955,38 @@ else _toast.ShowError(result.Error); } + private bool _resyncing; + + // Follow-up #1/#2: materialize/refresh this template's (and its subtree's) + // inherited member rows so the editable tabs are complete and the + // base-changed banner clears. In-process TemplateService call (the page is + // already RequireDesign-gated, matching the management command's role gate). + private async Task ResyncInheritedMembers() + { + if (_resyncing) return; + _resyncing = true; + try + { + var user = await GetCurrentUserAsync(); + var result = await TemplateService.ResyncInheritedMembersAsync(Id, user); + if (result.IsSuccess) + { + var r = result.Value; + _toast.ShowSuccess( + $"Resynced inherited members — {r.MembersAdded} added, {r.MembersUpdated} updated, {r.MembersRemoved} removed across {r.TemplatesChanged} template(s)."); + await LoadAsync(); + } + else + { + _toast.ShowError(result.Error); + } + } + finally + { + _resyncing = false; + } + } + // ---- Alarms Tab ---- private RenderFragment RenderAlarmsTab() => __builder => { diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ResolvedTemplateMembers.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ResolvedTemplateMembers.cs index 545aae2e..64080e5b 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ResolvedTemplateMembers.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ResolvedTemplateMembers.cs @@ -105,6 +105,23 @@ public sealed record ResolvedTemplateMemberInfo public string? EffectiveTriggerConfiguration { get; init; } } +/// +/// Result of a : how many derived +/// templates were brought into sync and how many inherited member rows were +/// added / updated / removed across the reconciled subtree. +/// +public sealed record ResyncInheritedMembersResult +{ + /// Number of templates in the subtree that had at least one inherited-row change. + public int TemplatesChanged { get; init; } + /// Inherited placeholder rows materialized (members missing from the stored set). + public int MembersAdded { get; init; } + /// Inherited placeholder rows re-synced to the live effective value. + public int MembersUpdated { get; init; } + /// Orphaned inherited placeholder rows removed (the base no longer defines them). + public int MembersRemoved { get; init; } +} + /// /// Staleness summary comparing a template's STORED member rows against the /// freshly-resolved inherited member set. diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs index 0063f8c1..35426680 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs @@ -17,6 +17,18 @@ public record ValidateTemplateCommand(int TemplateId); /// public record GetResolvedTemplateMembersCommand(int TemplateId); +/// +/// Reconciles a template's STORED inherited member rows with the resolved +/// effective inherited set, for the template and its whole derived subtree: +/// materializes missing inherited placeholders (e.g. base members added after a +/// derived template was created), re-syncs drifted placeholders to the live +/// effective value, and removes orphaned inherited rows. After a resync the +/// editor's editable member tabs are complete and the "base changed" staleness +/// banner clears. Designer-gated, audited. Response: +/// . +/// +public record ResyncInheritedMembersCommand(int TemplateId); + // Template member operations public record AddTemplateAttributeCommand(int TemplateId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked, string? ElementDataType = null); public record UpdateTemplateAttributeCommand(int AttributeId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked, string? ElementDataType = null); diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs index 25a88365..c30b6239 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs @@ -200,6 +200,7 @@ public class ManagementActor : ReceiveActor or AddTemplateNativeAlarmSourceCommand or UpdateTemplateNativeAlarmSourceCommand or DeleteTemplateNativeAlarmSourceCommand or AddTemplateScriptCommand or UpdateTemplateScriptCommand or DeleteTemplateScriptCommand or AddTemplateCompositionCommand or DeleteTemplateCompositionCommand + or ResyncInheritedMembersCommand or CreateSharedScriptCommand or UpdateSharedScriptCommand or DeleteSharedScriptCommand or CreateSharedSchemaCommand or UpdateSharedSchemaCommand or DeleteSharedSchemaCommand or CreateDatabaseConnectionDefCommand or UpdateDatabaseConnectionDefCommand or DeleteDatabaseConnectionDefCommand @@ -253,6 +254,7 @@ public class ManagementActor : ReceiveActor DeleteTemplateCommand cmd => await HandleDeleteTemplate(sp, cmd, user.Username), ValidateTemplateCommand cmd => await HandleValidateTemplate(sp, cmd), GetResolvedTemplateMembersCommand cmd => await HandleGetResolvedTemplateMembers(sp, cmd), + ResyncInheritedMembersCommand cmd => await HandleResyncInheritedMembers(sp, cmd, user.Username), // Template members AddTemplateAttributeCommand cmd => await HandleAddAttribute(sp, cmd, user.Username), @@ -261,9 +263,9 @@ public class ManagementActor : ReceiveActor AddTemplateAlarmCommand cmd => await HandleAddAlarm(sp, cmd, user.Username), UpdateTemplateAlarmCommand cmd => await HandleUpdateAlarm(sp, cmd, user.Username), DeleteTemplateAlarmCommand cmd => await HandleDeleteAlarm(sp, cmd, user.Username), - AddTemplateNativeAlarmSourceCommand cmd => await HandleAddNativeAlarmSource(sp, cmd), - UpdateTemplateNativeAlarmSourceCommand cmd => await HandleUpdateNativeAlarmSource(sp, cmd), - DeleteTemplateNativeAlarmSourceCommand cmd => await HandleDeleteNativeAlarmSource(sp, cmd), + AddTemplateNativeAlarmSourceCommand cmd => await HandleAddNativeAlarmSource(sp, cmd, user.Username), + UpdateTemplateNativeAlarmSourceCommand cmd => await HandleUpdateNativeAlarmSource(sp, cmd, user.Username), + DeleteTemplateNativeAlarmSourceCommand cmd => await HandleDeleteNativeAlarmSource(sp, cmd, user.Username), ListTemplateNativeAlarmSourcesCommand cmd => await HandleListNativeAlarmSources(sp, cmd), AddTemplateScriptCommand cmd => await HandleAddScript(sp, cmd, user.Username), UpdateTemplateScriptCommand cmd => await HandleUpdateScript(sp, cmd, user.Username), @@ -645,6 +647,20 @@ public class ManagementActor : ReceiveActor return TemplateInheritanceResolver.Resolve(cmd.TemplateId, allTemplates); } + /// + /// Resync inherited members (follow-up #1/#2): reconciles a template's stored + /// inherited rows (and its derived subtree's) with the resolved effective set — + /// materializing missing placeholders, re-syncing drifted ones, and removing + /// orphans — so the editor's editable tabs are complete and the staleness + /// banner clears. Designer-gated; the service owns the audit row. + /// + private static async Task HandleResyncInheritedMembers(IServiceProvider sp, ResyncInheritedMembersCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.ResyncInheritedMembersAsync(cmd.TemplateId, user); + return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error); + } + // ======================================================================== // Template folder handlers // ======================================================================== @@ -2226,7 +2242,7 @@ public class ManagementActor : ReceiveActor // ── Native alarm source bindings (read-only mirror; repository-direct CRUD) ── - private static async Task HandleAddNativeAlarmSource(IServiceProvider sp, AddTemplateNativeAlarmSourceCommand cmd) + private static async Task HandleAddNativeAlarmSource(IServiceProvider sp, AddTemplateNativeAlarmSourceCommand cmd, string user) { var repo = sp.GetRequiredService(); var source = new TemplateNativeAlarmSource(cmd.Name) @@ -2240,10 +2256,13 @@ public class ManagementActor : ReceiveActor }; await repo.AddTemplateNativeAlarmSourceAsync(source); await repo.SaveChangesAsync(); + // Propagate the new source to derived descendants (#1/#2). Native-source + // CRUD lives here (not TemplateService), so call the propagation directly. + await sp.GetRequiredService().ReconcileDescendantsAsync(cmd.TemplateId, user); return source; } - private static async Task HandleUpdateNativeAlarmSource(IServiceProvider sp, UpdateTemplateNativeAlarmSourceCommand cmd) + private static async Task HandleUpdateNativeAlarmSource(IServiceProvider sp, UpdateTemplateNativeAlarmSourceCommand cmd, string user) { var repo = sp.GetRequiredService(); var source = await repo.GetTemplateNativeAlarmSourceByIdAsync(cmd.NativeAlarmSourceId) @@ -2256,14 +2275,21 @@ public class ManagementActor : ReceiveActor source.IsLocked = cmd.IsLocked; await repo.UpdateTemplateNativeAlarmSourceAsync(source); await repo.SaveChangesAsync(); + // Propagate the changed source to derived descendants (#1/#2). + await sp.GetRequiredService().ReconcileDescendantsAsync(source.TemplateId, user); return source; } - private static async Task HandleDeleteNativeAlarmSource(IServiceProvider sp, DeleteTemplateNativeAlarmSourceCommand cmd) + private static async Task HandleDeleteNativeAlarmSource(IServiceProvider sp, DeleteTemplateNativeAlarmSourceCommand cmd, string user) { var repo = sp.GetRequiredService(); + // Capture the owning template before delete so descendants can be reconciled. + var source = await repo.GetTemplateNativeAlarmSourceByIdAsync(cmd.NativeAlarmSourceId); await repo.DeleteTemplateNativeAlarmSourceAsync(cmd.NativeAlarmSourceId); await repo.SaveChangesAsync(); + // Remove the now-orphaned inherited placeholder from derived descendants (#1/#2). + if (source != null) + await sp.GetRequiredService().ReconcileDescendantsAsync(source.TemplateId, user); return cmd.NativeAlarmSourceId; } diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs index 9b97fe20..9a6ab7c2 100644 --- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs @@ -1,6 +1,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; +using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; using ZB.MOM.WW.ScadaBridge.Commons.Types; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; @@ -302,6 +303,9 @@ public class TemplateService await _auditService.LogAsync(user, "Create", "TemplateAttribute", attribute.Id.ToString(), attribute.Name, attribute, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); + // Propagate the new member to derived descendants (#1/#2). + await ReconcileDescendantsAsync(templateId, user, cancellationToken); + return Result.Success(attribute); } @@ -396,6 +400,9 @@ public class TemplateService await _auditService.LogAsync(user, "Update", "TemplateAttribute", attributeId.ToString(), existing.Name, existing, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); + // Propagate the changed effective value to derived descendants (#1/#2). + await ReconcileDescendantsAsync(existing.TemplateId, user, cancellationToken); + return Result.Success(existing); } @@ -431,6 +438,9 @@ public class TemplateService await _auditService.LogAsync(user, "Delete", "TemplateAttribute", attributeId.ToString(), attribute.Name, null, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); + // Remove the now-orphaned inherited placeholder from derived descendants (#1/#2). + await ReconcileDescendantsAsync(attribute.TemplateId, user, cancellationToken); + return Result.Success(true); } @@ -526,6 +536,9 @@ public class TemplateService await _auditService.LogAsync(user, "Create", "TemplateAlarm", alarm.Id.ToString(), alarm.Name, alarm, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); + // Propagate the new member to derived descendants (#1/#2). + await ReconcileDescendantsAsync(templateId, user, cancellationToken); + return Result.Success(alarm); } @@ -608,6 +621,9 @@ public class TemplateService await _auditService.LogAsync(user, "Update", "TemplateAlarm", alarmId.ToString(), existing.Name, existing, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); + // Propagate the changed effective value to derived descendants (#1/#2). + await ReconcileDescendantsAsync(existing.TemplateId, user, cancellationToken); + return Result.Success(existing); } @@ -642,6 +658,9 @@ public class TemplateService await _auditService.LogAsync(user, "Delete", "TemplateAlarm", alarmId.ToString(), alarm.Name, null, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); + // Remove the now-orphaned inherited placeholder from derived descendants (#1/#2). + await ReconcileDescendantsAsync(alarm.TemplateId, user, cancellationToken); + return Result.Success(true); } @@ -687,6 +706,9 @@ public class TemplateService await _auditService.LogAsync(user, "Create", "TemplateScript", script.Id.ToString(), script.Name, script, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); + // Propagate the new member to derived descendants (#1/#2). + await ReconcileDescendantsAsync(templateId, user, cancellationToken); + return Result.Success(script); } @@ -770,6 +792,9 @@ public class TemplateService await _auditService.LogAsync(user, "Update", "TemplateScript", scriptId.ToString(), existing.Name, existing, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); + // Propagate the changed effective value to derived descendants (#1/#2). + await ReconcileDescendantsAsync(existing.TemplateId, user, cancellationToken); + return Result.Success(existing); } @@ -804,6 +829,9 @@ public class TemplateService await _auditService.LogAsync(user, "Delete", "TemplateScript", scriptId.ToString(), script.Name, null, cancellationToken); await _repository.SaveChangesAsync(cancellationToken); + // Remove the now-orphaned inherited placeholder from derived descendants (#1/#2). + await ReconcileDescendantsAsync(script.TemplateId, user, cancellationToken); + return Result.Success(true); } @@ -1090,9 +1118,450 @@ public class TemplateService }); } + // Native alarm sources follow the same inherit/override/lock model as the + // other member types (TemplateNativeAlarmSource), and the inheritance + // resolver + staleness summary count them — so materialize their inherited + // placeholders here too, or a freshly-composed derived template reports + // stale for every inherited native source (follow-up #1/#2). + foreach (var src in baseTemplate.NativeAlarmSources) + { + derived.NativeAlarmSources.Add(new TemplateNativeAlarmSource(src.Name) + { + Description = src.Description, + ConnectionName = src.ConnectionName, + SourceReference = src.SourceReference, + ConditionFilter = src.ConditionFilter, + IsLocked = src.IsLocked, + IsInherited = true, + LockedInDerived = false, + }); + } + return derived; } + // ======================================================================== + // Inherited-member propagation & resync (follow-up #1/#2) + // ======================================================================== + // + // A derived template stores IsInherited placeholder rows mirroring the + // members it inherits. BuildDerivedTemplate materializes them at compose time, + // but a member ADDED / UPDATED / REMOVED on a base AFTER the derivation + // existed never reached the children — so derived row-sets drifted incomplete + // (the editor's editable tabs missed inherited members; #1) and stored rows + // diverged from the resolved set (#2). Two mechanisms keep them in sync: + // • ReconcileDescendantsAsync — called automatically after every base member + // mutation, propagates the change to the whole derived subtree. + // • ResyncInheritedMembersAsync — an explicit operator action (CLI + UI) + // that repairs an already-stale template and its subtree. + // Both delegate to one order-independent reconcile that only ever touches + // IsInherited placeholder rows (never an authored override), which the + // inheritance resolver ignores when picking winners — so reconciling one + // template never changes what another resolves. + + /// + /// Reconciles the stored inherited member rows of a template and its entire + /// derived subtree against the resolved effective inherited set: materializes + /// missing placeholders, re-syncs drifted ones, and removes orphaned ones. + /// This is the operator-facing repair action behind the CLI + /// template resync-members command and the editor's "Resync" button. + /// + /// The template to resync (its subtree is included). + /// Username for the audit row. + /// Cancellation token. + /// A result carrying the per-subtree change counts. + public async Task> ResyncInheritedMembersAsync( + int templateId, + string user, + CancellationToken cancellationToken = default) + { + var all = await _repository.GetAllTemplatesAsync(cancellationToken) ?? Array.Empty