Commit Graph

77 Commits

Author SHA1 Message Date
Joseph Doherty
2a76be1f94 feat(audit): multi-value filters across ManagementService, CLI and Central UI 2026-05-21 05:27:17 -04:00
Joseph Doherty
37c7a0e5ac feat(auditlog): multi-value AuditLogQueryFilter dimensions 2026-05-21 05:15:51 -04:00
Joseph Doherty
b3b02a8cb6 fix(centralui): apply status/stuck query-string filters on the Site Calls page 2026-05-21 05:08:50 -04:00
Joseph Doherty
44f1ee372a feat(centralui): Site Call KPI tiles on the Health dashboard 2026-05-21 05:04:16 -04:00
Joseph Doherty
d73b459057 fix(centralui): single relay toast, paging/skip polish, extra Site Calls tests 2026-05-21 04:59:12 -04:00
Joseph Doherty
7e9d74697b feat(centralui): Site Calls page with Retry/Discard and Audit drill-in 2026-05-21 04:51:14 -04:00
Joseph Doherty
babf5b99e7 feat(ui): notification detail modal shows message body + recipients 2026-05-21 02:49:17 -04:00
Joseph Doherty
ef5cf76026 feat(ui): notification report row double-click opens detail modal 2026-05-21 02:39:41 -04:00
Joseph Doherty
c66ef71017 feat(ui): SMTP config form TlsMode field
Add a TlsMode read-only row and a None/StartTLS/SSL select to the SMTP
Configuration page edit form. New configs default to None; edits load
and persist the chosen mode through the repository.
2026-05-21 02:13:02 -04:00
Joseph Doherty
fac31c6018 fix(ui): AuditLogQueryService uses scope-per-query to avoid DbContext race (#23 M7) 2026-05-20 21:33:38 -04:00
Joseph Doherty
6dea84cd28 feat(security): OperationalAudit + AuditExport permissions for Audit Log surface (#23 M7)
Bundle G (#23 M7-T15): replace the temporary Admin-only gate on the Audit
Log surface with two new permission policies — OperationalAudit (read) and
AuditExport (bulk-export) — so the read path and the forensic-export path
can be delegated independently.

ScadaLink.Security
- AuthorizationPolicies: add OperationalAudit + AuditExport policy
  constants; register them via RequireClaim with an explicit role allow-list
  (OperationalAuditRoles, AuditExportRoles) so the role-to-permission
  mapping is documented in one place.
- Default mapping: Admin and Audit roles grant both policies; AuditReadOnly
  grants OperationalAudit only (read access without bulk export); Design
  and Deployment grant neither.

ScadaLink.CentralUI
- AuditLogPage: switch the page-level [Authorize] to the OperationalAudit
  policy and wrap the Export-CSV button in an AuthorizeView gated on
  AuditExport so an OperationalAudit-only operator still sees the page +
  filters but cannot trigger the CSV pull.
- ConfigurationAuditLog: switch from RequireAdmin to OperationalAudit so
  both pages under the Audit nav group share the same gate.
- NavMenu: the Audit nav group now gates on OperationalAudit so the
  section header + both child links match the per-page policies.
- AuditExportEndpoints: switch RequireAuthorization from RequireAdmin to
  AuditExport — this is the authoritative gate; the AuthorizeView on the
  button is just a UX affordance.

Tests
- New AuditLogPagePermissionTests covers the 5 brief-mandated cases plus
  defence-in-depth for Admin-alone and AuditReadOnly users on the endpoint.
- SecurityTests: add policy-level coverage for the new role→permission
  matrix (Theory rows pin every role/policy combination).
- AuditExportEndpointsTests: switch to AddScadaLinkAuthorization() so the
  test host exercises the real production wiring under the new gate.
- AuditLogPageScaffoldTests: wrap the page render in a
  CascadingAuthenticationState so the new in-page AuthorizeView resolves
  the principal.
2026-05-20 21:09:42 -04:00
Joseph Doherty
8744630adb feat(ui): server-side streaming CSV export of Audit Log (#23 M7) 2026-05-20 20:57:01 -04:00
Joseph Doherty
943c2ced39 feat(ui): Audit KPI tiles on Health dashboard (#23 M7)
Adds three KPI tiles to the central Health dashboard for the Audit channel:
volume (rows in the last hour), error rate (Failed/Parked/Discarded over
total), and backlog (sum of SiteAuditBacklog.PendingCount across all sites).

Repo + service:
- IAuditLogRepository.GetKpiSnapshotAsync(window, nowUtc) — single aggregate
  SELECT over the trailing window returning total + error counts; nowUtc is
  optional for production callers and pinned by integration tests against the
  shared MSSQL fixture so the global counts are deterministic.
- AuditLogQueryService.GetKpiSnapshotAsync() — composes the repo aggregate
  with a sum of SiteAuditBacklog.PendingCount read from ICentralHealthAggregator.
- AuditLogKpiSnapshot record in Commons/Types/.

UI:
- New AuditKpiTiles Blazor component (Components/Health/) — three Bootstrap
  card-tiles, click navigates to /audit/log with the matching pre-filter.
- Health.razor wires the tiles in alongside the existing Notification Outbox
  KPIs; LoadAuditKpis() runs on every 10s refresh tick and degrades to em
  dashes + inline error if the query fails.
- AuditLogPage extended to parse ?status= so the error-rate tile drill-in
  (?status=Failed) auto-loads the grid.

Tests:
- AuditLogRepositoryTests: GetKpiSnapshotAsync mixed-status + empty-window
  cases against the MSSQL migration fixture.
- AuditLogQueryServiceTests: forwarding + backlog composition; sites with
  null SiteAuditBacklog contribute zero.
- AuditKpiTilesTests: 9 bUnit tests covering tile render, error-rate maths
  with safe zero-events handling, em-dash unavailable path, click-through
  navigation, and warning/danger border thresholds.
- HealthPageTests: new Renders_AuditKpiTiles_WithValues plus IAuditLogQueryService
  stub registration in the constructor so existing outbox tests still pass.
- AuditLogPageScaffoldTests: ?status=Failed auto-load + unknown status drop.
2026-05-20 20:43:57 -04:00
Joseph Doherty
38fc9b4102 feat(ui): drill-ins from detail pages to Audit Log (#23 M7)
Adds "Recent audit activity" deep links from four edit/detail pages into
the central Audit Log, each with a pre-filter encoded in the query string
that the Audit Log page (Bundle D0) now parses on initialization:

  - External Systems (Design/ExternalSystemForm)      → ?target={Name}
  - API Keys         (Admin/ApiKeyForm)                → ?actor={Name}&channel=ApiInbound
  - Sites            (Admin/SiteForm)                  → ?site={SiteIdentifier}
  - Instances        (Deployment/InstanceConfigure)    → ?instance={UniqueName}

The link is suppressed on create/new flows where there is nothing to
drill into yet. Instance is UI-only on the filter bar (the repository
filter contract has no instance column), so the page-side prefill threads
through the InitialInstanceSearch seam on AuditFilterBar.

Site Calls (#22 M7-T11) drill-in is DEFERRED: the Central UI does not
yet host a Site Calls listing page, per M3 reality notes. Add the
drill-in when that page lands.

#23 M7-T12
2026-05-20 20:26:28 -04:00
Joseph Doherty
1c20e81d77 feat(ui): drill-in from Notifications to Audit Log (#23 M7) 2026-05-20 20:20:54 -04:00
Joseph Doherty
450f8bca28 feat(ui): AuditLogPage parses query-string filters for drill-ins (#23 M7) 2026-05-20 20:19:47 -04:00
Joseph Doherty
ae4480e7aa feat(ui): AuditDrilldownDrawer with JSON/SQL render, cURL, drill-back, redaction badges (#23 M7)
Implements Bundle C (M7-T4 through M7-T8) of the Audit Log #23 M7
Central UI work: a right-side off-canvas drawer that opens from
AuditResultsGrid row clicks and renders one AuditEvent in full.

Cohesive single-component delivery:
- Read-only fields stacked (form-layout memory): Channel/Kind, Status,
  HttpStatus, Target, Actor, Source* provenance, CorrelationId,
  OccurredAtUtc, IngestedAtUtc, DurationMs.
- Channel-aware body renderer: DbOutbound {sql, parameters} payloads
  render a code-block with CSS-only .language-sql class plus a
  parameter <dl>; other channels JSON-pretty-print when parseable and
  fall back to verbatim <pre>.
- Redaction badges on Request/Response when the body contains the
  <redacted> or <redacted: redactor error> sentinels.
- Copy-as-cURL (API channels only) builds a curl command from Target
  + optional {method, headers, body} RequestSummary JSON and writes
  it via navigator.clipboard.writeText.
- Show-all-events drill-back navigates to /audit/log?correlationId={id}
  when the event carries a CorrelationId.
- Close button + backdrop-click both raise OnClose.

AuditLogPage wires Event/IsOpen/OnClose; row clicks now open the
drawer (HandleRowSelected pins _selectedEvent + _drawerOpen=true).

11 bUnit tests cover field rendering, JSON pretty-print, verbatim
fallback, SQL block, conditional buttons, redaction badges,
navigation drill-back, and clipboard interop. No third-party UI
libraries: Bootstrap offcanvas + scoped razor.css only.
2026-05-20 20:13:33 -04:00
Joseph Doherty
e052aa4ff8 feat(ui): AuditResultsGrid + AuditLogQueryService with keyset paging (#23 M7)
Adds the results grid + query facade for the central Audit Log page
(#23 M7-T3):

* IAuditLogQueryService / AuditLogQueryService — CentralUI facade over
  IAuditLogRepository.QueryAsync so the grid can be tested with a stubbed
  query source. Default page size is 100; callers can override per call.

* AuditResultsGrid.razor + .razor.cs — Blazor Server component (Bootstrap
  only, no third-party UI libs). Renders the 10 columns from
  Component-AuditLog.md §10 (OccurredAtUtc, Site, Channel, Kind, Status,
  Target, Actor, DurationMs, HttpStatus, ErrorMessage). Keyset-paged via
  the last visible row's (OccurredAtUtc, EventId) as the cursor; Next-page
  button disabled when the current page is short (no count query). Row
  clicks emit OnRowSelected(AuditEvent) for Bundle C's drilldown drawer.
  Status badges are colour-coded (Delivered=green; Failed/Parked/Discarded
  =red; other=gray). Error messages truncated to 80 chars with full text
  on hover.

* Column model framework: a ColumnOrder [Parameter] reorders columns by
  stable string keys; unknown keys are dropped. M7 scope decision (in the
  class doc): the framework is in place but drag-reorder / resize UX is
  not implemented — M7.x can add persisted-per-user reordering without
  rewriting the column model.

* AuditLogPage wired: hosts AuditFilterBar + AuditResultsGrid, threads
  the filter through and stubs OnRowSelected for Bundle C.

* AuditLogQueryService registered as scoped in AddCentralUI.

* Tests: 4 grid bUnit tests (10 columns rendered, next-page cursor
  carries last row, row click raises callback, badge classes for
  Failed vs Delivered), 2 service tests (filter+paging pass-through,
  default page size of 100). AuditLogPageScaffoldTests updated to
  provide the new ISiteRepository + IAuditLogQueryService stubs the
  page now resolves.
2026-05-20 20:02:46 -04:00
Joseph Doherty
13e84a76a7 feat(ui): AuditFilterBar component (#23 M7)
Adds the filter bar for the central Audit Log page (#23 M7-T2):

* AuditQueryModel — UI binding model with chip-style multi-select state for
  Channel/Kind/Status/Site, a Channel→Kind narrowing map (CachedSubmit and
  CachedResolve appear under both ApiOutbound and DbOutbound per
  Component-AuditLog.md §4), time-range presets (5min/1h/24h/Custom),
  free-text Instance/Script/Target/Actor searches and an Errors-only toggle.
  Collapses to the single-value AuditLogQueryFilter on ToFilter(utcNow);
  multi-select chips take the first selected per dimension and the
  Errors-only toggle pins Failed when Status chips are empty (chip-set wins
  otherwise) — documented Bundle B scope decision.

* AuditFilterBar.razor + .razor.cs — Blazor Server component (Bootstrap
  only, no third-party UI libs). Renders the 10 spec elements plus the
  Errors-only toggle, populates Site chips from ISiteRepository at
  initialisation, exposes [Parameter] EventCallback<AuditLogQueryFilter>
  OnFilterChanged and an optional NowUtcProvider seam for time-window tests.

* AuditFilterBarTests — 5 bUnit tests pinning element presence, Apply
  callback payload, Channel→Kind narrowing, Errors-only toggle precedence
  and the LastHour time-window collapse.
2026-05-20 19:56:49 -04:00
Joseph Doherty
12b86bea7a feat(ui): scaffold Audit Log page + Audit nav group (#23 M7)
Adds the central-side Audit Log page scaffold at /audit/log (M7-T1) and
introduces a new Audit nav group (M7-T9) that hosts both the new page and
the renamed Configuration Audit Log. The page body is intentionally a
heading + two placeholders — Bundle B will land the AuditFilterBar and
AuditResultsGrid behind them.

The Audit nav group sits between Monitoring and the per-user footer; both
items inside are Admin-only, so the section header lives inside the
RequireAdmin AuthorizeView (non-admins see no orphan header).

bUnit scaffold tests pin the page heading, the section header order, and
the two child links; the existing 338 CentralUI tests continue to pass.
2026-05-20 19:49:11 -04:00
Joseph Doherty
562a1d1678 test(central-ui): assert the Health KPIs link is a real anchor element 2026-05-19 06:24:39 -04:00
Joseph Doherty
82745ef916 feat(central-ui): link Health outbox tiles to the Notification KPIs page 2026-05-19 06:22:02 -04:00
Joseph Doherty
0be1feb561 feat(central-ui): add the Notifications nav section 2026-05-19 06:17:58 -04:00
Joseph Doherty
8bb860ad5f test(central-ui): cover the per-site KPI error path on the Notification KPIs page 2026-05-19 06:14:19 -04:00
Joseph Doherty
22bac058dd feat(central-ui): Notification KPIs page with per-site breakdown 2026-05-19 06:09:43 -04:00
Joseph Doherty
34e464edab refactor(central-ui): split Notification Report out of the Outbox page 2026-05-19 06:03:15 -04:00
Joseph Doherty
0fa4ac5525 refactor(central-ui): contextual errors, parallel recipient load, delete-path test for Notification Lists 2026-05-19 05:54:48 -04:00
Joseph Doherty
0f90c0ad9c feat(central-ui): standalone Notification Lists page 2026-05-19 05:49:45 -04:00
Joseph Doherty
c8b5871782 fix(notification-outbox): re-align Central UI sandbox Notify API with production
The script-analysis sandbox Notify surface was stale after the Notification
Outbox change: SandboxNotifyTarget.Send returned Task<NotificationResult> and
there was no Status method, while production NotifyTarget.Send returns
Task<string> (a NotificationId) plus NotifyHelper.Status. A script that
test-ran cleanly in the sandbox would not compile against the real site
runtime.

- Move the NotificationDeliveryStatus record from ScadaLink.SiteRuntime.Scripts
  into ScadaLink.Commons.Messages.Notification so both production and the
  CentralUI sandbox reference the exact same type (CentralUI does not, and
  should not, reference SiteRuntime). Production NotifyHelper.Status is
  otherwise untouched.
- Rewrite SandboxNotifyHelper/SandboxNotifyTarget to be a signature-faithful
  no-op fake: Send returns Task<string> (a fake NotificationId), Status returns
  Task<NotificationDeliveryStatus>. Production now enqueues into the site S&F
  engine, which has no central-side equivalent in the sandbox, so the fake no
  longer carries an INotificationDeliveryService.
- Add script-analysis tests proving a script using the new Notify shape both
  diagnoses clean and runs in the sandbox.
2026-05-19 03:44:34 -04:00
Joseph Doherty
9e7bc7b541 feat(notification-outbox): add outbox KPI tiles to Health dashboard 2026-05-19 03:05:41 -04:00
Joseph Doherty
9b05e48ea6 test(notification-outbox): cover Discard and query-failure paths on the Outbox page 2026-05-19 03:02:48 -04:00
Joseph Doherty
ad9872705d feat(notification-outbox): add Notification Outbox UI page 2026-05-19 02:58:49 -04:00
Joseph Doherty
01509a045f feat(central-ui): add Min time between runs field to the script form
The template script editor had no input for MinTimeBetweenRuns, so a
WhileTrue trigger configured through the UI always saved a null interval
and degraded to a single edge fire. The Add/Edit Script modal now has a
"Min time between runs" number+unit (ms/sec/min) field.

- Visible only for ValueChange / Conditional / Expression triggers — the
  auto-firing triggers MinTimeBetweenRuns throttles. Hidden for Interval
  (its own period is the cadence), Call (invoked explicitly, never
  throttled), and None.
- For a WhileTrue Conditional/Expression trigger the field is labelled as
  the re-fire interval and shows a warning while it is blank.
- Wired through the new-script and edit-script save paths (edit previously
  only preserved the existing value, never let the user change it).

New DurationInput helper does the TimeSpan <-> number+unit conversion;
ScriptTriggerConfigCodec.SupportsMinTimeBetweenRuns classifies trigger
types. Both TDD'd — 21 new tests. CentralUI suite 316 green; verified
end-to-end in the browser (visibility per trigger type, WhileTrue warning,
save/reload round-trip).
2026-05-18 16:44:15 -04:00
Joseph Doherty
437fe154e7 feat(triggers): add WhileTrue fire mode for Conditional/Expression script triggers
Conditional and Expression script triggers gain an optional `mode` field
in their TriggerConfiguration JSON:

- OnTrue (default): unchanged edge/per-change firing. An absent mode field
  parses as OnTrue, so every existing trigger config behaves identically.
- WhileTrue: fires on the false->true edge, then re-fires on a periodic
  timer while the condition holds; stops on the true->false edge. The
  re-fire cadence is the script's MinTimeBetweenRuns; with none configured
  the trigger degrades to a single edge fire and logs a warning.

ScriptActor tracks condition truth state and manages a dedicated
"whiletrue-trigger" timer. ScriptTriggerConfigCodec and ScriptTriggerEditor
round-trip the mode and expose an OnTrue/WhileTrue selector for the two
trigger kinds. Design: docs/plans/2026-05-18-whiletrue-trigger-mode-design.md

Tests: 7 ScriptActor runtime tests (edge fire, timer re-fire, stop,
re-arm, no-MinTimeBetweenRuns degrade, OnTrue regressions) + 14 codec /
editor tests. SiteRuntime suite 206 green, CentralUI suite 295 green.
2026-05-18 10:44:11 -04:00
Joseph Doherty
cfa8667c78 test(central-ui): fix test-host hang in CentralUI.Tests
DiffDialogTests.SetupBodyLockInterop registered bUnit SetupVoid planned
invocations that were never completed; DisposeAsync_WhileOpen awaited
DiffDialog.DisposeAsync -> TryUnlockBodyAsync -> InvokeVoidAsync on one of
them, suspending the test forever so the test host never exited (regression
from the CentralUI-023 catch-narrowing). SetupBodyLockInterop now uses Loose
JSInterop mode. Also dispose the leaked WebApplication instances in the Auth
tests (FileSystemWatcher + ConsoleLoggerProcessor threads) and the extra
ServiceProvider in the DebugView tests. Suite now runs 281 tests in ~7s and
exits cleanly.
2026-05-17 05:43:05 -04:00
Joseph Doherty
e55bd46ca1 fix(health-monitoring): resolve HealthMonitoring-015 — nullable LastReportReceivedAt
A heartbeat-registered site that has never sent a full report now has
LastReportReceivedAt = null instead of the year-0001 sentinel. TimestampDisplay
accepts DateTimeOffset? and renders null as a placeholder ('awaiting first
report') rather than a ~2000-year-stale date. Cross-module: HealthMonitoring +
CentralUI.
2026-05-17 05:43:05 -04:00
Joseph Doherty
d7d74ebe5e fix(central-ui): resolve CentralUI-020..025 — auth-ping idle logout, DebugView race, push-handler disposal guard, JS-interop catch narrowing, claim-constant helper, SessionExpiry tests 2026-05-17 03:18:16 -04:00
Joseph Doherty
34588ae10c fix(central-ui): resolve CentralUI-006 — push-based deployment status via IDeploymentStatusNotifier, remove 10s polling timer 2026-05-17 00:02:45 -04:00
Joseph Doherty
1e2e7d2e7c fix(central-ui): resolve CentralUI-005 — sliding cookie session expiry (Security AddCookie + AuthEndpoints + SessionExpiry) 2026-05-16 23:54:31 -04:00
Joseph Doherty
d7b275fc9b fix(central-ui): resolve CentralUI-015..019 — pager windowing, logout CSRF, narrowed catch blocks, coverage; CentralUI-015 re-triaged Won't Fix 2026-05-16 22:04:21 -04:00
Joseph Doherty
71b90ba499 fix(central-ui): resolve CentralUI-007..014 — nav authz, UTC date filters, disposal guards, N+1 fix, async script analysis 2026-05-16 20:58:03 -04:00
Joseph Doherty
87f14c190a fix(central-ui): resolve CentralUI-002/003/004 — site-scope enforcement, per-circuit console capture, cached auth state 2026-05-16 19:33:09 -04:00
Joseph Doherty
a9bd7ee37c fix(central-ui): resolve CentralUI-001 — enforce script trust model before sandbox execution
ScriptAnalysisService.RunInSandboxAsync compiled and executed arbitrary
user C# in the central host process with no trust-model enforcement — the
forbidden-API set was only a Monaco editor diagnostic. A Design-role user
could run System.IO/Process/Reflection/network code on the central node.

Added a Roslyn semantic gate (EnforceTrustModel) invoked after compilation
and before script.RunAsync, and on nested shared scripts in callSharedFunc;
a script referencing any forbidden API is rejected before it runs.

Reworked FindForbiddenApiUsages: it now resolves every identifier against
the semantic model and checks types and members, so a fully-qualified call
(System.IO.File.WriteAllText) is caught — the pre-fix check only inspected
the leftmost identifier and missed that shape. This is a static semantic
gate, not a process sandbox.

Adds gate regression tests that fail against the pre-fix code, plus a
clean-script test guarding against over-blocking.
2026-05-16 18:41:12 -04:00
Joseph Doherty
9c60592632 build: adopt NuGet Central Package Management
Move all package versions into Directory.Packages.props so every project
resolves a single consistent version. Consolidates the Roslyn packages
(Microsoft.CodeAnalysis.CSharp.Scripting/Workspaces) onto 5.0.0, which
resolves the pre-existing NU1608 version-skew error in the test projects.
2026-05-16 15:56:30 -04:00
Joseph Doherty
fd1518f4f4 test(central-ui): remove vacuous tests for removed analyzer diagnostics
Six tests asserted DoesNotContain(SCADA004/SCADA005) or an empty InlayHints
result — all pass for the wrong reason now that those diagnostics and the
positional InlayHints were removed in the analyzer realignment. They also
used the obsolete top-level CallScript syntax. Removed.
2026-05-16 15:06:30 -04:00
Joseph Doherty
b949dc4183 test(central-ui): realign analyzer tests with the reworked script-call API 2026-05-16 15:04:06 -04:00
Joseph Doherty
3cc174c3cd test(central-ui): fix the CentralUI.Tests build
Two stale references blocked compilation: the DataConnection page tests
still pointed at Components.Pages.Admin (the pages moved to .Design), and
ScriptAnalysisServiceTests constructed ScriptAnalysisService without the
IServiceProvider parameter. The project now compiles.
2026-05-16 14:44:30 -04:00
Joseph Doherty
751248feb6 feat(alarms): HiLo trigger type with per-band level, hysteresis, messages, overrides
Adds a new HiLo alarm trigger type with four configurable setpoints
(LoLo / Lo / Hi / HiHi). Each setpoint carries an optional priority,
deadband (for hysteresis), and operator message. The site runtime emits
AlarmStateChanged with an AlarmLevel field so consumers can differentiate
warning vs critical bands.

Plumbing:
  - new AlarmLevel enum + AlarmStateChanged.Level/Message init properties
  - AlarmTriggerEditor (Blazor) gets a HiLo render with severity tinting
  - AlarmTriggerConfigCodec extracted from the editor for testability
  - sitestream.proto carries level + message over gRPC
  - SemanticValidator enforces numeric attribute, setpoint ordering,
    non-negative deadband
  - on-trigger scripts get an Alarm global (Name/Level/Priority/Message)
    so notification routing can branch by severity
  - per-instance InstanceAlarmOverride entity + EF migration + flattening
    step + CLI commands; HiLo overrides merge setpoint-by-setpoint, binary
    types whole-replace
  - DebugView shows a Level badge + per-band message tooltip
  - App.razor auto-reloads on permanent Blazor circuit failure
  - docker/regen-proto.sh automates the proto regen workflow (the linux/arm64
    protoc segfault means generated files are checked in for now)
2026-05-13 03:23:32 -04:00
Joseph Doherty
783da8e21a feat(ui): structured editors for script schemas and alarm triggers
Replace raw-JSON text inputs with rich UI: script parameter/return types use
a JSON Schema builder (SchemaBuilder + JsonSchemaShapeParser, with a migration
to convert existing definitions); alarm trigger config uses a type-aware
editor with a flattened attribute picker (AlarmTriggerEditor). AlarmActor
gains optional direction (rising/falling/either) on RateOfChange triggers.
2026-05-13 00:33:00 -04:00
Joseph Doherty
0b24b4537d feat(ui/scripts): editor support for self/child/parent accessors
Phases 3+4 of the script-scope rollout. Wires the runtime accessors
landed in efba01d through to Monaco completion, diagnostics, and
hover.

New analyzer surface in ScriptAnalysisService:

  String-literal completion contexts (added to TryStringLiteralCompletions):
    Attributes["..."]                       -> SelfAttributes
    Children["..."]                         -> composition names
    Children["X"].Attributes["..."]         -> child template's attributes
    Children["X"].CallScript("...")         -> child template's scripts
    Parent.Attributes["..."]                -> parent template's attributes
    Parent.CallScript("...")                -> parent template's scripts

  Diagnostics:
    SCADA006   Attribute "Typo" is not declared on {this template,
               child composition 'X', the parent}.  (warning)
    SCADA007   Composition "Unknown" is not declared on this template.
               (warning)

  CallShared / CallScript snippet-expansion now routes through the
  child / parent shape catalogs when invoked on Children["X"] /
  Parent — picking a child script accepts `Sample", ${1:count})`.

Contract additions:
  - AttributeShape (Name, Type) record
  - CompositionContext (Name, Attributes, Scripts) record
  - SelfAttributes / Children / Parent fields on DiagnoseRequest,
    CompletionsRequest, HoverRequest, SignatureHelpRequest

ScriptHost (analyzer-side globals) gains stub AttributeBag /
ChildrenBag / CompositionBag types so Roslyn doesn't emit CS0103 on
Attributes / Children / Parent. The stubs are never invoked — only
their signatures are read by the analyzer's compilation pass.

MonacoEditor.razor exposes SelfAttributes / Children / Parent
parameters; GetContext returns them; monaco-init.js forwards all
three on completion / hover / signature-help / diagnostics requests.

TemplateEdit fetches each composition's resolved child template
shape via GetTemplateWithChildrenAsync, and queries GetAllTemplatesAsync
for any single parent that composes the open template. Multi-parent
or no-parent → Parent is suppressed.

11 new xUnit tests on the new completion / diagnostic paths. Total:
149 -> 159.

Browser-verified via curl:
  - Children["..."] suggests composition names
  - Attributes["..."] suggests attributes with type detail
  - Attributes["Typo"] squiggles SCADA006
  - Children["Unknown"] squiggles SCADA007
  - No spurious CS0103 on the new accessors

Hover, signature help, and inlay hints for the new accessors keep
working because they reuse the same dispatch logic.
2026-05-12 05:53:13 -04:00