Commit Graph

221 Commits

Author SHA1 Message Date
Joseph Doherty
b8dece0e70 feat(notification-outbox): add Email notification delivery adapter 2026-05-19 01:26:33 -04:00
Joseph Doherty
8d52890245 feat(notification-outbox): add NotificationOutboxOptions and delivery adapter abstraction 2026-05-19 01:20:49 -04:00
Joseph Doherty
fb589bf1da feat(notification-outbox): scaffold ScadaLink.NotificationOutbox project 2026-05-19 01:16:58 -04:00
Joseph Doherty
c547f82957 feat(notification-outbox): add notification message and outbox query contracts 2026-05-19 01:13:36 -04:00
Joseph Doherty
2c59d59b61 feat(notification-outbox): add NotificationOutbox repository 2026-05-19 01:02:06 -04:00
Joseph Doherty
761595309b feat(notification-outbox): add Notification EF configuration and DbSet 2026-05-19 00:55:58 -04:00
Joseph Doherty
87ac9b8a4d feat(notification-outbox): add Type field to NotificationList 2026-05-19 00:52:23 -04:00
Joseph Doherty
ed7fddb0b5 test(notification-outbox): cover all Notification constructor null guards 2026-05-19 00:50:52 -04:00
Joseph Doherty
397a62677f feat(notification-outbox): add Notification entity 2026-05-19 00:48:48 -04:00
Joseph Doherty
926ca902bd test(notification-outbox): fold notification enum tests into EnumTests convention 2026-05-19 00:47:23 -04:00
Joseph Doherty
f9b942bb94 feat(notification-outbox): add NotificationType and NotificationStatus enums 2026-05-19 00:45:05 -04:00
Joseph Doherty
06462a0100 feat(template-engine): contained names for composition-derived templates
A composition-derived template now stores its contained name — the
composition slot's InstanceName (e.g. "Pump"), unique only within its
owner — instead of the dotted global path ("Motor Controller.Pump").
The qualified hierarchical name is computed on read.

- TemplateNaming.QualifiedName: walks the OwnerCompositionId chain to
  build the dotted path; null-safe, cycle-guarded.
- TemplateConfiguration: the unique index on Template.Name becomes
  filtered (WHERE IsDerived = 0) — base templates stay globally unique;
  derived templates' uniqueness is the existing (TemplateId,
  InstanceName) index on TemplateComposition.
- Migration ContainedDerivedTemplateNames: rewrites derived rows to the
  contained name; Down rebuilds the dotted names via a recursive CTE
  before restoring the global index.
- TemplateService: composition create/rename store the contained name;
  the dotted-name collision pre-checks and cascade-rename are removed
  (a slot rename no longer touches nested derived templates).
- TemplateEdit: title shows the contained name; the qualified path is a
  breadcrumb subtitle; "composed inside" uses the owner's qualified name.

TDD: 4 TemplateNaming tests + updated composition tests. TemplateEngine
293, ConfigurationDatabase 114, CentralUI 316 green. Migration applied to
the dev cluster and verified in the browser (Motor Controller.Pump now
titled "Pump"; nested Motor Controller.Pump.TempSensor resolves).

Design: docs/plans/2026-05-18-contained-template-names-design.md
2026-05-18 17:50:30 -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
6139a65a7b fix(site-runtime): fan tag updates out to every attribute sharing a tag path
InstanceActor._tagPathToAttribute was a Dictionary<string,string> — one tag
path mapped to a single attribute. When two attributes reference the same PLC
node (e.g. two composed cooling-tank modules both reading ns=3;s=Tank.Level,
or a pump's TempSensor and AlarmSensor both reading ns=3;s=Sensor.Reading),
SubscribeToDcl's map assignment overwrote, so only the last-registered
attribute ever received values — the rest stayed permanently Uncertain.

The map is now Dictionary<string,List<string>>; HandleTagValueUpdate fans each
update out to every attribute referencing the tag path, and each distinct tag
path is still subscribed only once per connection.
2026-05-18 04:21:26 -04:00
Joseph Doherty
fa7b12c4a3 test(playwright): align E2E nav tests with current NavMenu
The role-navigation and navigation E2E tests asserted on a stale nav model —
labels 'Data Connections', 'Instances', 'Areas' that NavMenu.razor no longer
uses, 'Connections' mapped to /admin instead of /design, and Event Logs /
Parked Messages treated as all-roles when they are Deployment-role gated.
SitesPage_ShowsTable expected an HTML <table> but Sites.razor renders site
cards. Corrected the expectations to the actual NavMenu/Sites markup; the
role-based authorization itself was already correct. Suite: 43/43.
2026-05-18 02:42:44 -04:00
Joseph Doherty
1038683c58 test(integration): repair IntegrationTests harness and stale API-key test
- ScadaLinkWebApplicationFactory removed the AkkaHostedService SINGLETON, not
  just its IHostedService registration, so IClusterNodeProvider's factory
  (Program.cs) could not resolve it — 10 tests failed at host build. Now removes
  only the factory-registered IHostedService descriptors and keeps the singleton.
- Configure an LDAP service account so ResolveUserDnAsync does search-then-bind
  against GLAuth (whose DN layout the no-service-account fallback DN never
  matched), fixing LoginEndpoint_WithValidLdapCredentials.
- IntegrationSurfaceTests: ApiKeyValidator now matches keys by HMAC hash over
  GetAllApiKeysAsync (ConfigurationDatabase-012); the test mocked the removed
  GetApiKeyByValueAsync path. Suite now 64/64.
2026-05-17 06:46:47 -04:00
Joseph Doherty
adf73ab116 test(host): configure ApiKeyPepper in CentralCompositionRootTests
ConfigurationDatabase-012 made ApiKeyHasher fail fast on a missing/weak HMAC
pepper, so resolving ApiKeyValidator from the central composition root now
requires ScadaLink:InboundApi:ApiKeyPepper to be configured. The composition-
root test's in-memory config now supplies a test pepper, like JwtSigningKey.
2026-05-17 06:36:04 -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
7da303d7bb fix(configuration-database): resolve ConfigurationDatabase-012 — store inbound-API keys as HMAC-SHA256 hashes
Inbound-API bearer credentials are no longer persisted in plaintext. ApiKey now
holds a KeyHash (peppered HMAC-SHA256); the key is shown once at creation and
only its hash is stored. Lookup and validation hash the presented candidate.
Cross-module: Commons (ApiKey, ApiKeyHasher), ConfigurationDatabase (mapping +
HashApiKeyValue migration), InboundAPI (ApiKeyValidator), ManagementService
(key creation), CentralUI (ApiKeys.razor). Existing keys must be re-issued.
2026-05-17 05:42:52 -04:00
Joseph Doherty
d6221419c6 fix(template-engine): resolve TemplateEngine-015,016 — cascade-rename nested derived templates, correct composed-script ParentPath 2026-05-17 03:18:41 -04:00
Joseph Doherty
0135a6b2a6 fix(store-and-forward): resolve StoreAndForward-015..017 — document maxRetries=0 contract, replicate operator retry/discard, real category in activity log 2026-05-17 03:18:41 -04:00
Joseph Doherty
be274212f0 fix(site-runtime): resolve SiteRuntime-017..019 — isolated attribute snapshot for child actors, corrected dispatcher doc, remove dead lifecycle handlers 2026-05-17 03:18:41 -04:00
Joseph Doherty
6d63fef934 fix(site-event-logging): resolve SiteEventLogging-012..014 — fault dropped-event tasks, escape LIKE wildcards, re-triage startup-purge finding (Won't Fix) 2026-05-17 03:18:41 -04:00
Joseph Doherty
a58cec5776 fix(security): resolve Security-012..015 — fail login on partial LDAP outage, escape-aware DN parsing, idle check on refresh, username normalization 2026-05-17 03:18:33 -04:00
Joseph Doherty
f5199e9da9 fix(notification-service): resolve NotificationService-014..018 — classify OAuth2 failures, fail on bad auth config, wire NotificationOptions fallback, disposable concurrency limiter 2026-05-17 03:18:33 -04:00
Joseph Doherty
bf6bd8de5a fix(management-service): resolve ManagementService-014..017 — site-scope enforcement on QueryDeployments, atomic override validation, curated fault messages, test coverage 2026-05-17 03:18:33 -04:00
Joseph Doherty
73a393076a fix(inbound-api): resolve InboundAPI-014..017 — return-value validation, reflection-gateway hardening, deadline-bound routed calls, RouteHelper test coverage 2026-05-17 03:18:33 -04:00
Joseph Doherty
aca65e85bb fix(host): resolve Host-012..015 — consume DownIfAlone in HOCON, sub-second timing precision, config-driven Serilog sinks, transient-only startup retry 2026-05-17 03:18:33 -04:00
Joseph Doherty
eae4077414 fix(health-monitoring): resolve HealthMonitoring-013,014,016 — shorter-timeout cadence, options validation, injected TimeProvider; HealthMonitoring-015 left open (cross-module design decision) 2026-05-17 03:18:24 -04:00
Joseph Doherty
da8c9f171b fix(external-system-gateway): resolve ExternalSystemGateway-015..017 — treat MaxRetries=0 as unset, scope HTTP connection cap to gateway clients, no bare trailing '?' 2026-05-17 03:18:24 -04:00
Joseph Doherty
4fa6f0e774 fix(deployment-manager): resolve DeploymentManager-015..017 — reconciliation applies post-success side effects, updates RevisionHash, corrected XML doc 2026-05-17 03:18:24 -04:00
Joseph Doherty
14ba5495d1 fix(data-connection-layer): resolve DataConnectionLayer-014..017 — real logger for OPC UA client, initial-connect failover, accurate subscribe response, per-tag write-batch results 2026-05-17 03:18:24 -04:00
Joseph Doherty
3d3f43229f fix(configuration-database): resolve ConfigurationDatabase-013,014 — fail-fast on missing key ring, single converter local; ConfigurationDatabase-012 left open (cross-module design decision) 2026-05-17 03:18:24 -04:00
Joseph Doherty
a768135237 fix(communication): resolve Communication-012..015 — endpoint-aware gRPC client cache, address-change recreation, correlation-id validation, node-flip tests 2026-05-17 03:18:17 -04:00
Joseph Doherty
a78c3bcb6f fix(commons): resolve Commons-013,014 — integral JSON index handling, distinguish Malformed vs Legacy OPC UA config 2026-05-17 03:18:17 -04:00
Joseph Doherty
21856a4be7 fix(cluster-infrastructure): resolve ClusterInfrastructure-009,010 — DownIfAlone consumption (via Host-012), validator enforces DownIfAlone=true 2026-05-17 03:18:17 -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
f82bcbed7c fix(cli): resolve CLI-014..016 — re-triage update-command contract, doc-surface drift, table-column union 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
a55502254e fix(external-system-gateway): resolve ExternalSystemGateway-011 — name-keyed repository lookups replace fetch-all-then-filter on the call hot path 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
b1f4251d75 fix(commons): resolve Commons-008 — replace ValueTuple in SetConnectionBindingsCommand with named ConnectionBinding record (CLI, ManagementService, TemplateEngine, CentralUI) 2026-05-16 23:54:31 -04:00
Joseph Doherty
adb5e75ec3 fix(template-engine): resolve TemplateEngine-011,013,014 — remove dead converter, duplicate-id-safe cycle detection, unified deletion logic; TemplateEngine-012 deferred 2026-05-16 22:32:30 -04:00
Joseph Doherty
9e2416b34c fix(store-and-forward): resolve StoreAndForward-006,007,008,009 — transactional parked reads, PipeTo, fault-isolated activity events; 002/011/012 deferred 2026-05-16 22:32:30 -04:00
Joseph Doherty
dd7626da63 fix(site-runtime): resolve SiteRuntime-012,013,015,016 — doc accuracy, shared LoggerFactory, execution-actor coverage; SiteRuntime-014 deferred 2026-05-16 22:32:30 -04:00
Joseph Doherty
b1ea78a9fd fix(site-event-logging): resolve SiteEventLogging-006,009,011 — severity index, accurate XML doc, dead-placeholder removal 2026-05-16 22:32:30 -04:00
Joseph Doherty
84a696b0e4 fix(security): resolve Security-009,010,011 — LDAP connection timeout, design-doc correction, security-path test coverage; Security-008 deferred 2026-05-16 22:24:03 -04:00
Joseph Doherty
a9bd017c88 fix(notification-service): resolve NotificationService-010,011,012 — disconnect SMTP on failure, relocate exception type, OAuth2/token-cache test coverage 2026-05-16 22:24:03 -04:00