Commit Graph

596 Commits

Author SHA1 Message Date
Joseph Doherty
624cf255a4 feat(transport): wire full SemanticValidator at bundle import time 2026-05-24 06:32:42 -04:00
Joseph Doherty
8e73e60f4a feat(transport): restore composition + alarm-script edges on bundle import 2026-05-24 06:16:24 -04:00
Joseph Doherty
cef77e1378 fix(transport): carry TemplateAlarm.OnTriggerScript by name in bundle DTO 2026-05-24 06:10:59 -04:00
Joseph Doherty
79d74ee59c fix(centralui): hint that notification list export does not include SMTP config
Add an unconditional alert-info banner in the Notification Lists fieldset
(Step 1) explaining that SMTP configurations are not auto-included as
dependencies and must be selected separately.
2026-05-24 06:05:53 -04:00
Joseph Doherty
e6706c26e6 fix(transport): preserve MinTimeBetweenRuns + ExternalSystem retry fields in bundle DTOs
Add TimeSpan? MinTimeBetweenRuns to TemplateScriptDto and int MaxRetries /
TimeSpan RetryDelay to ExternalSystemDto; wire both directions in
EntitySerializer. Extends the existing script round-trip assertion and adds
Roundtrip_external_system_preserves_retry_config.
2026-05-24 06:05:26 -04:00
Joseph Doherty
a2b8b69281 fix(transport): NavMenu Admin-only visibility + BundleImportUnlockFailed audit + docker appsettings
- NavMenu: move Import Bundle out of the nested RequireDesign/RequireAdmin
  double-gate into the top-level Admin section so an Admin-only user sees it
  without needing the Design role; Export Bundle stays in the Design section.
- TransportImport: inject IAuditService + ScadaLinkDbContext; emit a
  BundleImportUnlockFailed audit row (best-effort, swallowed on failure) on
  every wrong-passphrase attempt in SubmitPassphraseAsync, with attempt
  number and error reason in afterState.
- docker central-node-a/b appsettings: add ScadaLink:Transport section with
  SourceEnvironment = "docker-cluster" so the importer picks up a non-null
  environment name in the audit trail.
- CentralUI.Tests: register IAuditService mock + SQLite in-memory
  ScadaLinkDbContext in TransportImportPageTests to satisfy the two new injects.
2026-05-24 05:59:04 -04:00
Joseph Doherty
ef025a325d feat(centralui): Bundle Import filter on ConfigurationAuditLog page 2026-05-24 05:44:21 -04:00
Joseph Doherty
39f994f9bc feat(centralui): add Export/Import Bundle nav entries 2026-05-24 05:38:48 -04:00
Joseph Doherty
acadb83712 feat(centralui): TransportImport wizard under Design nav group 2026-05-24 05:38:09 -04:00
Joseph Doherty
0dbc0c02f9 feat(centralui): TransportExport wizard under Design nav group
Implements Task T21 of the Transport feature. A four-step Blazor wizard
(Select → Review → Encrypt → Download) under /design/transport/export,
gated on AuthorizationPolicies.RequireDesign:

  1. Select  — TemplateFolderTree (checkbox-mode) plus flat checkbox
               lists for shared scripts, external systems, DB connections,
               notification lists, SMTP configs, API keys, API methods.
  2. Review  — runs DependencyResolver, surfaces seed vs auto-included.
               "Include all dependencies" toggle re-resolves on flip.
  3. Encrypt — passphrase + confirm with strength meter, secret-count
               warning over the resolved closure, explicit unencrypted
               opt-out path (calls BundleExporter with passphrase=null
               so the audit row tags UnencryptedBundleExport).
  4. Download— calls IBundleExporter.ExportAsync, streams bytes to the
               browser via JS interop (wwwroot/js/transport.js), displays
               filename + size + SHA-256 + encryption status.

Source environment is sourced from new TransportOptions.SourceEnvironment
(bound from ScadaLink:Transport:SourceEnvironment, defaults "scadalink"),
filename pattern scadabundle-{env}-{yyyy-MM-dd-HHmmss}.scadabundle.

Tests (bUnit + policy): step 1 group rendering, step 2 dependency
expansion (Pump composes Motor), step 4 full walkthrough verifying
ExportAsync receives the selected ids + authenticated identity, and a
RequireDesign policy-deny test for users without the Design role. Also
unit-pins the filename-sanitisation contract.
2026-05-24 05:30:16 -04:00
Joseph Doherty
01f4eeaef5 refactor(centralui): extract TemplateFolderTree as shared component 2026-05-24 05:18:12 -04:00
Joseph Doherty
e099ed2038 feat(centralui): TreeView checkbox-selection mode with tri-state 2026-05-24 05:13:04 -04:00
Joseph Doherty
9a3f5231db feat(transport): register AddTransport() on central nodes 2026-05-24 05:09:51 -04:00
Joseph Doherty
cda80cf821 fix(transport): robust failure-audit when rollback throws + doc clarifications
Address one Blocker and three Important findings from code review of
2c34f12 (BundleImporter.ApplyAsync):

- BLOCKER: wrap RollbackAsync in nested try/catch so a rollback fault
  does not swallow the BundleImportFailed audit row. Dispose the
  failed transaction before the audit-write so the new SaveChangesAsync
  uses a fresh implicit transaction instead of enlisting in the broken
  one. Surface the rollback exception's message on the failure row
  alongside the original cause, and swallow audit-write faults per the
  design's best-effort-audit invariant. Add regression integration
  test using a SQLite transaction interceptor that throws on rollback.

- Document re-entrancy assumption on IAuditCorrelationContext: scoped
  lifetime, single circuit, concurrent imports within a shared scope
  must serialize externally.

- Document repository audit responsibility on BundleImporter: repos
  are thin EF wrappers; ApplyAsync writes audit rows explicitly. If
  repos ever start emitting audit rows, the explicit calls here must
  be removed to avoid double-logging.

- Document BundleSessionStore thread-safety: ConcurrentDictionary
  primitives are safe under concurrent callers; BundleSession itself
  is not thread-safe.
2026-05-24 05:06:04 -04:00
Joseph Doherty
2c34f12a6f feat(transport): BundleImporter.ApplyAsync transactional with audit correlation 2026-05-24 04:55:43 -04:00
Joseph Doherty
2400249453 feat(transport): BundleImporter.PreviewAsync diff engine 2026-05-24 04:41:24 -04:00
Joseph Doherty
5fc6790c36 feat(transport): BundleImporter.LoadAsync with manifest validation 2026-05-24 04:37:02 -04:00
Joseph Doherty
7c70ce0dbf feat(transport): BundleExporter with audit logging 2026-05-24 04:30:18 -04:00
Joseph Doherty
901d9affdf feat(transport): in-memory BundleSessionStore with TTL + lockout 2026-05-24 04:20:55 -04:00
Joseph Doherty
06c2b20178 feat(transport): DependencyResolver with topological closure 2026-05-24 04:19:23 -04:00
Joseph Doherty
550ab0e034 feat(transport): BundleSerializer ZIP packer/reader 2026-05-24 04:11:11 -04:00
Joseph Doherty
ee76b84b0f feat(transport): bundle entity DTOs + secret carving in EntitySerializer 2026-05-24 04:08:43 -04:00
Joseph Doherty
447bf84b13 feat(transport): ManifestBuilder + ManifestValidator with schema-version gating 2026-05-24 04:04:58 -04:00
Joseph Doherty
dc669a119b feat(transport): AES-256-GCM + PBKDF2 BundleSecretEncryptor 2026-05-24 04:03:44 -04:00
Joseph Doherty
c5bd5418ad feat(transport): add TransportOptions 2026-05-24 03:58:07 -04:00
Joseph Doherty
7e51274812 feat(transport): scaffold ScadaLink.Transport project + test projects 2026-05-24 03:57:07 -04:00
Joseph Doherty
f32b59a557 feat(transport): AuditService stamps BundleImportId from correlation context 2026-05-24 03:55:17 -04:00
Joseph Doherty
233e0f996e feat(transport): EF migration AddBundleImportIdToAuditLog 2026-05-24 03:50:30 -04:00
Joseph Doherty
33f7b3979d feat(transport): add IAuditCorrelationContext scoped service 2026-05-24 03:49:26 -04:00
Joseph Doherty
ee10eba04c feat(transport): add BundleImportId column on AuditLogEntry 2026-05-24 03:48:22 -04:00
Joseph Doherty
9442c9a92c feat(transport): add IBundleExporter / IBundleImporter interfaces 2026-05-24 03:47:27 -04:00
Joseph Doherty
7e89f2092f feat(transport): add bundle manifest DTOs in Commons 2026-05-24 03:46:09 -04:00
Joseph Doherty
d630e2646b feat(ui): show SourceNode under SourceSiteId in audit log detail popup
The audit log drilldown drawer (and the execution-tree node-detail modal,
which shares this component) now renders the SourceNode field directly
under SourceSiteId so provenance reads 'site → node → instance → script'
in declared order. Two focused tests pin the field's presence in both
populated and null cases plus the inter-field ordering.
2026-05-23 19:01:48 -04:00
Joseph Doherty
f973f49254 fix(ui): remove LDAP credentials tagline from Login page 2026-05-23 18:56:24 -04:00
Joseph Doherty
c754666a3d fix(ui): carry SourceNode on SiteCallDetail + NotificationDetail records
The Site Calls and Notifications detail modals were reading SourceNode from
the summary record (d.SourceNode) while every other field read from the
detail record (det.X). The pattern works today because the modal always
opens via a row click that pre-loads the summary, but a future drill-in
from a deep link or refresh path could leave the summary stale or null and
the field would render blank or wrong.

Add SourceNode to both detail records, project it through the actor's
ToDetail mapping, and switch the razor markup to read det.SourceNode. Now
the modal binds uniformly to the detail record across all fields.
2026-05-23 18:37:53 -04:00
Joseph Doherty
d18a6e6fa0 feat(ui): add Node column + filter to SiteCalls grid 2026-05-23 18:08:25 -04:00
Joseph Doherty
b9c017136d feat(ui): add Node column + filter to NotificationOutbox grid 2026-05-23 18:04:59 -04:00
Joseph Doherty
bb29d65a94 feat(ui): add Node column + filter to AuditLog grid 2026-05-23 18:01:36 -04:00
Joseph Doherty
466e1454fe test(sitecall-audit): symmetric SourceNode coverage on DbOutbound emitter + clarify DI comments
Two follow-ups from the T13/T14 code review:

- M1: Add CachedWrite_StampsSourceNode_OnSubmitTelemetryRow and
  CachedWrite_NoSourceNodeWired_LeavesSourceNodeNull to DatabaseCachedWriteEmissionTests,
  mirroring the existing ApiOutbound SourceNode tests in
  ExternalSystemCachedCallEmissionTests. Site-emitter coverage now symmetric
  across both cached-call channels.
- M2: Clarify the GetService(INodeIdentityProvider) DI comments on the
  CachedCallTelemetryForwarder and CachedCallLifecycleBridge factories:
  it's test composition roots that may not register the provider, not
  central production. Both site and central hosts always register it via
  SiteServiceRegistration.BindSharedOptions.
2026-05-23 17:50:14 -04:00
Joseph Doherty
06ed0acead feat(sitecall-audit): carry + persist SourceNode end-to-end via cached telemetry
Site: site emitters of SiteCallOperational (ExternalSystemClient, the script-API
cached call path in ScriptRuntimeContext, CachedCallLifecycleBridge) inject
INodeIdentityProvider and stamp SourceNode = NodeName at construction.

OperationTrackingStore call site in CachedCallTelemetryForwarder now stamps
SourceNode too.

Central: SiteCallAuditRepository.UpsertAsync INSERT includes SourceNode in the
column list; conditional monotonic UPDATE uses
COALESCE(@SourceNode, SourceNode) so later packets cannot blank a previously-
stamped value. After this commit every SiteCalls row carries node-a/node-b in
SourceNode (subject to monotonic preservation).
2026-05-23 17:41:22 -04:00
Joseph Doherty
d1fcab490c feat(notif-outbox): carry + persist SourceNode end-to-end via NotificationSubmit
Site: inject INodeIdentityProvider where NotificationSubmit is built; stamp
SourceNode = NodeName at construction.

Central: NotificationOutboxActor.HandleSubmit copies submit.SourceNode onto
the Notification row; the repository INSERT persists it (EF tracked-entity
insert flows it through automatically; raw-SQL extension if not).

After this commit, every Notifications row carries the originating site
node-a/node-b in SourceNode. Existing notifications submitted pre-feature
remain NULL.
2026-05-23 17:28:23 -04:00
Joseph Doherty
e6341580b3 test(audit): lock null-provider passthrough on CentralAuditWriter
Two follow-ups flagged by code review on Tasks 11/12:

- Lock the back-compat contract for CentralAuditWriter's optional
  `nodeIdentity = null` ctor parameter with two explicit tests
  (`WriteAsync_PassesThroughCallerSourceNode_WhenNoProviderInjected` and
  `WriteAsync_LeavesSourceNodeNull_WhenNoProviderInjected`). The previous
  null-provider path was only exercised incidentally via legacy
  CentralAuditWriterTests setups; the new tests make the contract explicit
  and distinct from the "provider supplied, returns null" path.

- Document why the catch-block log references `evt` rather than the
  post-stamp record: the three logged fields (EventId, Kind, Status) are
  immutable across the filter+stamp chain, so referencing either name is
  equivalent — but the comment warns future maintainers to switch names if
  they ever add a field the chain mutates (e.g. SourceNode).
2026-05-23 17:18:45 -04:00
Joseph Doherty
974a36826a feat(audit): stamp SourceNode at CentralAuditWriter + persist via AuditLogRepository
CentralAuditWriter injects INodeIdentityProvider and stamps the event before
handing to the repository. AuditLogRepository.InsertIfNotExistsAsync now
includes SourceNode in the INSERT column list. Caller-provided value wins
(supports any future direct-write callsite that already has its own node id).
2026-05-23 17:11:23 -04:00
Joseph Doherty
479870e40c feat(audit): stamp SourceNode at site SqliteAuditWriter from INodeIdentityProvider
Caller-provided SourceNode wins (preserves reconciled rows from other nodes);
otherwise the writer fills it from the local INodeIdentityProvider.NodeName.
Reads from the provider on every write — singleton lifetime means zero overhead.
2026-05-23 17:08:21 -04:00
Joseph Doherty
277882d230 feat(site-runtime): add SourceNode column to OperationTracking + thread through RecordEnqueueAsync 2026-05-23 16:54:48 -04:00
Joseph Doherty
f3cb8c0791 feat(audit): add SourceNode column to site SQLite AuditLog (idempotent upgrade) 2026-05-23 16:50:16 -04:00
Joseph Doherty
8fb9eb0ce7 chore(db): align SourceNode unicode metadata + document partition-aligned index recipe
Tidies flagged by code review on the T6/T7/T8 migration bundle:

- Add `.IsUnicode(false)` to the three SourceNode EF property mappings to
  match every other ASCII varchar column on the same entities. Physical
  column was already `varchar(64)` because `HasColumnType` wins, but the EF
  model metadata flag was inconsistent.
- Add `unicode: false` to the three AddColumn<string> calls in the migrations
  + their Designer snapshots so the historical snapshots match the model.
- Update the model snapshot to carry IsUnicode(false) on each SourceNode entry.
- Document the SELECT-list invariant on SiteCallAuditRepository.QueryAsync:
  EF Core's FromSqlInterpolated requires every entity-tracked column in the
  result set, so future SiteCall columns must extend the list too.
- Amend plan Task 6 Step 2 to document the partition-aligned raw-SQL index
  recipe and the staging-table sync requirement.
2026-05-23 16:45:19 -04:00
Joseph Doherty
1a77bc5f38 feat(db): add SourceNode column to SiteCalls 2026-05-23 16:34:30 -04:00
Joseph Doherty
16b685b96b feat(db): add SourceNode column to Notifications 2026-05-23 16:34:30 -04:00
Joseph Doherty
552d7832a3 feat(db): add SourceNode column + IX_AuditLog_Node_Occurred index to AuditLog 2026-05-23 16:34:28 -04:00