docs(comments): strip internal task/milestone/bundle bookkeeping from code comments

Remove project bookkeeping citations from shipped code comments across the
solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/
issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels,
and C/D/K/S/T phase labels.

Comment text only — no code logic, string/log literals, or XML-doc structure
changed. Genuine descriptions are preserved (only the citation is stripped),
and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365,
UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker
TaskReferenceInComment / TrackingReferenceInComment checks plus targeted
grep passes; full solution builds clean, append-only guard tests pass.
This commit is contained in:
Joseph Doherty
2026-07-07 11:03:26 -04:00
parent 67005ca4c0
commit 9cff87fe85
435 changed files with 2338 additions and 2547 deletions
@@ -17,8 +17,8 @@ using ZB.MOM.WW.ScadaBridge.Security;
namespace ZB.MOM.WW.ScadaBridge.ManagementService;
/// <summary>
/// Minimal-API endpoints exposing the central Audit Log (#23) over HTTP for the
/// ScadaBridge CLI (M8). Three routes:
/// Minimal-API endpoints exposing the central Audit Log over HTTP for the
/// ScadaBridge CLI. Three routes:
/// <list type="bullet">
/// <item><c>GET /api/audit/query</c> — keyset-paged JSON page, gated on the
/// <see cref="AuthorizationPolicies.OperationalAudit"/> permission.</item>
@@ -154,13 +154,13 @@ public static class AuditEndpoints
var last = canonical[^1];
nextCursor = new
{
// C3: canonical OccurredAtUtc is a DateTimeOffset; the cursor key is UTC.
// Canonical OccurredAtUtc is a DateTimeOffset; the cursor key is UTC.
afterOccurredAtUtc = last.OccurredAtUtc.UtcDateTime,
afterEventId = last.EventId,
};
}
// C3 (Task 2.5): decompose canonical rows into the flat AuditExportRow so the
// Decompose canonical rows into the flat AuditExportRow so the
// CLI's JSON shape (24-field) is unchanged.
var events = canonical.Select(AuditExportRow.From).ToList();
var payload = new { events, nextCursor };
@@ -501,7 +501,7 @@ public static class AuditEndpoints
}
var last = page[^1];
// C3: canonical OccurredAtUtc is a DateTimeOffset; the keyset cursor column is UTC.
// Canonical OccurredAtUtc is a DateTimeOffset; the keyset cursor column is UTC.
cursor = new AuditLogPaging(ExportPageSize, last.OccurredAtUtc.UtcDateTime, last.EventId);
}
}
@@ -534,7 +534,7 @@ public static class AuditEndpoints
/// <summary>
/// Applies the caller's <see cref="AuthenticatedUser.PermittedSiteIds"/> to the
/// audit-log filter (Management-019). System-wide callers (empty PermittedSiteIds —
/// audit-log filter. System-wide callers (empty PermittedSiteIds —
/// Admin or a Deployment-style role with no scope rules attached to its mapping)
/// see the filter unchanged. A scoped caller has any caller-supplied
/// <c>sourceSiteId</c> intersected with their permitted set: an empty caller filter
@@ -552,7 +552,7 @@ public static class AuditEndpoints
{
// Empty PermittedSiteIds is the system-wide signal (Administrator,
// system-wide Deployer). System-wide audit roles also fall here — the
// design treats Administrator/Viewer (the post-Task-1.7 audit roles) as
// design treats Administrator/Viewer (the audit roles) as
// non-site-scoped unless an operator attaches scope rules to the LDAP
// mapping; if they do, this helper enforces them.
if (user.PermittedSiteIds.Length == 0)
@@ -6,8 +6,8 @@ namespace ZB.MOM.WW.ScadaBridge.ManagementService;
/// <summary>
/// Flat, wire-shape view of a canonical <see cref="ZB.MOM.WW.Audit.AuditEvent"/> for the
/// management CLI's <c>/api/audit/query</c> + <c>/api/audit/export</c> endpoints. C3
/// (Task 2.5) made the canonical record the repository seam type; this DTO preserves the
/// management CLI's <c>/api/audit/query</c> + <c>/api/audit/export</c> endpoints. The
/// canonical record was later made the repository seam type; this DTO preserves the
/// existing 24-field JSON/CSV shape the CLI consumes by decomposing the canonical row
/// (via <see cref="AuditRowProjection"/>) at the endpoint boundary.
/// </summary>
@@ -61,8 +61,7 @@ public class ManagementActor : ReceiveActor
/// Akka.NET conventions, coordinator-style actors use a Resume-based strategy so a faulted
/// child preserves its state rather than restarting. <see cref="ManagementActor"/> spawns
/// no children today, but declaring the strategy explicitly matches the convention and
/// makes the contract correct ahead of any future worker actors (finding
/// ManagementService-005).
/// makes the contract correct ahead of any future worker actors.
/// </summary>
/// <returns>A one-for-one Resume strategy with no retry limit.</returns>
public static SupervisorStrategy CreateSupervisorStrategy() =>
@@ -77,7 +76,7 @@ public class ManagementActor : ReceiveActor
/// <summary>
/// Serializer settings for command results. <see cref="ReferenceHandler.IgnoreCycles"/>
/// keeps EF-backed entity graphs with bidirectional navigation properties from throwing;
/// camelCase matches what the CLI / HTTP layer expect (finding ManagementService-007).
/// camelCase matches what the CLI / HTTP layer expect.
/// </summary>
private static readonly JsonSerializerOptions ResultSerializerOptions = new()
{
@@ -151,7 +150,7 @@ public class ManagementActor : ReceiveActor
// unanticipated fault whose raw .Message can disclose internal detail
// (server/database names, constraint names, stack context) — return a
// generic message and let the operator correlate via the server log
// using the correlation ID (finding ManagementService-016).
// using the correlation ID.
var clientMessage = cause is ManagementCommandException
? cause.Message
: $"An internal error occurred (CorrelationId={correlationId}).";
@@ -173,9 +172,9 @@ public class ManagementActor : ReceiveActor
// (which requires OperationalAuditRoles). New audit consumers
// should use the REST endpoint; this command is retained for
// backward compatibility with the CentralUI Configuration Audit
// Log page (Management-018).
// Log page.
or QueryAuditLogCommand
// MgmtSvc-021: SMTP/SMS provider-config updates rotate stored secrets
// SMTP/SMS provider-config updates rotate stored secrets
// (SMTP Credentials, Twilio AuthToken). Both the /notifications/smtp and
// /notifications/sms Central UI pages enforce RequireAdmin, so the actor
// gate must match — otherwise a Designer blocked in the UI could still
@@ -229,7 +228,7 @@ public class ManagementActor : ReceiveActor
or RetryParkedMessageCommand or DiscardParkedMessageCommand
or DebugSnapshotCommand => Roles.Deployer,
// Two-person secured write (M7 / T14b). Submit is an Operator action;
// Two-person secured write. Submit is an Operator action;
// approve/reject are Verifier actions. Separation of duties (a write may
// not be verified by its submitter) is enforced inside the handler — the
// role gate only ensures the caller holds the right coarse role.
@@ -349,7 +348,7 @@ public class ManagementActor : ReceiveActor
UpdateSharedScriptCommand cmd => await HandleUpdateSharedScript(sp, cmd, user.Username),
DeleteSharedScriptCommand cmd => await HandleDeleteSharedScript(sp, cmd, user.Username),
// Schema Library (M9-T32c)
// Schema Library
ListSharedSchemasCommand => await HandleListSharedSchemas(sp),
GetSharedSchemaCommand cmd => await HandleGetSharedSchema(sp, cmd),
CreateSharedSchemaCommand cmd => await HandleCreateSharedSchema(sp, cmd, user.Username),
@@ -403,15 +402,15 @@ public class ManagementActor : ReceiveActor
DiscardParkedMessageCommand cmd => await HandleDiscardParkedMessage(sp, cmd, user),
DebugSnapshotCommand cmd => await HandleDebugSnapshot(sp, cmd, user),
// Secured writes (M7 / T14b). Approve executes the write — once a
// Secured writes. Approve executes the write — once a
// Verifier wins the compare-and-swap the value is relayed to the site
// MxGateway connection (Task C3).
// MxGateway connection.
SubmitSecuredWriteCommand cmd => await HandleSubmitSecuredWrite(sp, cmd, user),
ApproveSecuredWriteCommand cmd => await HandleApproveSecuredWrite(sp, cmd, user),
RejectSecuredWriteCommand cmd => await HandleRejectSecuredWrite(sp, cmd, user),
ListSecuredWritesCommand cmd => await HandleListSecuredWrites(sp, cmd),
// Transport (#24) bundle operations
// Transport bundle operations
ExportBundleCommand cmd => await HandleExportBundle(sp, cmd, user.Username),
PreviewBundleCommand cmd => await HandlePreviewBundle(sp, cmd),
ImportBundleCommand cmd => await HandleImportBundle(sp, cmd, user.Username),
@@ -420,8 +419,7 @@ public class ManagementActor : ReceiveActor
// "ResolveRoles + command" flow is retired — the HTTP endpoint performs LDAP
// auth and role resolution itself before sending a single envelope. Leaving a
// handler would expose role-mapping data to any raw ClusterClient sender with
// no role requirement; the command now falls through to the default below
// (finding ManagementService-011).
// no role requirement; the command now falls through to the default below.
_ => throw new NotSupportedException($"Unknown command type: {command.GetType().Name}")
};
}
@@ -482,7 +480,7 @@ public class ManagementActor : ReceiveActor
/// Logs an audit entry after a successful mutation.
/// </summary>
/// <remarks>
/// Audit-logging contract (finding ManagementService-009). Every mutating operation is
/// Audit-logging contract. Every mutating operation is
/// audited exactly once, by whichever layer owns the write:
/// <list type="bullet">
/// <item>Handlers that mutate a repository directly (site, area, data-connection,
@@ -591,7 +589,7 @@ public class ManagementActor : ReceiveActor
}).ToList()
};
// M9-T32b: supply the JSON-Schema $ref resolution seam from the shared-schema
// Supply the JSON-Schema $ref resolution seam from the shared-schema
// library so a dangling {"$ref":"lib:Name"} in a template script schema is flagged
// here (design-time validate) consistently with the deploy path. The library is
// pre-loaded into a name→JSON map (the seam ValidationService consumes is sync).
@@ -600,7 +598,7 @@ public class ManagementActor : ReceiveActor
var schemaLibrary = sharedSchemas.ToDictionary(s => s.Name, s => s.SchemaJson, StringComparer.Ordinal);
Func<string, string?> resolveSchemaRef = name => schemaLibrary.GetValueOrDefault(name);
// #259: also pass shared scripts so a dangling $ref in a SHARED script's schema is
// Also pass shared scripts so a dangling $ref in a SHARED script's schema is
// caught at design-time (not only on the deploy path). Mirror the deploy-path mapping
// in FlatteningPipeline: SharedScript entity → ResolvedScript (name + schema fields).
var sharedScriptEntities = await repo.GetAllSharedScriptsAsync();
@@ -634,7 +632,7 @@ public class ManagementActor : ReceiveActor
}
/// <summary>
/// Read-only authoring resolve (M9/T26a): returns the effective inherited
/// Read-only authoring resolve: returns the effective inherited
/// member set for a template — computed fresh from the full inheritance
/// chain — plus a staleness summary. Loads every template (children
/// eager-loaded) so the resolver can walk an arbitrary-depth chain; never
@@ -648,7 +646,7 @@ public class ManagementActor : ReceiveActor
}
/// <summary>
/// Resync inherited members (follow-up #1/#2): reconciles a template's stored
/// Resync inherited members: 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
@@ -803,7 +801,7 @@ public class ManagementActor : ReceiveActor
{
await EnforceSiteScopeForInstance(sp, user, cmd.InstanceId);
// Multi-override apply is all-or-nothing (finding ManagementService-015).
// Multi-override apply is all-or-nothing.
// InstanceService.SetAttributeOverrideAsync commits each override
// independently, so a mid-batch failure on an invalid attribute would
// otherwise leave the instance partially mutated. Validate every
@@ -962,10 +960,10 @@ public class ManagementActor : ReceiveActor
}
// ========================================================================
// Secured-write handlers (M7 / T14b)
// Secured-write handlers
//
// Two-person workflow: an Operator submits a pending write against an
// MxGateway data connection; a distinct Verifier approves (Task C3) or
// MxGateway data connection; a distinct Verifier approves or
// rejects it. The role gate (GetRequiredRole) only verifies the coarse role;
// the separation-of-duties rule (a write may not be verified by its
// submitter) is enforced here.
@@ -999,7 +997,7 @@ public class ManagementActor : ReceiveActor
/// identically to every other emit site.
/// </summary>
/// <remarks>
/// #206: routes through <see cref="ICentralAuditWriter"/> (rather than calling
/// Routes through <see cref="ICentralAuditWriter"/> (rather than calling
/// <see cref="IAuditLogRepository.InsertIfNotExistsAsync"/> directly) so the writing
/// central node's <c>SourceNode</c> (<c>central-a</c>/<c>central-b</c>) is stamped via
/// the writer's <see cref="INodeIdentityProvider"/> — satisfying the design's
@@ -1040,7 +1038,7 @@ public class ManagementActor : ReceiveActor
verifierUser = row.VerifierUser
}));
// #206: the central direct-write writer is a singleton that opens its own
// The central direct-write writer is a singleton that opens its own
// per-call scope for the (scoped) IAuditLogRepository and stamps SourceNode
// from the local INodeIdentityProvider — no scope needed here.
var auditWriter = sp.GetRequiredService<ICentralAuditWriter>();
@@ -1090,7 +1088,7 @@ public class ManagementActor : ReceiveActor
var repo = sp.GetRequiredService<ISecuredWriteRepository>();
entity.Id = await repo.AddAsync(entity);
// T14b — one append-only audit row per lifecycle event. Emitted AFTER the row is
// One append-only audit row per lifecycle event. Emitted AFTER the row is
// persisted (so it carries the store-assigned id); best-effort — see helper.
await EmitSecuredWriteAuditAsync(
sp, AuditKind.SecuredWriteSubmit, AuditStatus.Submitted, entity, actor: entity.OperatorUser);
@@ -1131,13 +1129,13 @@ public class ManagementActor : ReceiveActor
row.VerifierComment = cmd.Comment;
row.DecidedAtUtc = decidedAtUtc;
// T14b — the approval decision is itself an audited lifecycle event (the
// The approval decision is itself an audited lifecycle event (the
// verifier won the CAS). Emitted with the in-flight Submitted status; the
// Execute row below records the terminal write outcome.
await EmitSecuredWriteAuditAsync(
sp, AuditKind.SecuredWriteApprove, AuditStatus.Submitted, row, actor: user.Username);
// D2 / T14 TOCTOU guard: re-assert the MxGateway protocol AT EXECUTE, not only at
// TOCTOU guard: re-assert the MxGateway protocol AT EXECUTE, not only at
// submit. The connection named on the row may have been reconfigured/recreated as a
// non-MxGateway (e.g. OPC UA) connection between submit and approval; relaying then
// would execute the secured write against a non-MxGateway adapter, violating the
@@ -1168,7 +1166,7 @@ public class ManagementActor : ReceiveActor
// Validate the value type BEFORE attempting the relay. An unknown type can
// never be decoded/written, so fail the row deterministically rather than
// leaving it stuck Approved. (Addresses the C2 reviewer's deferred
// leaving it stuck Approved. (Addresses the reviewer's deferred
// ValueType-validation note.)
if (!Enum.TryParse<Commons.Types.Enums.DataType>(row.ValueType, ignoreCase: true, out var dataType))
{
@@ -1182,7 +1180,7 @@ public class ManagementActor : ReceiveActor
return ToSecuredWriteDto(row);
}
// C3 robustness fix: Decode is UNGUARDED in the pre-T14b code — a List-typed
// Robustness fix: Decode is UNGUARDED without a try/catch — a List-typed
// value carrying corrupt JSON throws out of the handler and leaves the row
// stuck Approved. Contain it: fail the row deterministically with the decode
// error, audit the failure, and return WITHOUT relaying (nothing to write).
@@ -1234,7 +1232,7 @@ public class ManagementActor : ReceiveActor
// UpdateAsync overwrites all columns -> pass the fully-populated entity.
await repo.UpdateAsync(row);
// T14b — terminal execute outcome: Delivered (relay succeeded) maps to canonical
// Terminal execute outcome: Delivered (relay succeeded) maps to canonical
// Success, Failed maps to canonical Failure (the error rides in the row detail).
await EmitSecuredWriteAuditAsync(
sp,
@@ -1271,7 +1269,7 @@ public class ManagementActor : ReceiveActor
// UpdateAsync overwrites all columns -> pass the fully-populated entity.
await repo.UpdateAsync(entity);
// T14b — reject is a terminal lifecycle event (canonical Discarded outcome).
// Reject is a terminal lifecycle event (canonical Discarded outcome).
// Actor = the verifier; the operator is carried in the row's extra detail.
await EmitSecuredWriteAuditAsync(
sp, AuditKind.SecuredWriteReject, AuditStatus.Discarded, entity, actor: user.Username);
@@ -1458,7 +1456,7 @@ public class ManagementActor : ReceiveActor
}
/// <summary>
/// Moves a data connection to another site (M9 / T24a). High-risk + data
/// Moves a data connection to another site. High-risk + data
/// integrity: every guard runs server-side BEFORE the write, and when in
/// doubt the move is BLOCKED with a clear error rather than risking an
/// orphaned binding/reference. No bindings or name references are rewritten.
@@ -1754,7 +1752,7 @@ public class ManagementActor : ReceiveActor
}
/// <summary>
/// SMS Notifications (S6): build the recipient set for a notification list according
/// SMS Notifications: build the recipient set for a notification list according
/// to its delivery channel. Email lists map each <paramref name="recipientEmails"/>
/// entry to an <see cref="NotificationRecipient.ForEmail"/> recipient; SMS lists map
/// each <paramref name="recipientPhones"/> entry to an <see cref="NotificationRecipient.ForSms"/>
@@ -1781,10 +1779,10 @@ public class ManagementActor : ReceiveActor
}
/// <summary>
/// MgmtSvc-020: project an SmtpConfiguration to a credential-free shape so the
/// Project an SmtpConfiguration to a credential-free shape so the
/// stored Credentials (SMTP password / OAuth2 client secret) never leaves this
/// boundary via response payloads or audit afterState. Mirrors the
/// ApiKey-projection pattern in HandleListApiKeys / CD-012's fix.
/// ApiKey-projection pattern in HandleListApiKeys.
/// </summary>
private static object SmtpConfigPublicShape(Commons.Entities.Notifications.SmtpConfiguration c) =>
new
@@ -1806,7 +1804,7 @@ public class ManagementActor : ReceiveActor
{
var repo = sp.GetRequiredService<INotificationRepository>();
var configs = await repo.GetAllSmtpConfigurationsAsync();
// MgmtSvc-020: project away the Credentials field — read access to this
// Project away the Credentials field — read access to this
// list is broader than the Admin-only UpdateSmtpConfig path that owns
// the secret.
return configs.Select(SmtpConfigPublicShape).ToList();
@@ -1827,7 +1825,7 @@ public class ManagementActor : ReceiveActor
if (cmd.Credentials is not null) config.Credentials = cmd.Credentials;
await repo.UpdateSmtpConfigurationAsync(config);
await repo.SaveChangesAsync();
// MgmtSvc-020: audit the credential-free shape — the *fact of* the change
// Audit the credential-free shape — the *fact of* the change
// (and which non-secret fields hold) is observable; the secret value is
// not persisted to the audit log where OperationalAuditRoles can read it.
var publicShape = SmtpConfigPublicShape(config);
@@ -1836,7 +1834,7 @@ public class ManagementActor : ReceiveActor
}
/// <summary>
/// MgmtSvc-020 (SMS): project an SmsConfiguration to a credential-free shape so the
/// Project an SmsConfiguration to a credential-free shape so the
/// stored AuthToken (Twilio Auth Token secret) never leaves this boundary via
/// response payloads or audit afterState. Mirrors <see cref="SmtpConfigPublicShape"/>.
/// </summary>
@@ -1858,7 +1856,7 @@ public class ManagementActor : ReceiveActor
{
var repo = sp.GetRequiredService<INotificationRepository>();
var configs = await repo.GetAllSmsConfigurationsAsync();
// MgmtSvc-020: project away the AuthToken field — read access to this
// Project away the AuthToken field — read access to this
// list is broader than the Admin-only UpdateSmsConfig path that owns
// the secret.
return configs.Select(SmsConfigPublicShape).ToList();
@@ -1877,7 +1875,7 @@ public class ManagementActor : ReceiveActor
// existing values intact (non-breaking for callers that do not send them,
// and so the secret AuthToken survives a config edit that does not rotate it).
if (cmd.ApiBaseUrl is not null) config.ApiBaseUrl = cmd.ApiBaseUrl;
// MgmtSvc-021: treat an empty/whitespace AuthToken as "omitted", not "clear".
// Treat an empty/whitespace AuthToken as "omitted", not "clear".
// A Twilio Auth Token is always required, so an empty value is never a valid
// rotation — guarding against IsNullOrEmpty (not just null) stops an accidental
// `--auth-token ""` from silently wiping the stored secret and 401-ing every
@@ -1886,7 +1884,7 @@ public class ManagementActor : ReceiveActor
if (!string.IsNullOrWhiteSpace(cmd.AuthToken)) config.AuthToken = cmd.AuthToken;
await repo.UpdateSmsConfigurationAsync(config);
await repo.SaveChangesAsync();
// MgmtSvc-020: audit the credential-free shape — the AuthToken secret is
// Audit the credential-free shape — the AuthToken secret is
// not persisted to the audit log where OperationalAuditRoles can read it.
var publicShape = SmsConfigPublicShape(config);
await AuditAsync(sp, user, "Update", "SmsConfiguration", config.Id.ToString(), config.AccountSid, publicShape);
@@ -1903,7 +1901,7 @@ public class ManagementActor : ReceiveActor
return await repo.GetAllMappingsAsync();
}
// Security-023 (membership half): an LDAP-group mapping's Role is a free string on the
// An LDAP-group mapping's Role is a free string on the
// wire (CLI/API), so reject anything outside the canonical Roles.All set at the single
// server-side write path. A non-canonical role never functioned (no policy or authz
// check matches it), so rejecting it removes a silent-misconfiguration footgun rather
@@ -1956,7 +1954,7 @@ public class ManagementActor : ReceiveActor
private static async Task<object?> HandleListApiKeys(IServiceProvider sp)
{
// Inbound-API key re-arch (C2): keys are now managed through the shared
// Inbound-API key re-arch: keys are now managed through the shared
// IInboundApiKeyAdmin seam (over the ZB.MOM.WW.Auth.ApiKeys library) rather than
// the SQL Server IInboundApiRepository. The seam projection is hash-free by
// construction — only identity, status, and method-scopes are returned; the
@@ -1970,7 +1968,7 @@ public class ManagementActor : ReceiveActor
private static async Task<object?> HandleCreateApiKey(IServiceProvider sp, CreateApiKeyCommand cmd, string user)
{
// Inbound-API key re-arch (C2): the library mints the key, persists only the
// Inbound-API key re-arch: the library mints the key, persists only the
// peppered hash, and assembles the one-time bearer token (sbk_<keyId>_<secret>).
// The token is shown to the operator only here, in the create response; it cannot
// be retrieved later. No hash/secret is stored or returned by ScadaBridge.
@@ -2019,7 +2017,7 @@ public class ManagementActor : ReceiveActor
var repo = sp.GetRequiredService<IDeploymentManagerRepository>();
// Instance-scoped query: enforce against the target instance's site
// before reading its deployment history (finding ManagementService-014).
// before reading its deployment history.
if (cmd.InstanceId.HasValue)
{
await EnforceSiteScopeForInstance(sp, user, cmd.InstanceId.Value);
@@ -2037,7 +2035,7 @@ public class ManagementActor : ReceiveActor
var permittedIds = new HashSet<string>(user.PermittedSiteIds);
var templateRepo = sp.GetRequiredService<ITemplateEngineRepository>();
// ManagementService-023: pre-load all instances ONCE via the repository's
// Pre-load all instances ONCE via the repository's
// bulk method and build an InstanceId -> SiteId? lookup, instead of issuing
// GetInstanceByIdAsync per distinct record.InstanceId (textbook N+1). The
// unfiltered branch now hits the configuration database exactly twice
@@ -2256,7 +2254,7 @@ public class ManagementActor : ReceiveActor
};
await repo.AddTemplateNativeAlarmSourceAsync(source);
await repo.SaveChangesAsync();
// Propagate the new source to derived descendants (#1/#2). Native-source
// Propagate the new source to derived descendants. Native-source
// CRUD lives here (not TemplateService), so call the propagation directly.
await sp.GetRequiredService<TemplateService>().ReconcileDescendantsAsync(cmd.TemplateId, user);
return source;
@@ -2275,7 +2273,7 @@ public class ManagementActor : ReceiveActor
source.IsLocked = cmd.IsLocked;
await repo.UpdateTemplateNativeAlarmSourceAsync(source);
await repo.SaveChangesAsync();
// Propagate the changed source to derived descendants (#1/#2).
// Propagate the changed source to derived descendants.
await sp.GetRequiredService<TemplateService>().ReconcileDescendantsAsync(source.TemplateId, user);
return source;
}
@@ -2287,7 +2285,7 @@ public class ManagementActor : ReceiveActor
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).
// Remove the now-orphaned inherited placeholder from derived descendants.
if (source != null)
await sp.GetRequiredService<TemplateService>().ReconcileDescendantsAsync(source.TemplateId, user);
return cmd.NativeAlarmSourceId;
@@ -2392,15 +2390,15 @@ public class ManagementActor : ReceiveActor
}
// ========================================================================
// Schema Library handlers (M9-T32c)
// Schema Library handlers
//
// Reusable named JSON-Schema library (SharedSchema + ISharedSchemaRepository,
// T32a). Repo-backed, mirroring the DatabaseConnection handler idiom: direct
// Reusable named JSON-Schema library (SharedSchema + ISharedSchemaRepository).
// Repo-backed, mirroring the DatabaseConnection handler idiom: direct
// repository writes plus an explicit AuditAsync after each mutation. The unique
// Name is enforced with a clear duplicate-name error and the SchemaJson is
// validated as a parseable JSON Schema (reusing InboundApiSchema.Parse) so a
// malformed library entry never reaches the $ref resolver (T32b) or the
// schema-driven value-entry forms (T30).
// malformed library entry never reaches the $ref resolver or the
// schema-driven value-entry forms.
// ========================================================================
private static async Task<object?> HandleListSharedSchemas(IServiceProvider sp)
@@ -2499,7 +2497,7 @@ public class ManagementActor : ReceiveActor
/// The <c>ParseWithRefs</c> (collecting) variant is used deliberately so a library
/// entry that itself carries a <c>{"$ref":"lib:Other"}</c> pointer is NOT rejected at
/// save time merely because the resolver is not consulted here — a dangling/cyclic
/// ref surfaces later on the deploy-time / runtime resolution path (T32b), exactly
/// ref surfaces later on the deploy-time / runtime resolution path, exactly
/// like a forward reference to a not-yet-created entry. Only malformed JSON is a
/// save-blocking error.
/// </para>
@@ -2681,7 +2679,7 @@ public class ManagementActor : ReceiveActor
private static async Task<object?> HandleUpdateApiKey(IServiceProvider sp, UpdateApiKeyCommand cmd, string user)
{
// Inbound-API key re-arch (C2): enable/disable via the shared seam (no secret change).
// Inbound-API key re-arch: enable/disable via the shared seam (no secret change).
var admin = sp.GetRequiredService<IInboundApiKeyAdmin>();
var updated = await admin.SetEnabledAsync(cmd.KeyId, cmd.IsEnabled);
if (!updated)
@@ -2693,7 +2691,7 @@ public class ManagementActor : ReceiveActor
private static async Task<object?> HandleSetApiKeyMethods(IServiceProvider sp, SetApiKeyMethodsCommand cmd, string user)
{
// Inbound-API key re-arch (C2): replace a key's method-scope set via the shared seam
// Inbound-API key re-arch: replace a key's method-scope set via the shared seam
// (no secret change). The library is authoritative for the scope replacement.
if (cmd.Methods is null || cmd.Methods.Count == 0)
throw new ManagementCommandException("At least one method must be specified for an API key.");
@@ -2800,7 +2798,7 @@ public class ManagementActor : ReceiveActor
}
// ========================================================================
// Transport (#24) bundle operations
// Transport bundle operations
// ========================================================================
/// <summary>
@@ -2823,11 +2821,11 @@ public class ManagementActor : ReceiveActor
var dbConnections = await externalRepo.GetAllDatabaseConnectionsAsync();
var notificationLists = await notifRepo.GetAllNotificationListsAsync();
var smtpConfigs = await notifRepo.GetAllSmtpConfigurationsAsync();
// SMS (S10b): central-only SMS provider configs, mirroring smtpConfigs.
// SMS: central-only SMS provider configs, mirroring smtpConfigs.
var smsConfigs = await notifRepo.GetAllSmsConfigurationsAsync();
// Inbound API keys are not transported between environments (re-arch C4); only methods.
// Inbound API keys are not transported between environments; only methods.
var apiMethods = await inboundRepo.GetAllApiMethodsAsync();
// M8 (B4): site/instance-scoped selection. Sites match on SiteIdentifier
// Site/instance-scoped selection. Sites match on SiteIdentifier
// (preferred) or Name; instances match on UniqueName.
var sites = await siteRepo.GetAllSitesAsync();
var instances = await templateRepo.GetAllInstancesAsync();
@@ -2888,11 +2886,11 @@ public class ManagementActor : ReceiveActor
SmtpConfigurationIds: ResolveIds(smtpConfigs, cmd.SmtpConfigurationNames, s => s.Host, s => s.Id, "SMTP configuration"),
ApiMethodIds: ResolveIds(apiMethods, cmd.ApiMethodNames, m => m.Name, m => m.Id, "API method"),
IncludeDependencies: cmd.IncludeDependencies,
// M8 (B4): site/instance-scoped selection. Under All=true include every
// Site/instance-scoped selection. Under All=true include every
// site + instance, mirroring how All includes every template/etc.
SiteIds: ResolveSiteIds(),
InstanceIds: ResolveIds(instances, cmd.InstanceNames, i => i.UniqueName, i => i.Id, "instance"),
// SMS (S10b): SmsConfiguration is keyed by AccountSid (no Name column);
// SMS: SmsConfiguration is keyed by AccountSid (no Name column);
// the bundle preview row shows AccountSid, so the CLI uses AccountSid too.
SmsConfigurationIds: ResolveIds(smsConfigs, cmd.SmsConfigurationNames, s => s.AccountSid, s => s.Id, "SMS configuration"));
@@ -2936,7 +2934,7 @@ public class ManagementActor : ReceiveActor
var mods = preview.Items.Count(i => i.Kind == ConflictKind.Modified);
var ids = preview.Items.Count(i => i.Kind == ConflictKind.Identical);
var blocks = preview.Items.Count(i => i.Kind == ConflictKind.Blocker);
// M8 (D3): surface the required site/connection mappings so an operator
// Surface the required site/connection mappings so an operator
// (CLI or UI) sees which references need resolving before applying.
return new PreviewBundleResult(
preview.Items, adds, mods, ids, blocks,
@@ -2981,7 +2979,7 @@ public class ManagementActor : ReceiveActor
$"Bundle has {blockers.Count} blocker(s); import aborted. {details}");
}
// M8 (D3): resolve every required site/connection reference into a
// Resolve every required site/connection reference into a
// BundleNameMap. Precedence: an explicit operator spec wins; otherwise
// fall back to the preview's auto-match; otherwise (no match) create-new
// ONLY if the create-missing flag is set, else fail with a clear message.
@@ -2992,7 +2990,7 @@ public class ManagementActor : ReceiveActor
// requires a unique resolution per key. Last-write-wins matches the
// Central UI's TransportImport.BuildDefaultResolutions behavior. For
// DataConnection rows the preview item Name is already site-qualified
// ({site}/{name}, D1-FIX), so keying generically by (EntityType, Name) is
// ({site}/{name}), so keying generically by (EntityType, Name) is
// automatically correct — no bare-connection-name special case needed.
var renameStamp = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss");
var resolutionsMap = new Dictionary<(string, string), ImportResolution>();
@@ -3144,8 +3142,8 @@ public class SiteScopeViolationException : Exception
}
/// <summary>
/// Thrown by a command handler to signal a curated, caller-safe failure (finding
/// ManagementService-016). Its <see cref="Exception.Message"/> is intended to be
/// Thrown by a command handler to signal a curated, caller-safe failure. Its
/// <see cref="Exception.Message"/> is intended to be
/// surfaced verbatim to the HTTP/CLI caller — e.g. a validation result or a
/// "not found" message. Unanticipated exceptions (database faults, parse errors,
/// null-reference, etc.) must NOT be this type, so that <c>MapFault</c> can return
@@ -18,8 +18,8 @@ public static class ManagementEndpoints
private static readonly TimeSpan DefaultAskTimeout = TimeSpan.FromSeconds(30);
/// <summary>
/// Resolves the ManagementActor Ask timeout from configuration
/// (finding ManagementService-010). Falls back to <see cref="DefaultAskTimeout"/>
/// Resolves the ManagementActor Ask timeout from configuration.
/// Falls back to <see cref="DefaultAskTimeout"/>
/// when options are absent or the configured value is not strictly positive — a
/// zero/negative timeout would make every management call fail immediately.
/// </summary>
@@ -43,7 +43,7 @@ public static class ManagementEndpoints
/// <summary>
/// Per-request body-size ceiling for the management endpoint. ASP.NET Core's
/// default cap is ~30 MB and would reject Transport (#24) Import calls -- a
/// default cap is ~30 MB and would reject Transport Import calls -- a
/// 100 MB raw bundle base64-inflates to ~140 MB plus envelope. 200 MB is
/// comfortable without going unbounded.
/// </summary>
@@ -142,7 +142,7 @@ public static class ManagementEndpoints
/// Parses a management request body — a JSON object with a <c>command</c> name and an
/// optional <c>payload</c> — into the strongly-typed command record. The parsed
/// <see cref="JsonDocument"/> is disposed deterministically and the missing-payload
/// case does not allocate a throwaway document (finding ManagementService-006).
/// case does not allocate a throwaway document.
/// </summary>
/// <param name="body">The raw JSON request body string.</param>
/// <returns>A <see cref="CommandParseResult"/> with the deserialized command on success, or an error on failure.</returns>