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
@@ -29,7 +29,7 @@
@:Add API Key
}
</h4>
@* Bundle D (#23 M7-T12) drill-in: deep-link into the central Audit Log
@* Drill-in: deep-link into the central Audit Log
pre-filtered to this API key's inbound calls. Inbound audit rows record
the key Name as Actor and live on the ApiInbound channel. *@
@if (IsEditMode && !string.IsNullOrWhiteSpace(_formName))
@@ -112,7 +112,7 @@
</div>
@code {
// Inbound-API key re-arch (C3): this form drives the IInboundApiKeyAdmin seam.
// Inbound-API key re-arch: this form drives the IInboundApiKeyAdmin seam.
// Keys are identified by an opaque string KeyId; method access is a set of method
// NAMES (scopes) carried on the key, replacing the old ApiMethod.ApprovedApiKeyIds CSV.
// The list of all methods still comes from IInboundApiRepository (methods stay in SQL).
@@ -97,7 +97,7 @@
</div>
@code {
// Inbound-API key re-arch (C3): this page reads keys from the IInboundApiKeyAdmin seam
// Inbound-API key re-arch: this page reads keys from the IInboundApiKeyAdmin seam
// (string KeyId, method-scopes) rather than the SQL Server ApiKey entity. The seam has no
// retrievable hash, so the old masked Key-Hash column is gone; KeyId identifies each row.
private List<InboundApiKeyInfo> _keys = new();
@@ -22,7 +22,7 @@
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<h6 class="card-title">@(IsEditMode ? "Edit Site" : "Add Site")</h6>
@* Bundle D (#23 M7-T12) drill-in: deep-link into the central Audit
@* Drill-in: deep-link into the central Audit
Log pre-filtered to this site's events. AuditEvent.SourceSiteId
stores the SiteIdentifier (string), so we pass that through. *@
@if (IsEditMode && !string.IsNullOrWhiteSpace(_formIdentifier))
@@ -12,7 +12,7 @@
<div class="container-fluid mt-3">
<h1 class="h4 mb-3">Audit Log</h1>
@* Trends panel (M6 / K15). A best-effort collapsible Bootstrap card sitting
@* Trends panel. A best-effort collapsible Bootstrap card sitting
above the audit query UI: one KpiTrendChart per AuditLog global metric over
a 24h (default) / 7d window. Series are fetched independently in the
code-behind — a failed fetch degrades only that chart to the unavailable
@@ -62,22 +62,22 @@
}
</div>
@* Filter bar (Bundle B / M7-T2). Apply hands the collapsed filter to the grid.
Bundle D (M7-T10..T12) threads a query-string instance prefill through
@* Filter bar. Apply hands the collapsed filter to the grid.
Threads a query-string instance prefill through
InitialInstanceSearch — UI-only because the filter contract has no instance column. *@
<div class="mb-3">
<AuditFilterBar OnFilterChanged="HandleFilterChanged"
InitialInstanceSearch="@_initialInstanceSearch" />
</div>
@* Export button (Bundle F / M7-T14). A plain <a download> link triggers the
@* Export button. A plain <a download> link triggers the
streaming CSV endpoint at /api/centralui/audit/export — chosen over a
SignalR-driven download because the request can stream 100k rows directly
to the response body without buffering through the Blazor circuit. The
href reflects the most recently applied filter; before Apply is clicked,
an unconstrained export is exposed.
Bundle G (#23 M7-T15) gates the button on the AuditExport policy so an
Gates the button on the AuditExport policy so an
OperationalAudit-only operator (read access without bulk export) sees the
page + filters but cannot trigger the CSV pull. The endpoint itself is
gated separately, so a hand-crafted URL still 403s — the AuthorizeView
@@ -96,7 +96,7 @@
</Authorized>
</AuthorizeView>
@* Results grid (Bundle B / M7-T3). Row clicks emit OnRowSelected for Bundle C's
@* Results grid. Row clicks emit OnRowSelected for the
drilldown drawer; the grid stays in "no events" mode until the user applies a
filter so the page does not auto-load the full audit table on first render. *@
<div>
@@ -104,7 +104,7 @@
</div>
</div>
@* Drilldown drawer (Bundle C / M7-T4..T8). Hosted at the page level so the
@* Drilldown drawer. Hosted at the page level so the
off-canvas overlay sits above the grid / filter bar irrespective of scroll. *@
<AuditDrilldownDrawer Event="@_selectedEvent"
IsOpen="@_drawerOpen"
@@ -11,19 +11,19 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Audit;
/// <summary>
/// Code-behind for the central Audit Log page (#23 M7). Bundle B (M7-T2 + M7-T3)
/// wires up <c>AuditFilterBar</c> and <c>AuditResultsGrid</c>: the page owns the
/// Code-behind for the central Audit Log page. Wires up <c>AuditFilterBar</c>
/// and <c>AuditResultsGrid</c>: the page owns the
/// active <see cref="AuditLogQueryFilter"/> and re-pushes a fresh instance to the
/// grid on every Apply (the grid uses reference identity as its "reload"
/// trigger). Row clicks land in <see cref="HandleRowSelected"/>, which pins
/// the selected row and opens the drilldown drawer.
///
/// <para>
/// Bundle D (M7-T10..T12) adds query-string drill-in parsing so other pages can
/// Adds query-string drill-in parsing so other pages can
/// deep-link to a pre-filtered Audit Log: <c>?correlationId=</c>, <c>?target=</c>,
/// <c>?actor=</c>, <c>?site=</c>, <c>?channel=</c>, <c>?kind=</c>, and the UI-only
/// <c>?instance=</c> are read on initialization. Bundle E (M7-T13) extends
/// this with <c>?status=</c> so the Health-dashboard Audit error-rate tile can
/// <c>?instance=</c> are read on initialization. This extends
/// with <c>?status=</c> so the Health-dashboard Audit error-rate tile can
/// drill in to <c>?status=Failed</c>. The ExecutionId follow-up adds
/// <c>?executionId=</c> for the "View this execution" drill-in, and the
/// ParentExecutionId follow-up adds <c>?parentExecutionId=</c> for the
@@ -51,7 +51,7 @@ public partial class AuditLogPage : IDisposable
[Inject] private NavigationManager Navigation { get; set; } = null!;
/// <summary>
/// KPI-history facade for the M6 (K15) Trends panel — fetched per metric,
/// KPI-history facade for the Trends panel — fetched per metric,
/// per window. Best-effort: a failed series fetch degrades that one chart to
/// the unavailable placeholder and never breaks the audit query UI.
/// </summary>
@@ -187,7 +187,7 @@ public partial class AuditLogPage : IDisposable
IReadOnlyList<AuditKind>? kinds =
AuditQueryParamParsers.ParseEnumList<AuditKind>(Raw(query, "kind"));
// Bundle E (M7-T13): the Health-dashboard Audit error-rate tile drills in
// The Health-dashboard Audit error-rate tile drills in
// with ?status=Failed (and operators may craft URLs with Parked/Discarded).
// Unknown values are silently dropped — the page still renders without
// the constraint.
@@ -248,7 +248,7 @@ public partial class AuditLogPage : IDisposable
private void HandleRowSelected(AuditEventView row)
{
// Bundle C: a grid row click hands us the full AuditEvent. We pin it as
// A grid row click hands us the full AuditEvent. We pin it as
// the selected row and open the drilldown drawer — the drawer is fully
// presentational so we do not need to refetch the row.
_selectedEvent = row;
@@ -263,7 +263,7 @@ public partial class AuditLogPage : IDisposable
}
// ─────────────────────────────────────────────────────────────────────────
// M6 (K15) — Audit Log trend charts.
// Audit Log trend charts.
//
// A best-effort Trends panel that sits above the audit query UI: one
// KpiTrendChart per AuditLog global metric, over a 24h (default) or 7d
@@ -278,8 +278,7 @@ public partial class AuditLogPage : IDisposable
/// <summary>
/// True while a window's series are being (re)fetched — disables the 24h/7d
/// window toggle buttons so a mid-flight click cannot stack overlapping loads
/// (mirrors the K13/K14 trend pages).
/// window toggle buttons so a mid-flight click cannot stack overlapping loads.
/// </summary>
private bool _trendsLoading;
@@ -367,7 +366,7 @@ public partial class AuditLogPage : IDisposable
}
/// <summary>
/// Bundle F (M7-T14): URL the Export-CSV link points at. Renders the most
/// URL the Export-CSV link points at. Renders the most
/// recently applied filter as query-string params so the server-side
/// streaming endpoint reproduces the user's current view. With no filter
/// applied yet, returns the bare endpoint — i.e. an unconstrained export.
@@ -394,7 +393,7 @@ public partial class AuditLogPage : IDisposable
// No capacity hint: the dimensions are multi-value, so the part count is
// unbounded by the number of filter fields.
var parts = new List<KeyValuePair<string, string?>>();
// Task 9: the filter dimensions are multi-value end-to-end. Emit ONE
// The filter dimensions are multi-value end-to-end. Emit ONE
// repeated query-string key per selected value (channel=A&channel=B); the
// export endpoint's ParseFilter reads the full repeated set.
if (filter.Channels is { Count: > 0 } channels)
@@ -13,7 +13,7 @@
<ToastNotification @ref="_toast" />
@* Bundle Import filter chip (T24). Set via ?bundleImportId={guid} query
@* Bundle Import filter chip. Set via ?bundleImportId={guid} query
string so drill-ins from the Import wizard / other pages can scope this
page to a single import run. Cleared via the × button, which navigates
back to the page without the query param so the user sees all rows. *@
@@ -192,7 +192,7 @@
@code {
/// <summary>
/// T24 (Transport). When non-null, scopes the page to a single bundle
/// When non-null, scopes the page to a single bundle
/// import run. Set via the <c>?bundleImportId=</c> query string from
/// drill-ins (Import wizard summary, future BundleImported row links).
/// </summary>
@@ -256,7 +256,7 @@
protected override async Task OnParametersSetAsync()
{
// T24: when the BundleImportId query param is set (or cleared), refetch
// When the BundleImportId query param is set (or cleared), refetch
// automatically so the user lands on a pre-filtered page from a drill-in
// link without having to click Search.
if (BundleImportId != _lastFetchedBundleImportId)
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Audit;
/// <summary>
/// Code-behind for the execution-chain tree page (Audit Log ParentExecutionId
/// feature, Task 10). Route <c>/audit/execution-tree</c>, reached via the Audit
/// feature). Route <c>/audit/execution-tree</c>, reached via the Audit
/// Log drilldown drawer's "View execution chain" action with
/// <c>?executionId={guid}</c>.
///
@@ -40,7 +40,7 @@ public partial class ExecutionTreePage
private bool _loading;
private string? _error;
// Execution-Tree Node Detail Modal feature (Task 4) — state backing the
// Execution-Tree Node Detail Modal feature — state backing the
// <ExecutionDetailModal>. A double-click on a tree node sets
// _modalExecutionId + flips _modalOpen true; the modal loads that
// execution's audit rows on the closed → open transition. _modalOpen is the
@@ -4,8 +4,8 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment;
/// <summary>
/// Pure (no Blazor/DI) builder that turns a flat list of streamed attribute (and,
/// in DV-4, alarm) events into a collapsible composition forest of
/// Pure (no Blazor/DI) builder that turns a flat list of streamed attribute (and
/// alarm) events into a collapsible composition forest of
/// <see cref="DebugTreeNode"/>. Path-qualified canonical names are split on
/// <c>'.'</c> to derive branch nodes; the terminal segment carries the payload.
/// </summary>
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; // AlarmState
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment;
/// <summary>
/// A node in a Debug View composition tree. Attribute (and, in DV-4, alarm)
/// A node in a Debug View composition tree. Attribute (and alarm)
/// names are path-qualified canonical names (e.g. <c>Motor1.Compressor.Pump</c>);
/// the tree is derived by splitting those names on <c>'.'</c>. A node is either a
/// branch (composition member, no payload, has children) or a leaf (one attribute
@@ -22,9 +22,9 @@ public sealed class DebugTreeNode
// Leaf payloads — exactly one is set on a leaf; both null on a pure branch.
public AttributeValueChanged? Attribute { get; init; }
public AlarmStateChanged? Alarm { get; init; } // computed leaf, native condition, or placeholder (DV-4)
public AlarmStateChanged? Alarm { get; init; } // computed leaf, native condition, or placeholder
public bool IsNativeBinding { get; init; } // branch grouping native conditions (DV-4)
public bool IsNativeBinding { get; init; } // branch grouping native conditions
// Roll-up (set by the builder for branch nodes).
public AlarmState WorstState { get; set; } = AlarmState.Normal;
@@ -25,7 +25,7 @@
<div class="d-flex align-items-center mb-3">
<button class="btn btn-outline-secondary btn-sm me-3" @onclick="GoBack">&larr; Back to Topology</button>
<h4 class="mb-0">Configure Instance</h4>
@* Bundle D (#23 M7-T12) drill-in: deep-link into the central Audit Log
@* Drill-in: deep-link into the central Audit Log
pre-filtered to this instance. Instance is UI-only on the filter bar
(AuditEvent has no Instance column), so we use the ?instance= UI-text
seam — the filter bar's Instance free-text input is pre-populated. *@
@@ -178,7 +178,7 @@
<div class="card mb-3">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<strong>Attribute Overrides</strong>
@* M7-T16: bulk import of attribute overrides from a CSV
@* Bulk import of attribute overrides from a CSV
(AttributeName,Value[,ElementType]). Selecting a file parses +
validates it against this instance's overridable attributes and —
all-or-nothing — applies every row through the SAME
@@ -598,7 +598,7 @@
// un-parseable List element caught on the pre-submit round-trip).
private Dictionary<string, string> _overrideErrors = new();
// M7-T16: CSV bulk-import result summary. _csvImportResult is the headline
// CSV bulk-import result summary. _csvImportResult is the headline
// ("Imported N overrides." or "Import rejected — N error(s)."); on failure the
// per-line messages are listed from _csvImportErrors. Null until an import runs.
private string? _csvImportResult;
@@ -1047,7 +1047,7 @@
_saving = false;
}
// ── M7-T16: CSV bulk override import ────────────────────
// ── CSV bulk override import ────────────────────
/// <summary>
/// Handles a selected override CSV. Reads the file text (size-capped), parses it
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment;
/// <summary>
/// Code-behind for <c>InstanceConfigure.razor</c> — hosts the pure, side-effect-free
/// core of the CSV bulk-override import (M7-T16) so it can be unit-pinned without
/// core of the CSV bulk-override import so it can be unit-pinned without
/// standing up the page's ≈7 injected services. The Razor side reads the uploaded
/// file's text, calls <see cref="OverrideCsvParser.Parse"/>, then feeds the result
/// here; on success it applies the returned dict through the SAME
@@ -198,7 +198,7 @@
private IReadOnlyList<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.DiagnosticMarker> _markers
= Array.Empty<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.DiagnosticMarker>();
// Inbound-API key re-arch (C3): the approved-keys list is driven by the IInboundApiKeyAdmin
// Inbound-API key re-arch: the approved-keys list is driven by the IInboundApiKeyAdmin
// seam, not ApiMethod.ApprovedApiKeyIds. The ApiMethod entity itself (name/script/params/etc.)
// still lives on IInboundApiRepository — only the key↔method approval relationship moved to
// per-key method-scopes. Keys are identified by an opaque string KeyId.
@@ -213,7 +213,7 @@
private ToastNotification _toast = default!;
// M9-T25: live per-connection health, keyed by DataConnection.Id. Sourced from
// Live per-connection health, keyed by DataConnection.Id. Sourced from
// the existing site→central health transport via IConnectionHealthQueryService
// and refreshed on a ~10s poll, mirroring the Health dashboard's timer pattern.
// A connection absent from the map renders an "Unknown" badge (tolerates a site
@@ -419,7 +419,7 @@
}
}
// ── M9-T24b / T33b: Move connection to another site ──
// ── Move connection to another site ──
// Opened via IDialogService.ShowAsync. The body dispatches MoveDataConnectionCommand
// through the guard-running ManagementActor path (IDataConnectionMoveService) — NOT
// a direct repository write — so the server enforces the Designer gate and every
@@ -451,7 +451,7 @@
.Select(r => (r.SiteId!.Value, r.Label))
.ToList();
// M9-T25: enum → Bootstrap badge class. Mirrors the Health dashboard's
// Enum → Bootstrap badge class. Mirrors the Health dashboard's
// GetConnectionHealthBadge (Components/Pages/Monitoring/Health.razor) so the
// design page surfaces the same colour coding for the same status. Kept as a
// small local mirror — the Health helper is a private page-local method, not a
@@ -12,7 +12,7 @@
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">@(Id.HasValue ? "Edit External System" : "Add External System")</h4>
@* Bundle D (#23 M7-T12) drill-in: deep-link into the central Audit Log
@* Drill-in: deep-link into the central Audit Log
pre-filtered to this external system's outbound API events. Audit rows
record the target by external-system name, so we filter on Target. *@
@if (Id.HasValue && !string.IsNullOrWhiteSpace(_name))
@@ -6,9 +6,9 @@
@inject ISchemaLibraryService SchemaLibraryService
@inject IDialogService Dialog
@* Schema Library (M9-T32c): list + create/edit (via SchemaBuilder) + delete the
@* Schema Library: list + create/edit (via SchemaBuilder) + delete the
reusable named JSON-Schema library entries that the {"$ref":"lib:Name"} resolver
(T32b) resolves against. Every mutation is dispatched through ISchemaLibraryService
resolves against. Every mutation is dispatched through ISchemaLibraryService
— the guard-running ManagementActor path — never a direct repo write. *@
<div class="container-fluid mt-3">
@@ -64,7 +64,7 @@
private Template? _ownerTemplate;
private TemplateComposition? _ownerComposition;
// M9-T26b: the FULL transitively-resolved inherited member set (multi-level
// M9: the FULL transitively-resolved inherited member set (multi-level
// chain + post-creation base additions) + staleness, fetched read-only via
// GetResolvedTemplateMembersCommand. Populated only for derived templates;
// null when the read failed (editor falls back to the stored-row view).
@@ -228,7 +228,7 @@
}
}
// M9-T26b: resolve the FULL inherited member set (whole chain +
// M9: resolve the FULL inherited member set (whole chain +
// post-creation base additions) read-only. The stored-row tables
// above only carry the IMMEDIATE base; this surfaces the rest with
// origin annotation + a base-changed staleness banner. Read-only:
@@ -314,7 +314,7 @@
</div>
}
@* M9-T26b: the FULL transitively-resolved inherited member set — the whole
@* M9: the FULL transitively-resolved inherited member set — the whole
chain (grandparent + further ancestors) plus base members added after
this template was created, which the immediate-base tables below cannot
show. Read-only preview; each row carries its origin template + lock
@@ -473,7 +473,7 @@
}
};
// ---- M9-T26b: full resolved inherited set (read-only preview) ----
// ---- M9: full resolved inherited set (read-only preview) ----
/// <summary>
/// Renders the FULL transitively-resolved effective member set (attributes,
@@ -425,7 +425,7 @@
}
}
// ---- Sibling reorder (T23b) ----
// ---- Sibling reorder ----
// Dispatches the reorder the same way Move/Rename do — directly through
// TemplateFolderService (the backend swaps SortOrder with the adjacent
// same-parent sibling, no-op at the ends) — then reloads so the new order
@@ -471,7 +471,7 @@
.ToList();
}
// ---- Root-level context menu (T23b) ----
// ---- Root-level context menu ----
// Right-click on the tree zone (empty space or below the tree) offers
// New Folder / New Template at root. Node right-clicks are handled by the
// TreeView's own context menu and never reach here.
@@ -9,7 +9,7 @@
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
@*
TransportExport wizard (Component #24, Task T21).
TransportExport wizard (Component #24).
A 4-step linear wizard:
Step 1 — Select : templates (tree, checkbox-mode) + flat artifact lists.
@@ -138,7 +138,7 @@
@RenderCheckboxList(_smtpConfigs, s => s.Id, s => s.Host, _selectedSmtpConfigs)
</fieldset>
@* S10c: SMS provider configs, mirroring the SMTP section above. Labelled by
@* SMS provider configs, mirroring the SMTP section above. Labelled by
AccountSid (the bundle key); the secret AuthToken is never rendered. *@
<fieldset class="mb-4" data-testid="group-sms-configs">
<legend class="h6">SMS Configurations</legend>
@@ -361,12 +361,12 @@
{
<li>SmtpConfig: @s.Host</li>
}
@* S10c: SMS configs in the closure, mirroring SmtpConfig above; AccountSid only, never AuthToken. *@
@* SMS configs in the closure, mirroring SmtpConfig above; AccountSid only, never AuthToken. *@
@foreach (var s in _resolved.SmsConfigs.OrderBy(s => s.AccountSid, StringComparer.OrdinalIgnoreCase))
{
<li>SmsConfig: @s.AccountSid</li>
}
@* Inbound API keys are not transported (re-arch C4) — methods only. *@
@* Inbound API keys are not transported — methods only. *@
@foreach (var m in _resolved.ApiMethods.OrderBy(m => m.Name, StringComparer.OrdinalIgnoreCase))
{
<li>ApiMethod: @m.Name</li>
@@ -20,7 +20,7 @@ using ZB.MOM.WW.ScadaBridge.Transport.Export;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Design;
/// <summary>
/// Code-behind for the TransportExport wizard (Transport feature, Task T21).
/// Code-behind for the TransportExport wizard (Transport feature).
///
/// Four-step state machine:
/// <list type="number">
@@ -72,12 +72,12 @@ public partial class TransportExport : ComponentBase
private List<DatabaseConnectionDefinition> _dbConnections = new();
private List<NotificationList> _notificationLists = new();
private List<SmtpConfiguration> _smtpConfigs = new();
// S10c: SMS provider configs, mirroring _smtpConfigs. Keyed by AccountSid in the
// SMS provider configs, mirroring _smtpConfigs. Keyed by AccountSid in the
// bundle; the wizard selects by entity id (same as SMTP) and never surfaces AuthToken.
private List<SmsConfiguration> _smsConfigs = new();
// Inbound API keys are not transported between environments (re-arch C4); only methods.
// Inbound API keys are not transported between environments; only methods.
private List<ApiMethod> _apiMethods = new();
// M8 (E1): site/instance-scoped export. Sites are listed flat; each site's
// Site/instance-scoped export. Sites are listed flat; each site's
// instances hang off it (loaded eagerly via ISiteRepository.GetInstancesBySiteIdAsync)
// so the operator can pick a whole site or drill into individual instances.
private List<Site> _sites = new();
@@ -93,11 +93,11 @@ public partial class TransportExport : ComponentBase
private readonly HashSet<int> _selectedDbConnections = new();
private readonly HashSet<int> _selectedNotificationLists = new();
private readonly HashSet<int> _selectedSmtpConfigs = new();
// S10c: SMS provider config selection, mirroring _selectedSmtpConfigs.
// SMS provider config selection, mirroring _selectedSmtpConfigs.
private readonly HashSet<int> _selectedSmsConfigs = new();
// No _selectedApiKeys: inbound API keys are not transported (re-arch C4).
// No _selectedApiKeys: inbound API keys are not transported.
private readonly HashSet<int> _selectedApiMethods = new();
// M8 (E1): site/instance selection backed by entity primary keys, matching the
// Site/instance selection backed by entity primary keys, matching the
// DependencyResolver's SiteIds/InstanceIds (NOT names — the resolver fetches by id).
private readonly HashSet<int> _selectedSites = new();
private readonly HashSet<int> _selectedInstances = new();
@@ -143,12 +143,12 @@ public partial class TransportExport : ComponentBase
_dbConnections = (await ExternalRepo.GetAllDatabaseConnectionsAsync()).ToList();
_notificationLists = (await NotificationRepo.GetAllNotificationListsAsync()).ToList();
_smtpConfigs = (await NotificationRepo.GetAllSmtpConfigurationsAsync()).ToList();
// S10c: SMS provider configs, mirroring the SMTP load path above.
// SMS provider configs, mirroring the SMTP load path above.
_smsConfigs = (await NotificationRepo.GetAllSmsConfigurationsAsync()).ToList();
// Inbound API keys are not transported (re-arch C4) — only methods are loaded.
// Inbound API keys are not transported — only methods are loaded.
_apiMethods = (await InboundApiRepo.GetAllApiMethodsAsync()).ToList();
// M8 (E1): sites + their instances for site/instance-scoped export. Each
// Sites + their instances for site/instance-scoped export. Each
// site's instances are loaded eagerly so the expandable picker has them
// without a per-click round-trip; sites are ordered by identifier to match
// the bundle's deterministic site ordering.
@@ -245,17 +245,17 @@ public partial class TransportExport : ComponentBase
DatabaseConnectionIds: _selectedDbConnections.ToList(),
NotificationListIds: _selectedNotificationLists.ToList(),
SmtpConfigurationIds: _selectedSmtpConfigs.ToList(),
// Inbound API keys are not transported (re-arch C4) — methods only.
// Inbound API keys are not transported — methods only.
ApiMethodIds: _selectedApiMethods.ToList(),
IncludeDependencies: _includeDependencies)
{
// M8 (E1): site/instance ids feed the resolver's SiteIds/InstanceIds.
// Site/instance ids feed the resolver's SiteIds/InstanceIds.
// Set via init-only properties so the positional ctor stays the documented
// additive shape; the resolver dedups a selected instance against its
// already-selected owning site.
SiteIds = _selectedSites.ToList(),
InstanceIds = _selectedInstances.ToList(),
// S10c: SMS provider config ids feed the resolver's SmsConfigurationIds,
// SMS provider config ids feed the resolver's SmsConfigurationIds,
// exactly as SmtpConfigurationIds above (init-only, trailing additive shape).
SmsConfigurationIds = _selectedSmsConfigs.ToList(),
};
@@ -325,7 +325,7 @@ public partial class TransportExport : ComponentBase
{
if (!string.IsNullOrEmpty(smtp.Credentials)) count++;
}
// S10c: SMS provider AuthToken is a secret, mirroring SMTP Credentials above.
// SMS provider AuthToken is a secret, mirroring SMTP Credentials above.
foreach (var sms in resolved.SmsConfigs)
{
if (!string.IsNullOrEmpty(sms.AuthToken)) count++;
@@ -485,7 +485,7 @@ public partial class TransportExport : ComponentBase
else set.Remove(id);
}
// ---- Step 1 site/instance helpers (M8 E1) ----
// ---- Step 1 site/instance helpers ----
/// <summary>Instances loaded for a site, or an empty list when the site has none.</summary>
private IReadOnlyList<Instance> InstancesFor(int siteId) =>
@@ -17,7 +17,7 @@ using ZB.MOM.WW.ScadaBridge.Transport.Import;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Design;
/// <summary>
/// Code-behind for the TransportImport wizard (Transport feature, Task T22).
/// Code-behind for the TransportImport wizard (Transport feature).
///
/// Five-step state machine:
/// <list type="number">
@@ -42,7 +42,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Design;
///
/// Cached bundle bytes: because <see cref="IBundleImporter.LoadAsync"/> currently
/// peeks the manifest by attempting decryption, encrypted bundles require two
/// LoadAsync invocations. CentralUI-031: we previously cached the raw bytes in a
/// LoadAsync invocations. We previously cached the raw bytes in a
/// <c>byte[] _bundleBytes</c> field, which buffered the full upload (default cap
/// 100 MB) in the component's per-circuit state — multiplied across concurrent
/// operator sessions, that produced real central-node memory pressure. The
@@ -71,7 +71,7 @@ public partial class TransportImport : ComponentBase, IDisposable
[Inject] private IAuditService AuditService { get; set; } = default!;
[Inject] private ScadaBridgeDbContext DbContext { get; set; } = default!;
// M8 E2: the Map step needs the destination environment's sites + each
// The Map step needs the destination environment's sites + each
// site's connections to populate the "map to existing target" dropdowns.
[Inject] private ISiteRepository SiteRepo { get; set; } = default!;
@@ -80,7 +80,7 @@ public partial class TransportImport : ComponentBase, IDisposable
private string? _errorMessage;
// ---- Session + cached bundle path ----
// CentralUI-031: the upload is streamed to a per-session temp file and only
// The upload is streamed to a per-session temp file and only
// the path is retained on the component, so we don't hold an entire bundle
// (up to MaxBundleSizeMb, default 100 MB) in per-circuit memory across the
// wizard's lifetime. The file is deleted on every wizard reset path and on
@@ -105,7 +105,7 @@ public partial class TransportImport : ComponentBase, IDisposable
// Keyed by (EntityType, Name) — matches BundleImporter.ApplyAsync's lookup.
private Dictionary<(string EntityType, string Name), ImportResolution>? _resolutions;
// ---- Step 3 (Map sub-section, M8 E2): name mapping ----
// ---- Step 3 (Map sub-section): name mapping ----
// The sentinel dropdown value for "Create new" — empty string can't collide
// with a real SiteIdentifier / connection Name (both are non-empty).
private const string CreateNewValue = "";
@@ -160,7 +160,7 @@ public partial class TransportImport : ComponentBase, IDisposable
return;
}
// CentralUI-031: stream the upload directly to a per-session temp
// Stream the upload directly to a per-session temp
// file so the central node's working set is bounded by the
// FileStream buffer (~80 KB) rather than the full bundle bytes.
// OpenReadStream's MaxAllowedSize defaults to 500_000 bytes — bump
@@ -209,7 +209,7 @@ public partial class TransportImport : ComponentBase, IDisposable
}
try
{
// CentralUI-031: read the staged bundle straight off disk; the
// Read the staged bundle straight off disk; the
// importer's LoadAsync only walks the stream forward, so a plain
// FileStream is sufficient (no need to buffer it back into memory).
using var stream = new FileStream(
@@ -435,7 +435,7 @@ public partial class TransportImport : ComponentBase, IDisposable
}
/// <summary>
/// M8 E2: prepares the Map sub-section state for a freshly loaded preview.
/// Prepares the Map sub-section state for a freshly loaded preview.
/// When the preview carries no required site/connection mappings this is a
/// no-op (the Map section is hidden) and we skip the site/connection reads
/// entirely — central-config-only bundles never touch the destination's
@@ -564,7 +564,7 @@ public partial class TransportImport : ComponentBase, IDisposable
}
/// <summary>
/// M8 E2: folds the operator's Map-step choices into a <see cref="BundleNameMap"/>.
/// Folds the operator's Map-step choices into a <see cref="BundleNameMap"/>.
/// A concrete chosen target → <see cref="MappingAction.MapToExisting"/> with that
/// target identifier/name; the "Create new" sentinel → <see cref="MappingAction.CreateNew"/>
/// with a null target. Iterates the preview's required mappings so the map's shape
@@ -607,7 +607,7 @@ public partial class TransportImport : ComponentBase, IDisposable
}
/// <summary>
/// M8 E2: parses a Modified item's <c>FieldDiffJson</c> and returns the
/// Parses a Modified item's <c>FieldDiffJson</c> and returns the
/// <c>lineDiff</c> object for the first code field that carries one, or null
/// when the diff has no line-level payload (ordinary fields render as a coarse
/// summary instead). Tolerant of malformed/absent JSON — a parse failure
@@ -772,7 +772,7 @@ public partial class TransportImport : ComponentBase, IDisposable
try
{
var user = await Auth.GetCurrentUsernameAsync();
// M8 E2: fold the operator's Map-step choices into the name map. For
// Fold the operator's Map-step choices into the name map. For
// central-config-only bundles (no required mappings) this is
// BundleNameMap.Empty, which the importer normalises away.
var nameMap = BuildNameMap();
@@ -814,7 +814,7 @@ public partial class TransportImport : ComponentBase, IDisposable
_confirmEnvironmentText = string.Empty;
_result = null;
_validationErrors = null;
// M8 E2: clear the Map sub-section state too.
// Clear the Map sub-section state too.
_targetSites = Array.Empty<Site>();
_targetConnections.Clear();
_siteChoices.Clear();
@@ -822,7 +822,7 @@ public partial class TransportImport : ComponentBase, IDisposable
}
/// <summary>
/// CentralUI-031: deletes the staged bundle temp file if any. Swallows IO
/// Deletes the staged bundle temp file if any. Swallows IO
/// failures — an undeletable temp file is best-effort cleanup and must not
/// block the wizard.
/// </summary>
@@ -852,7 +852,7 @@ public partial class TransportImport : ComponentBase, IDisposable
}
/// <summary>
/// CentralUI-031: ensures the staged temp file does not survive circuit
/// Ensures the staged temp file does not survive circuit
/// teardown. Blazor invokes Dispose when the user navigates away or the
/// circuit ends, so an abandoned wizard cleans up automatically.
/// </summary>
@@ -72,13 +72,13 @@
PerNodeSnapshots="@_siteCallNodeKpis"
PerNodeAvailable="@_siteCallNodeKpiAvailable" />
@* Audit Log (#23) M7 Bundle E — three KPI tiles for the Audit channel
@* Three KPI tiles for the Audit channel
(volume / error rate / backlog). Refreshed alongside the site states. *@
<AuditKpiTiles Snapshot="@_auditKpi"
IsAvailable="@_auditKpiAvailable"
ErrorMessage="@_auditKpiError" />
@* Site Health Trends (M6 K16) — per-site Site Health KPI history. Loads on a
@* Site Health Trends (M6) — per-site Site Health KPI history. Loads on a
separate path from the 10s tile-refresh timer so a trend-query fault can
never disturb the live dashboard or its polling loop. The site selector
reuses the site keys already loaded into _siteStates; the window toggle
@@ -438,7 +438,7 @@
private bool _outboxKpiAvailable;
private string? _outboxKpiError;
// Audit Log (#23) M7 Bundle E — Audit KPI tiles. Volume + error rate come
// Audit KPI tiles. Volume + error rate come
// from a 1h aggregate over the central AuditLog table; backlog sums the
// per-site SiteAuditBacklog.PendingCount via the health aggregator.
private AuditLogKpiSnapshot? _auditKpi;
@@ -452,13 +452,13 @@
private bool _siteCallKpiAvailable;
private string? _siteCallKpiError;
// Per-node Site Call KPI breakdown (T6: M5.2 per-node stuck-count KPIs).
// Per-node Site Call KPI breakdown (M5.2 per-node stuck-count KPIs).
// Passed to SiteCallKpiTiles as an optional sub-table.
private IReadOnlyList<SiteCallNodeKpiSnapshot> _siteCallNodeKpis =
Array.Empty<SiteCallNodeKpiSnapshot>();
private bool _siteCallNodeKpiAvailable;
// ── Site Health Trends (M6 K16) ───────────────────────────────────────────
// ── Site Health Trends (M6) ───────────────────────────────────────────────
// Per-site Site Health KPI history, loaded on a path entirely separate from
// the 10s tile-refresh timer (LoadSiteHealthTrendsAsync, never called from
// the timer tick). The site keys are a snapshot of the dashboard's site set,
@@ -512,7 +512,7 @@
await RefreshNow();
// Site Health Trends (M6 K16) load on their own path — never from the
// Site Health Trends (M6) load on their own path — never from the
// timer tick below — so a trend-query fault can't disturb the live tile
// refresh. Seed the selector from the sites just loaded into _siteStates
// and query the default site.
@@ -694,7 +694,7 @@
}
}
// Per-node site-call KPI loader (T6: M5.2). Best-effort; a fault silently
// Per-node site-call KPI loader (M5.2). Best-effort; a fault silently
// suppresses the per-node sub-table rather than degrading the dashboard.
private async Task LoadSiteCallNodeKpis()
{
@@ -72,7 +72,7 @@
</div>
}
@* ── Per-node breakdown (T6: additive) ── *@
@* ── Per-node breakdown (additive) ── *@
<h5 class="mb-2">Per-node breakdown</h5>
@if (_perNodeError != null)
{
@@ -162,7 +162,7 @@
</div>
}
@* ── Trends (T11: first KPI-history consumer) ── *@
@* ── Trends (first KPI-history consumer) ── *@
<div class="d-flex justify-content-between align-items-center mt-4 mb-2" data-test="notification-trends">
<h5 class="mb-0">Trends</h5>
<div class="btn-group btn-group-sm" role="group" aria-label="Trend window">
@@ -205,15 +205,15 @@
private IReadOnlyList<SiteNotificationKpiSnapshot> _perSite = Array.Empty<SiteNotificationKpiSnapshot>();
private string? _perSiteError;
// ── Per-node (T6: M5.2 per-node stuck-count KPIs) ──
// ── Per-node (M5.2 per-node stuck-count KPIs) ──
private IReadOnlyList<NodeNotificationKpiSnapshot> _perNode = Array.Empty<NodeNotificationKpiSnapshot>();
private string? _perNodeError;
private bool _loading;
// ── Trends (T11: first KPI-history consumer) ──
// ── Trends (first KPI-history consumer) ──
// Window in hours: 24h (default) or 168h (7d). Toggling re-queries.
// Per-metric isolation (mirrors the K14 SiteCallsReport pattern): each metric
// Per-metric isolation (mirrors the SiteCallsReport pattern): each metric
// carries its own series + availability + error, each loaded via its own
// try/catch so one metric's failure only blanks that one chart and the others
// still render their already-fetched data.
@@ -172,7 +172,7 @@
<td><TimestampDisplay Value="@n.CreatedAt" Format="yyyy-MM-dd HH:mm" /></td>
<td><TimestampDisplay Value="@n.DeliveredAt" Format="yyyy-MM-dd HH:mm" NullText="—" /></td>
<td class="text-end" @ondblclick:stopPropagation="true">
@* Bundle D (#23 M7-T10) drill-in: NotificationId is the audit
@* NotificationId is the audit
CorrelationId, so the link deep-links into the central Audit
Log pre-filtered to this notification's lifecycle events. *@
<a class="btn btn-outline-secondary btn-sm me-1"
@@ -14,7 +14,7 @@
@inject IDialogService Dialog
@inject ILogger<SecuredWrites> Logger
@* Secured Writes (M7 OPC UA / MxGateway UX, Task C5 / T14b). Two-person workflow:
@* Secured Writes. Two-person workflow:
an Operator submits a write against an MxGateway connection; a DIFFERENT Verifier
approves (which relays the write to the device) or rejects. This page only SUBMITS
commands — the server (ManagementActor) enforces roles, the no-self-approval guard,
@@ -224,7 +224,7 @@
OnNext="NextPage" />
}
@* ── Trends (K14: collapsible KPI-history charts) ──
@* ── Trends (collapsible KPI-history charts) ──
A best-effort section — each chart's GetSeriesAsync is wrapped so a KPI
backend hiccup renders the chart unavailable rather than breaking the
grid above. The series load lazily on first expand (and re-load on the
@@ -11,7 +11,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.SiteCalls;
/// <summary>
/// Code-behind for the central Site Calls report page (Site Call Audit #22). A
/// Code-behind for the central Site Calls report page. A
/// near-mirror of <see cref="ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Notifications.NotificationReport"/>:
/// it queries the central <c>SiteCalls</c> table via
/// <see cref="ZB.MOM.WW.ScadaBridge.Communication.CommunicationService.QuerySiteCallsAsync"/>,
@@ -60,7 +60,7 @@ public partial class SiteCallsReport
private ToastNotification _toast = default!;
private List<Site> _sites = new();
// CentralUI-028: unfiltered site list so a permitted-site lookup resolves
// Unfiltered site list so a permitted-site lookup resolves
// correctly for a SourceSite whose Site was filtered out of the dropdown.
private List<Site> _allSites = new();
private bool _siteScopeSystemWide;
@@ -96,7 +96,7 @@ public partial class SiteCallsReport
private DateTime? _fromFilter;
private DateTime? _toFilter;
// ── Trends (K14) ──────────────────────────────────────────────────────────
// ── Trends ──────────────────────────────────────────────────────────────
// Collapsible KPI-history charts for the SiteCallAudit / Global series. The
// section is collapsed on init and the series load lazily on first expand
// (and on each 24h/7d window toggle), so the page's primary job — the call
@@ -127,7 +127,7 @@ public partial class SiteCallsReport
try
{
_allSites = (await SiteRepository.GetAllSitesAsync()).ToList();
// CentralUI-028: restrict the source-site dropdown to the user's
// Restrict the source-site dropdown to the user's
// permitted set. System-wide users see the full list back unchanged.
_sites = await SiteScope.FilterSitesAsync(_allSites);
_siteScopeSystemWide = await SiteScope.IsSystemWideAsync();
@@ -249,7 +249,7 @@ public partial class SiteCallsReport
var response = await CommunicationService.QuerySiteCallsAsync(request);
if (response.Success)
{
// CentralUI-028: drop any row whose source site is outside the
// Drop any row whose source site is outside the
// user's permitted set, as a row-level safety net behind the
// dropdown restriction.
_siteCalls = await FilterPermittedAsync(response.SiteCalls);
@@ -278,7 +278,7 @@ public partial class SiteCallsReport
private async Task RetrySiteCall(SiteCallSummary c)
{
// CentralUI-028: server-side re-check before relaying — a Retry relay must
// Server-side re-check before relaying — a Retry relay must
// not fire for a site outside the caller's permitted set, even if the row
// somehow appeared in the grid.
if (!await IsRowSiteAllowedAsync(c.SourceSite))
@@ -473,7 +473,7 @@ public partial class SiteCallsReport
private static string? NullIfEmpty(string s) => string.IsNullOrWhiteSpace(s) ? null : s.Trim();
/// <summary>
/// CentralUI-027: <c>&lt;input type="datetime-local"&gt;</c> binds with
/// <c>&lt;input type="datetime-local"&gt;</c> binds with
/// <see cref="DateTimeKind.Unspecified"/> and the value is the operator's
/// browser-local wall-clock. Tag it <see cref="DateTimeKind.Local"/> and
/// convert to UTC before the value enters the wire query — otherwise the
@@ -547,7 +547,7 @@ public partial class SiteCallsReport
return Task.FromResult(_permittedSiteIds.Contains(resolved.Id));
}
// ── Trends (K14) ──────────────────────────────────────────────────────────
// ── Trends ──────────────────────────────────────────────────────────────
/// <summary>
/// Expand/collapse the Trends section. The series load on first expand (and