Commit Graph

127 Commits

Author SHA1 Message Date
Joseph Doherty 82f52e81ce fix(ui): mirror the EWS username:password credentials rule on the SMTP page 2026-08-10 06:53:15 -04:00
Joseph Doherty 3e490f2a7d test(ui): bUnit coverage for the SMTP page transport selector state machine 2026-08-10 06:50:39 -04:00
Joseph Doherty d0af884760 feat(scripts): add the Alarms.CurrentAsync() read accessor for site scripts
MES alarm-status API §5.2 (docs/plans/2026-06-30-mes-alarm-status-api.md,
Phase 1 tasks 2-4). Site `Call` scripts had NO way to read alarm condition
state: the `Alarm` global exists only inside an on-trigger handler and
describes the one alarm that fired, and native mirrored conditions were
reachable only from the Debug View. That gap blocked the CvdReactor
SimpleAlarmStatus/AlarmStatus scripts entirely -- they cannot be written
without it. `Alarms.CurrentAsync()` closes it.

The data was already local: the script runs inside its own Instance Actor's
context, so this is a LOCAL Ask -- the same mechanism attribute reads use, no
cross-cluster hop. A dedicated GetAlarmSnapshotRequest/Response is used rather
than reusing DebugSnapshotRequest, which would materialise every attribute
value on every alarm poll; both are served from the same
BuildAlarmStatesSnapshot(), so the script view and the operator's Debug View
can never disagree.

Deliberate shape decisions:
  - NOT scope-prefixed, unlike Attributes. Alarm identity is not a
    scope-relative attribute name (computed alarms are keyed by configured
    name, native conditions by a source-supplied reference), so prefixing
    would hand a composed script a silently truncated list.
  - Read-only. Native alarms are a read-only mirror of the source (no
    ack-back), so no acknowledge/shelve operation is exposed.
  - Placeholder rows are NOT pre-filtered: a caller must be able to tell
    "binding configured and quiet" from "binding unknown". The documented
    filter is `Active && !IsConfiguredPlaceholder`.
  - ScriptAlarm lives in Commons so the runtime accessor and the compile-only
    surface project to the SAME type -- a script binding at the design-time
    gate binds identically at the site. Condition is the authority for
    active/acked/severity, so one filter expression works across computed and
    native alarms.

Mirrored on BOTH design-time surfaces. ScriptCompileSurface is covered by the
reflection parity guard (AlarmsAccessor added to its mirror pairs). The Central
UI Test-Run SandboxScriptHost is the third, hand-maintained mirror that the
parity test cannot reach (Central UI does not reference Site Runtime); without
it the design page would false-flag CS1061 on scripts the deploy gate accepts.
It throws a labelled ScriptSandboxException at run time -- there is no central
route to per-instance alarm state, and returning an empty list would read as
"nothing is in alarm", which is worse than an error.

ScriptTrustPolicy needs NO change, and the reason is structural rather than
incidental: the trust boundary is a deny-list over API roots, not an allow-list
of context members. A test pins that no ForbiddenScopes entry prefixes the
Commons script-surface namespace, so a future deny-list entry cannot silently
make ScriptAlarm untouchable.

Tests: 6 accessor cases (Ask contract, full native projection incl. AckTime,
unacked, computed-alarm derivation, placeholder visibility, scope-independence),
2 InstanceActor snapshot cases incl. equality with the Debug View row set, the
full MES script shape compiling against ScriptCompileSurface, 2 trust cases,
and a sandbox diagnose-clean case reading every projected ScriptAlarm field.
2026-08-01 13:12:30 -04:00
Joseph Doherty 316153dc98 feat(centralui): hide OffsetPager on a single page, opt-in — NotificationReport enabled (M10 residual)
DECISION: a pager whose only two controls are permanently disabled is noise, so
the conventional behaviour ships — the bar is hidden when the result set is a
single page. NotificationReport, which raised the finding, opts in.

RATIONALE for opt-in rather than default-on. OffsetPager is shared, and its
summary span ("Page N of Y · T total") doubles as the ONLY total-count readout
on ConfigurationAuditLog — auto-hiding there would silently delete the result
count for every query returning under a page, and the existing Playwright
fixture asserts "3 total" on exactly such a query. So the behaviour lives in the
shared component (reusable for the next consumer) behind HideWhenSinglePage,
default false, which keeps every current consumer byte-identical.

The guard fires only when single-page-ness is POSITIVELY established:
PageCount <= 1 AND Page <= 1 AND !HasNextPage. A null TotalCount means the host
cannot count, so the bar renders — the controls never vanish while a further
page still exists.

NotificationReport's own Playwright pagination test seeds 51 rows (2 pages) and
is unaffected. 5 new bUnit tests cover hide-on-single-page, hide-on-empty,
show-on-multi-page, show-when-TotalCount-unknown, and the default-off path.
2026-08-01 11:28:20 -04:00
Joseph Doherty a506b19d17 refactor(centralui): migrate TemplateEdit's four page-embedded modals to the DialogService host (M10 residual)
TemplateEdit was the last page still hand-rolling its own modal chrome after
M10 — T34c only tokenized its backdrops. All four member-authoring forms
(Attribute, Alarm, Native Alarm Source, Script) now open through
IDialogService.ShowAsync, so the single DialogHost in MainLayout owns the
backdrop, focus trap, Escape and focus restoration.

Pattern copied from the already-migrated pages (MoveDataConnectionDialog +
Templates' Move/Rename dialogs): each body is its own component beside the page
taking a DialogContext<bool>, owning its form state, rendering validation and
server errors INLINE while staying open, and closing with Close(true) only once
the save succeeded — at which point the page reloads. An inline RenderFragment
would NOT have worked: DialogHost renders the captured fragment in its own tree,
so the page's StateHasChanged could never refresh it.

Persistence deliberately stayed on the page (it owns TemplateService, the
inherited-member rules, and the repository-direct native-source path) and is
reached through an OnSaveAsync delegate returning null on success or the message
to display. Behaviour preserved verbatim, including the List-attribute encode +
Decode round-trip check, the name/trigger-type read-only-on-edit rules, the
duplicate-native-source-name guard, and NormalizeExecutionTimeout.

TemplateScriptDialog keeps all four tab panels mounted (Monaco and the JSONJoy
island must not tear down on tab switch) and hosts the Test Run panel, which
needs the live unsaved editor buffer, so it injects ScriptAnalysisService
directly and cancels an in-flight run on dispose.

Extraction moved markup that three structural source-scanning tests pinned;
all three were repointed at the new files rather than weakened:
  - TemplateNativeAlarmSourceEditorTests (+ a new test asserting the form is
    host-mounted and the body renders no chrome of its own)
  - AttributeListEditorTests (list-editor reveal now in the dialog body)
  - TestRunWarningTests (Real I/O warning travelled with the script panel)

Build 0/0; CentralUI.Tests 973/973 green.
2026-08-01 11:28:20 -04:00
Joseph Doherty fdfd5e1b27 feat(centralui): TreeView full WAI-ARIA keyboard navigation (M10 residual R7)
TreeView<TItem> handled only Enter/Space on the chevron; the tree itself was
unreachable by keyboard. Implements the WAI-ARIA tree pattern:

- Roving tabindex on li[role=treeitem] — exactly one node is tabbable, so the
  whole tree is a single Tab stop. Target resolved per render: last-focused node
  while still visible -> SelectedKey -> first visible node. Browser focus follows
  via a per-node ElementReference + FocusAsync in OnAfterRenderAsync, guarded by
  the same JSException/JSDisconnectedException/InvalidOperationException triple
  the context-menu focus already used (a no-op under bUnit).
- ArrowDown/ArrowUp move between VISIBLE nodes; ArrowRight expands a collapsed
  branch else moves to first child; ArrowLeft collapses an expanded branch else
  moves to parent; Home/End jump to first/last; Enter/Space activate through the
  SAME path a click takes (OnContentClick / OnCheckboxToggle) so selection
  semantics never diverge between input modes.
- ARIA: aria-level, aria-posinset, aria-setsize added; aria-selected now renders
  true/false on selectable+checkbox trees and stays absent on non-selectable ones.
- Event scoping: @onkeydown:stopPropagation on the li, chevron, checkbox and
  content slot, so nested nodes do not double-handle and consumer controls inside
  NodeContent keep their own key handling. Browser scroll-on-Space/Arrow is
  suppressed by a minimal NATIVE inline onkeydown on the root ul, targeted at
  treeitems only — Blazor's preventDefault directive is all-or-nothing per
  element, so on the li it would trap Tab and cancel Enter/Space on consumer
  buttons. No CSP is configured, so the inline handler runs.

BuildVisibleNodes() mirrors RenderNode's visibility rules and must stay in step.

Tests: 31 new bUnit tests in TreeViewKeyboardNavigationTests; the 47 existing
TreeView tests are unchanged and still green. Docs: new keyboard/a11y section in
docs/components/TreeView.md.
2026-08-01 11:28:20 -04:00
Joseph Doherty a26d6ba317 feat(central-ui): native-alarm-source CSV import on InstanceConfigure
The Native Alarm Source Overrides card had no bulk affordance — the CSV path
shipped CLI-only (instance native-alarm-source import --file), so the UI could
only retarget one source at a time inline.

Adds a second InputFile on that card, mirroring the attribute importer's UX
(hidden input behind a button-styled label, 512 KB cap, success/error alert
with the per-line error list, toast). Parsing reuses the SHARED
NativeAlarmSourceOverrideCsvParser — the exact parser the CLI uses — and the
new pure InstanceConfigure.BuildNativeAlarmSourceCsvImport applies the same
batch rules the server enforces in
ManagementActor.HandleSetInstanceNativeAlarmSourceOverrides: the source must
resolve, must not be template-locked, and may appear at most once; any error
rejects the whole file and applies nothing.

Semantics match the CLI: merge, not full replace — sources absent from the
file keep their existing override, a blank field keeps the inherited value,
and an all-blank row clears that source's override (equivalent to the CLI's
all-null override row, without leaving a dead row behind).

Persistence reuses the inline editor's path: SaveNativeOverride's upsert body
is extracted to UpsertNativeOverrideCore (no SaveChangesAsync inside), so the
import commits the validated batch in one SaveChangesAsync — no new server
method, no duplicated parsing.

Tests: InstanceConfigureNativeAlarmCsvImportTests (9) — happy path, blank-field
inheritance, all-blank clear, merge semantics, unknown/locked/duplicate source
and parser-error rejection, plus structural pins on the InputFile wiring.
Docs: Component-CentralUI.md native-alarm-source card gains the import bullet.
2026-08-01 11:11:40 -04:00
Joseph Doherty 1c9159a7ce fix(tests): qualify Health page type — Communication.Health namespace shadowed it 2026-07-24 13:35:24 -04:00
Joseph Doherty c8e2f4da02 feat(cluster): site-pair manual failover relayed from the central UI (Task 10)
Central and each site are SEPARATE Akka clusters, so central cannot act on a
site's membership -- it asks. New TriggerSiteFailover/SiteFailoverAck contract
travels the existing ClusterClient command/control channel (mirroring the
RetryParkedOperation relay); the site's own SiteCommunicationActor performs the
graceful Leave and acks the outcome.

- ClusterFailoverCoordinator moved out of Host into Communication/ClusterState,
  beside ActiveNodeEvaluator. Both paths now share ONE oldest-Up implementation;
  SiteCommunicationActor cannot reference Host, and the two definitions must not
  drift or the node asked to leave stops being the singleton host.
- Site scope is the SITE-SPECIFIC role (site-{SiteId}), not the base Site role --
  site singletons are placed on the former, so the base role would move the wrong
  node. Pinned by a unit test asserting the role string and by a real-cluster test.
- Site-side guards: refuses a command addressed to another site (a misroute must
  never fail over a site the operator did not select), refuses when there is no
  peer, and reports a fault as an ack rather than throwing into supervision --
  a restart there would drop central's Ask into a bare timeout and lose the reason.
- Ack is sent before the Leave takes effect so it still reaches central.
- UI: the same control now serves both scopes via a SiteId parameter. The site
  confirmation deliberately does NOT claim the admin's page will disconnect --
  it won't, and crying wolf there devalues the central warning that is real. A
  site refusal and an unreachable site surface distinctly.
- Rolling upgrade: a site on an older binary has no handler, so the message
  dead-letters and the Ask times out, reported as "site did not respond". That
  is the honest outcome; documented on the contract.

Fallout fixed: HealthPageTests now renders the page inside
CascadingAuthenticationState with the real policy set and IAuthorizationService,
because the cards embed an AuthorizeView. That mirrors production, where the
layout supplies the cascading value.
2026-07-22 07:48:56 -04:00
Joseph Doherty caf14a3e03 feat(ui): admin manual-failover control on the health page
CentralFailoverControl lives in Components/Health/ (matching AuditKpiTiles /
SiteCallKpiTiles) rather than inline in Health.razor, so it is testable without
standing up the whole dashboard's DI graph.

- Admin-gated via AuthorizeView + RequireAdmin. The Health page itself is
  intentionally all-roles, so the gate belongs on the control, not the page.
- Disabled with an explanatory title when the pair has no online standby;
  the authoritative guard remains server-side against live cluster membership.
- Confirmation dialog (IDialogService, the page idiom) warns that singletons
  hand over, in-flight work on the active node is interrupted, and THIS PAGE
  will disconnect and reconnect against the new active node -- Traefik routes
  the UI to the node being restarted, so a working failover otherwise reads as
  a crash the admin caused.
- A refused failover (service returns null) surfaces the refusal; the UI never
  reports a failover that did not happen.

7 bUnit tests. Two harness requirements that bit first: AuthorizeView needs a
cascading AuthenticationState (the app supplies it from the layout), and
BunitContext pre-registers a placeholder IAuthorizationService that throws on
policy evaluation -- both handled the same way NavMenuTests documents.

Runbook paragraphs added to Component-ClusterInfrastructure.md (new Manual
Failover section) and docker/README.md.
2026-07-22 07:00:10 -04:00
Joseph Doherty f056b67e9a fix(build): suppress AngleSharp advisory instead of pinning (corrects ecf6b628)
ecf6b628 pinned AngleSharp to 1.5.2 to clear GHSA-pgww-w46g-26qg. That fixed the
build but BROKE 33 CentralUI bunit tests at runtime with

    MissingMethodException: AngleSharp.Dom.IHtmlCollection`1.get_Item(Int32)

because 1.5.x changed IHtmlCollection<T>'s indexer and bunit is compiled against
1.1.x. My mistake: I verified `dotnet build` was clean and did not run the test
suite before committing, so a runtime-only break sailed through.

There is no working patched combination upstream. Verified empirically:
  AngleSharp 1.1.2 / 1.2.0 / 1.3.0 / 1.4.0 -> still flagged NU1902
  AngleSharp 1.5.0 / 1.5.2                 -> patched, but breaks bunit at runtime
  bunit up to 2.7.2 (latest)               -> still resolves AngleSharp 1.4.0

So the real choice is a suppressed advisory or an unbuildable suite. Scoped
suppression wins here because the reach is nil: AngleSharp is an HTML parser bunit
uses to assert on rendered markup, it ships in no production project, and the only
"documents" it parses are our own components' output.

Explicitly NOT the same call as GHSA-2m69-gcr7-jv3q (SQLitePCLRaw), whose
suppression was removed in 2026-07: that one masked a vulnerability on PRODUCTION
code paths and had a working patched version available. Neither is true here.
Revisit when bunit ships against AngleSharp 1.5+.

Verified:
  dotnet build ZB.MOM.WW.ScadaBridge.slnx -> 0 Error(s), 0 Warning(s)
  CentralUI.Tests                          -> 925 passed, 0 failed (was 33 failing)

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-19 03:14:58 -04:00
Joseph Doherty ecf6b62850 fix(build): pin AngleSharp 1.5.2 to unbreak the solution build
GHSA-pgww-w46g-26qg (NU1902, moderate) on AngleSharp 1.1.1, pulled in
transitively by bunit into CentralUI.Tests. Under TreatWarningsAsErrors this
made `dotnet build ZB.MOM.WW.ScadaBridge.slnx` fail on a CLEAN main - the whole
suite was unbuildable and every "0 warnings / suite green" gate unverifiable.

Pre-existing, not introduced here; surfaced while starting LocalDb Phase 1,
whose per-task verification depends on a green baseline.

Same fix as the identical break in HistorianGateway (historiangw @ 6bc005d):
pin the leaf, test-only. AngleSharp is a bunit HTML-parsing dependency and
reaches no production project. Bumping bunit does not help - 2.0.33-preview is
the current preview line and still resolves the vulnerable AngleSharp.

Verified: dotnet build ZB.MOM.WW.ScadaBridge.slnx -> 0 Error(s), 0 Warning(s)
(was 1 Error before this commit).

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-19 02:37:50 -04:00
Joseph Doherty 2a0faeab6f fix(security): actually fix GHSA-2m69-gcr7-jv3q instead of suppressing it
The NuGetAuditSuppress in Directory.Packages.props was masking a LIVE high-severity
vulnerability, not documenting an accepted one. Only the Host project resolved a
patched SQLitePCLRaw.lib.e_sqlite3 2.1.12 (transitively, via ZB.MOM.WW.Auth.ApiKeys).
Every other SQLite consumer - AuditLog, SiteRuntime, StoreAndForward, SiteEventLogging
and 11 test projects - still resolved the vulnerable 2.1.11.

The suppression's rationale was factually wrong: it claimed 'the only patched native
lib is the SQLitePCLRaw 3.x line'. 2.1.12 patches this advisory within the 2.1.x line,
so the feared risky force-override of the whole family to 3.x is unnecessary.

Fix: explicit PackageReference to the patched 2.1.12 in each SQLite-consuming project,
plus the central PackageVersion row. Suppression removed, so the advisory is audited
again rather than silenced.

Rejected alternative: CentralPackageTransitivePinningEnabled. It clears the advisory in
one line but makes every central version a ceiling for transitive resolution, demanding
bumps to Google.Protobuf, Grpc.Net.Client, Microsoft.Data.SqlClient and Newtonsoft.Json.
That is a gRPC/data-access change to a production SCADA platform and deserves its own
reviewed commit.

Verified: forced full-solution restore reports no NU1903 (only the pre-existing,
unrelated AngleSharp NU1902 in CentralUI.Tests); all four previously-vulnerable src
projects now resolve 2.1.12; 55 projects build 0 warnings / 0 errors; 1108 tests pass
across the SQLite layer (AuditLog 354, ConfigurationDatabase 357, Security 181,
StoreAndForward 152, SiteEventLogging 64).
2026-07-18 05:30:56 -04:00
Joseph Doherty ca4b0dd849 merge(r2): r2-plan04 2026-07-13 11:09:36 -04:00
Joseph Doherty 76ef97f729 fix(ui): disposal guard on Alarm Summary live callback, matching the DebugView pattern (plan R2-07 T9) 2026-07-13 10:58:50 -04:00
Joseph Doherty 925c869826 fix(ui): Alarm Summary poll defers row rebuilds to the live path while IsLive (plan R2-07 T8) 2026-07-13 10:57:09 -04:00
Joseph Doherty d33337a834 fix(ui): drop stale-site poll results on Alarm Summary site switch (plan R2-07 T7) 2026-07-13 10:55:25 -04:00
Joseph Doherty 9a1b160e79 fix(kpi): query service applies catalog aggregation at the bucketer — rate charts keep one unit across the raw/rollup boundary (plan R2-04 T6) 2026-07-13 10:10:30 -04:00
Joseph Doherty 8c888f13a0 merge(deferred-10): aggregated live alarm stream for Alarm Summary
Merges plan #10 (feat/live-alarm-stream) into main alongside plan #22.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 13:01:04 -04:00
Joseph Doherty b91ed3c840 feat(alarm-summary): live-cache-driven updates with poll fallback (plan #10 T5)
Wire the operator Alarm Summary page to the transient per-site live alarm
cache (ISiteAlarmLiveCache, T4). Live-cache-first: on site select the page
subscribes and rebuilds rows/rollup from near-real-time onChanged deltas; the
15s poll is kept untouched as the authority for NotReporting and as the
safety net whenever the cache is not live (pre-seed or degraded stream). Both
paths mutate shared state only via the Blazor dispatcher, so they never race,
and each rebuild is an idempotent snapshot.

- IAlarmSummaryService.BuildFromLiveAlarms: flattens a live AlarmStateChanged
  snapshot to AlarmSummaryRows with the same deterministic instance-then-name
  sort as GetSiteAlarmsAsync; NotReporting always empty on the live path.
- AlarmSummary.razor: inject ISiteAlarmLiveCache; subscribe on select,
  re-subscribe on site change, unsubscribe on leave + Dispose.
- Tests: service BuildFromLiveAlarms flatten/sort/empty; page subscribe,
  poll-fallback render, onChanged rebuild, unsubscribe on leave/change/dispose.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 12:25:36 -04:00
Joseph Doherty bbfc1b3278 feat(kpihistory): add 30d/90d trend windows to Audit/SiteCalls/Notifications/Health (plan #22 T7)
Adds 30d (720h) and 90d (2160h) toggle buttons alongside the existing
24h/7d controls on all four KPI trend surfaces. No fetch-logic changes:
the setters already recompute fromUtc and re-fetch, and the query service
(T5) transparently routes windows > RollupThresholdHours to hourly rollups
— so these windows are what actually exercise the rollup path. bUnit tests
extend the Audit and SiteCalls trend harnesses to cover the new buttons.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 12:12:17 -04:00
Joseph Doherty a10170f365 feat(kpihistory): route long-range KPI queries to hourly rollups by threshold (plan #22 T5)
GetSeriesAsync now branches on window width: windows wider than
RollupThresholdHours (default 168h/7d) read pre-aggregated hourly rollups
via GetHourlySeriesAsync; windows at or below the threshold read raw
samples via GetRawSeriesAsync (preserving intra-minute detail on 24h/7d).
The boundary is strict greater-than, so exactly 168h stays on the raw
path. KpiSeriesBucketer.Bucket runs unchanged on whichever series (a 90d
rollup can still exceed the point ceiling). Both ctors route identically
via a shared FetchSeriesAsync helper reading _options.RollupThresholdHours,
the same options instance that already supplies DefaultMaxSeriesPoints.
GetSeriesAsync public signature unchanged.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 12:01:59 -04:00
Joseph Doherty 22136d36d9 feat(testrun): route WaitForAttribute in the Test-Run sandbox (deferred #14)
Closes deferred-work register item #14 with full fidelity. The Central UI Test-Run
sandbox previously threw ScriptSandboxException for Attributes.WaitAsync/WaitForAsync;
now the value-equality forms route to the bound deployed instance over the same
cross-site path inbound Route.To().WaitForAttribute() uses.

- Commons: additive trailing RouteToWaitForAttributeRequest.RequireGoodQuality
  (default false; message-evolution safe) so quality-gated waits route too.
- SiteRuntime: DeploymentManagerActor's routed-wait handler now threads
  request.RequireGoodQuality into the InstanceActor WaitForAttributeRequest
  (previously hard-coded to default).
- CentralUI sandbox: ISandboxInstanceGateway.WaitForAttributeAsync +
  SandboxInstanceGateway impl (via CommunicationService.RouteToWaitForAttributeAsync);
  SandboxAttributeAccessor value-equality WaitAsync/WaitForAsync route through it
  (codec-encoded target, scope-resolved name). Predicate overloads stay unsupported
  (an in-process lambda can't be routed) and throw a clearer, dedicated message.
- Tests: sandbox accessor routing + predicate/unbound rejection (CentralUI);
  site-handler RequireGoodQuality threading (SiteRuntime). Register + the historical
  waitfor-deferred plan doc updated.

Full slnx build 0/0; CentralUI 59, SiteRuntime routed-wait 4, InboundAPI wait 32 green.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 09:29:38 -04:00
Joseph Doherty d8a39f3c35 feat(ui): secured-write history paging + submission age in approve dialog (arch-review S2/P3)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:57:58 -04:00
Joseph Doherty 75bf14a35a chore(ui): memoize AlarmSummary rows + keyboard/aria-sort on sortable headers (arch-review P4/UA6)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:16:40 -04:00
Joseph Doherty adc488a9f9 fix(ui): reentrancy guard on Health/AlarmSummary poll timers (arch-review S3)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:04:18 -04:00
Joseph Doherty 40088a2123 fix(notifications): validate Twilio AccountSid format at save — closes the un-escaped URI interpolation door
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 04:36:47 -04:00
Joseph Doherty f0dd016207 feat(esg): TimeoutSeconds in ExternalSystem UI form + Component-ExternalSystemGateway doc sync — closes the spec-drift gap
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 04:07:44 -04:00
Joseph Doherty 854a6a8c0b perf(deployment): staleness/comparison paths no longer Roslyn-compile every script — deploy gate remains the authoritative compile
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-09 15:49:55 -04:00
Joseph Doherty 9049690bdb fix(centralui-tests): register stub IBrowseService in SecuredWritesTests harness (PLAN-07 known-issue resolved)
The Secured Writes page embeds NodeBrowserDialog (@inject IBrowseService) for
the OPC UA tag picker; bUnit resolves the injected service at render time, so
three SecuredWritesTests failed with 'no registered service of type
IBrowseService'. Register a Substitute.For<IBrowseService>() singleton in the
fixture (mirrors NodeBrowserDialogSearchTests). Suite now 7/7 green.

Marks the PLAN-07 'Post-plan known issues' entry and the 00-MASTER-TRACKER
registry cross-reference RESOLVED.
2026-07-09 05:55:53 -04:00
Joseph Doherty 87c7255912 feat(ui): health dashboard shows metrics-stale badge and offline-since timestamp 2026-07-08 16:23:43 -04:00
Joseph Doherty c6a32e8fef test(central-ui): cover tree-select path for OnNodeSelected DataType 2026-06-27 13:48:58 -04:00
Joseph Doherty 1ad19e28f6 feat(central-ui): NodeBrowserDialog emits full node + optional ShowSearch
Fully suppress search UI (box + results panel) when ShowSearch=false, and
reset search state on ShowAsync so a reused dialog never shows stale results.
2026-06-27 13:36:50 -04:00
Joseph Doherty 265f115a1f feat(central-ui): add Galaxy-type to DataType mapper for secured writes 2026-06-27 13:27:32 -04:00
Joseph Doherty 99254b71de perf(ui): topology page — staleness off the live loop + parallelized one-shot 2026-06-26 16:53:44 -04:00
Joseph Doherty 10f752df02 feat(deploy): stage pending config + send RefreshDeploymentCommand (notify-and-fetch) 2026-06-26 13:06:07 -04:00
Joseph Doherty 1f261263b2 fix(centralui): surface inherited compositions in the templates tree (followup #9)
The templates tree rendered a derived/composed member (e.g. LeftReactorSide,
derived from ReactorSide) as a flat leaf, omitting compositions it inherits
from its base (e.g. LeakTest composed onto ReactorSide). BuildCompositionLeavesFor
recursed only over a template's OWN composition rows; an inherited composition
row lives on the ancestor, and TemplateComposition has no IsInherited placeholder
(unlike attributes/alarms/scripts/native-sources), so the child's own Compositions
was empty. Same 'derived templates don't surface inherited members' family as
followups #1/#2, but for compositions. Deploy/flatten was always correct
(TemplateResolver.ResolveAllMembers walks the chain) — display-only.

Fix:
- BuildCompositionLeavesFor now renders the EFFECTIVE composition set (own +
  inherited) via EffectiveCompositionsFor, which walks the inheritance chain
  (leaf->root, child wins on InstanceName), mirroring the resolver.
- Inherited slots are flagged (TemplateTreeNode.IsInherited), badged 'inherited'
  in the label, and their context menu offers only 'Open composed template'
  (Rename/Delete edit the ancestor's slot, so suppressed on inherited nodes).
- The same inherited row can appear under several derived members (LeakTest under
  both LeftReactorSide and RightReactorSide), so composition nodes use a
  path-qualified KeyOverride to keep TreeView selection/expansion keys unique;
  recursion is cycle-guarded.

Tests: +1 bUnit (TemplatesPageTests.Renders_InheritedComposition_UnderDerivedComposedMember);
CentralUI suite 867 green; full solution builds 0/0.

Docs: Component-CentralUI.md (effective composition set in tree); known-issues
tracker #9 recorded + resolved.

Note: CentralUI change — shows on wonder-app-vd03 only after that host is redeployed.
2026-06-24 19:29:48 -04:00
Joseph Doherty b3f6833b36 fix(templateengine+centralui): resolve follow-ups #3 (derived-template collisions) and #7 (sandbox batch/wait surface)
#3 — CollisionDetector counted a derived template's IsInherited placeholder
rows as a distinct origin from the parent members the inheritance walk
re-adds, reporting a spurious "Naming collision" for every inherited row and
blocking any attribute/composition add to a derived template. CollectDirectMembers
now skips IsInherited rows on the direct-template and inherited-parent walks;
it keeps them for the composed-module walk, where placeholders are the sole
representation of a derived module's inherited members (that walk does not
climb the composed template's parent chain).

#7 — SandboxAttributeAccessor (Central UI Test-Run host) omitted
WriteBatchAndWaitAsync / WaitAsync / WaitForAsync, so the editor false-flagged
valid instance scripts with CS1061 even though `template validate` and the
deploy gate accept them. Added the five overloads mirroring the runtime
AttributeAccessor; they throw a labelled ScriptSandboxException if run in
Test Run (the central sandbox has no device-batch / event-waiter transport).

Tests: +3 CollisionDetector unit + 1 end-to-end TemplateService (derived add
now succeeds); +2 ScriptAnalysisService diagnose-clean. Each new test verified
to fail without its fix with the exact user-facing symptom. Full suites green
(TemplateEngine.Tests 438, CentralUI.Tests 866).

Docs: Component-TemplateEngine.md (inherited-placeholder collision rule),
Component-ScriptAnalysis.md (third sandbox surface + its compile-clean guard),
known-issues tracker #3/#7 marked resolved and the minor note promoted to #8.
2026-06-24 15:03:27 -04:00
Joseph Doherty b3c9014379 fix(inbound): authorize+secure Database helper, async/deadline-bound DB, wait-timeout-bound WaitForAttribute
Resolves InboundAPI-026/027/028/029 (+ newly-surfaced -030).

- 026: authorize the scoped Database helper in the design doc; SQL-injection
  protection is parameter binding (values never concatenated); allow writes via
  ExecuteAsync; drop the false 'read-only' claim. Named connections only.
- 027: async ADO.NET end-to-end (no .GetAwaiter().GetResult()); honour the method
  deadline token on ExecuteScalarAsync/ExecuteReaderAsync/ExecuteNonQueryAsync +
  a CommandTimeout backstop derived from the method timeout.
- 028: negative-path tests (null-gateway, deadline cancellation, parameterization)
  + e2e Database + WaitForAttribute cases through the real endpoint.
- 029: WaitForAttribute is bounded by its WAIT timeout (per-wait CTS + client-abort
  + explicit token), NOT the method deadline (spec §6) — a long wait may outlive the
  method timeout; WithRequestAborted threads the raw client-abort token separately.
- 030: Central UI compile-surface mirrors (InboundScriptHost / SandboxInboundScriptHost)
  gained the Database member (drifted since the runtime helper was added) so the
  authorized async API type-checks at the design-time gate.
2026-06-23 22:00:17 -04:00
Joseph Doherty 33e1802e6d feat(sms): make FromNumber optional — support Twilio Messaging-Service-only configs (UI-Med-2)
Code-review finding UI-Med-2: the design doc + delivery adapter treat FromNumber and
MessagingServiceSid as either-or, but the entity ctor, EF schema, UI and CLI all hard-
required FromNumber — so a Messaging-Service-only Twilio config (a normal production
setup) could not be created. Bring the implementation into line with the spec:

- Commons: SmsConfiguration.FromNumber -> string? (ctor fromNumber optional);
  UpdateSmsConfigCommand.FromNumber -> string?.
- ConfigurationDatabase: FromNumber.IsRequired(false) + migration SmsFromNumberOptional
  (ALTER COLUMN nullable, idempotent; Down backfills '' — harmless, MsgSid keeps it
  deliverable) + regenerated model snapshot.
- Transport: SmsConfigDto.FromNumber -> string? (round-trips a Messaging-Service-only config).
- CentralUI: form validation requires AccountSid + at-least-one-of(FromNumber, MsgSid);
  nullable create/edit paths; From-number help text.
- CLI: --from-number no longer Required; BuildUpdateSmsConfigCommand validates the either-or.
- Adapter: From branch null-forgiving (guarded by the existing incomplete-config check).

Tests: ManagementActor MsgSid-only persists null FromNumber; CLI MsgSid-only builds +
neither-throws + contract (--from-number not Required); CentralUI MsgSid-only save.
2026-06-19 15:19:40 -04:00
Joseph Doherty a9393c8913 test(sms): regression tests for code-review fixes
Lock the behaviors changed by the review-fix commit + the security invariants:

- ManagementActorTests: UpdateSms/SmtpConfig now require Administrator (updated the
  existing success cases from Designer); + UpdateSmsConfig_WithDesignerRole_Returns
  Unauthorized and _WithEmptyAuthToken_PreservesExistingToken regression tests.
- SecretEncryptionTests: SmsConfiguration.AuthToken stored-encrypted round-trip +
  null round-trip (AccountSid stays plaintext) — guards ApplySecretColumnEncryption.
- ArtifactDiffTests: CompareSmsConfiguration New/Identical/Modified + the secret
  presence-only invariant (value never echoed, presence-flip shows <present> only).
- UpdateCommandContractTests: notification sms update core fields Required, --auth-token optional.
- NotificationListsPageTests: SMS recipient badge shows phone, not "Name <>".
- NotificationOutboxActorDispatchTests: SMS-typed notification routes to the SMS
  adapter (StubAdapter.Type made configurable), not the Email adapter.
- NotificationRecipientTests (new): ForEmail/ForSms + public-ctor invariants.
2026-06-19 15:09:47 -04:00
Joseph Doherty 9558d98343 feat(sms): export wizard SMS-config selection (S10c) 2026-06-19 11:16:32 -04:00
Joseph Doherty 78fadb82d2 test/fix(sms): scope S8 Type-column assertion + S9 RetryDelay display (review polish) 2026-06-19 11:00:06 -04:00
Joseph Doherty 4860aeff62 feat(sms): SMS configuration Central UI page + nav (S9) 2026-06-19 10:52:56 -04:00
Joseph Doherty 58ac06a358 feat(sms): NotificationLists Type column (S8) 2026-06-19 10:51:11 -04:00
Joseph Doherty f0c69aad83 feat(sms): NotificationListForm adapter-gated Type selector + per-type recipients (S7) 2026-06-19 10:38:29 -04:00
Joseph Doherty e814bf5efb fix(ui): per-field sub-schema for escape-hatch Monaco editors + dispose guard (#261) 2026-06-19 03:28:58 -04:00
Joseph Doherty 282bc8b53c fix(ui): templates root-menu Escape + double-menu guard + reorder disabled tests (#258)
- Root context menu now has tabindex/focus + Escape-key close (OnRootMenuKeyDown) mirroring the node menu
- Opening root menu calls _tree.DismissNodeContextMenu(); opening node menu fires OnNodeContextMenuOpened → DismissRootContextMenu so only one menu is ever visible
- Add FolderContextMenu_MoveUp_IsDisabled_OnFirstSibling and FolderContextMenu_MoveDown_IsDisabled_OnLastSibling bUnit tests
2026-06-19 03:28:18 -04:00
Joseph Doherty 8f85cce298 fix(ui): schema-library delete-audit name + busy guard + edit-row guard + sanitized create-race test (#260) 2026-06-19 03:28:11 -04:00
Joseph Doherty fb18253f32 fix(deploy): normalize snapshot List values (Decode→Encode) before staleness/diff (#102); CLI --value native-List help 2026-06-19 02:24:26 -04:00