Commit Graph

490 Commits

Author SHA1 Message Date
Joseph Doherty 487859bff0 docs+code: close Theme 1 — 24 design-doc / XML-doc drift findings
Doc/XML-comment drift + small adherence fixes across 17 modules. Highlights:
- Host-017: site CoordinatedShutdown ordering — SiteStreamGrpcServer gains
  CancelAllStreams() (refuse new streams, cancel active), wired into
  Program.cs site branch via ApplicationStopping.
- InboundAPI-021: ParentExecutionId now travels on RouteToGet/SetAttributes
  symmetric with RouteToCallRequest; RouteHelper stamps from _parentExecutionId.
- ClusterInfra-012: ClusterOptionsValidator now requires both seed nodes.
- Comm-018: SiteCommunicationActor.HeartbeatMessage.IsActive derived from
  cluster leader check (was hardcoded true).
- DM-020: reconciliation audit row attributes the current user, not prior deployer.
- SEL-019: EventLogPurgeService early-exits on standby via active-node check.
- Plus comment/XML-doc accuracy fixes across AuditLog, ConfigurationDatabase,
  NotificationOutbox, SiteRuntime, SiteCallAudit; doc refreshes for Component-
  Commons / -ManagementService / -CLI / -ExternalSystemGateway / -HealthMonitoring
  / -Transport / -ConfigurationDatabase; CD-023 index-name doc alignment.

11 new regression tests (RouteHelper x4, SiteStreamGrpcServer x2,
ClusterOptionsValidator x1, SiteCommunicationActor x1, DeploymentService x1,
EventLogPurgeService x3). Build clean (0 warnings); InboundAPI/Communication/
Host suites all green. README regenerated: 112 open (was 136).
2026-05-28 06:28:31 -04:00
Joseph Doherty e3ca9af1be fix(transport): Overwrite resolution now syncs child collections (2 findings)
Transport-001: template Overwrite now diff-and-merges the bundle's
Attributes / Alarms / Scripts onto the target template via three private
helpers (SyncTemplateAttributesAsync / SyncTemplateAlarmsAsync /
SyncTemplateScriptsAsync). Each helper emits one audit row per detected
add / update / delete and feeds the post-merge state into the existing
ResolveAlarmScriptLinks and ResolveCompositionEdges passes.

Transport-002: external-system Overwrite now syncs the Methods collection
via a parallel SyncExternalSystemMethodsAsync helper mirroring the T-001
shape, with ExternalSystemMethodAdded / Updated / Deleted audit rows.

Both fixes are covered by new integration tests in BundleImporterApplyTests.
README regenerated — open findings dropped from 146 to 136; all 10 open
High findings are now closed (0 Critical, 0 High, 46 Medium, 90 Low
remaining).
2026-05-28 05:54:03 -04:00
Joseph Doherty ac96b83b08 fix(high-severity): close 9 of 10 open High findings across 8 modules
Comm-016: delete dead HandleConnectionStateChanged + _debugSubscriptions /
_inProgressDeployments tracking + ConnectionStateChanged message record.
Disconnect detection is owned by the transport layers (gRPC keepalive PING
~25s; Ask-timeout at CommunicationService). Updates the
Component-Communication.md design doc to make that explicit.

SnF-018: NotificationForwarder.DeliverAsync now discards a corrupt buffered
payload (Warning log + return true) instead of returning false and parking
the row — honoring the design's "notifications do not park" invariant.

DM-018: reconciliation no longer force-sets Enabled, preserving an
intentional Disabled state after central failover.

ESG-018: DeliverBufferedAsync (both ExternalSystemClient + DatabaseGateway)
catches JsonException and returns false, turning a corrupt buffered row
into a parked operation instead of a retry-forever poison message.

InboundAPI-022: register ActiveNodeGate as IActiveNodeGate in the Central
DI branch so standby-node gating is actually wired up in production.

NS-019: remove orphaned NotificationDeliveryService /
INotificationDeliveryService / NotificationResult; central notification
delivery now lives entirely in NotificationOutbox.

SEL-016: normalise From/To filters to UTC before ISO-string compare so
non-UTC DateTimeOffset clients no longer get spuriously excluded events.

TE-017: include Description on attributes/alarms and a HashableConnections
projection (protocol, endpoint JSON, failover count) in the revision hash
and DiffService; staleness detection now catches description-only and
connection-endpoint edits.

Transport-001 and Transport-002 (also High) remain Open — they're being
handled in a follow-up batch because both touch BundleImporter.cs and
must serialise.
2026-05-28 05:40:15 -04:00
Joseph Doherty f936f55f51 fix(concurrency): close 8 race / thread-safety findings across CD, DCL, SR
CD-015: rewrite NotificationOutboxRepository.InsertIfNotExistsAsync as raw-SQL
IF NOT EXISTS … INSERT with SqlException 2601/2627 catch, ending the
at-least-once livelock on the site→central notification handoff.

DCL-018/019/020/021/022: add _subscribesInFlight guard so concurrent
same-tag subscribes don't orphan an adapter handle; delete the latent
dead _subscriptionHandles dictionary; stop double-counting
_totalSubscribed when an unresolved tag is promoted via another instance;
release adapter handles on mid-flight unsubscribe; gate the
tag-resolution retry timer with IsTimerActive so subscribe bursts don't
reset it into starvation.

SR-020: add _terminatingActorsByName shadow so a third deploy arriving
during a pending redeploy doesn't crash on InvalidActorNameException —
displaced senders get a Failed/superseded response and the latest
command wins on Terminated.

SR-024: split OperationTrackingStore reads from writes (fresh
SqliteConnection per GetStatusAsync) so long writes don't block status
queries; rewrite Dispose to drop the sync-over-async bridge that could
deadlock on a non-reentrant SyncContext; Interlocked.Exchange makes the
dispose-once flag race-safe across both paths.
2026-05-28 05:20:13 -04:00
Joseph Doherty 5d2386cc9d fix(transport): close bundle security + plaintext-retention gaps (4 findings)
T-003: move the unlock lockout server-side. The 3-strike counter used to
live in the Razor page only — a second tab / CLI caller could re-upload
the same bytes and grind PBKDF2 indefinitely. The counter now lives in
IBundleSessionStore, keyed by ContentHash, so retries against identical
bundle bytes are throttled regardless of client. BundleLockedException
surfaces the new typed error path.

T-005: bind the manifest's non-derivative fields into AES-GCM AAD. A
SHA-256 of the manifest (with ContentHash + Encryption normalised to
sentinels) is now passed to AesGcm.Encrypt / .Decrypt, so a tampered
SourceEnvironment / ExportedBy / CreatedAtUtc on a stolen bundle yields
an authentication-tag mismatch instead of slipping past the Step-4
typo-resistant confirmation gate.

T-006: cap zip entry count, decompressed length, and compression ratio
in LoadAsync's envelope validator BEFORE any payload is decompressed,
using ZipArchiveEntry.Length / .CompressedLength. New TransportOptions
fields default to 4 entries / 200 MB / 50x ratio.

T-007: clear decrypted plaintext on the ApplyAsync failure path and zero
the buffer on success before removing the session, so a 100 MB
DecryptedContent doesn't sit in memory for the 30-min TTL after a failed
apply. A BundleSessionEvictionService BackgroundService now also drives
EvictExpired periodically so abandoned sessions clear without needing a
fresh Get() call to trigger lazy eviction.

Also resolves NO-010 — the misleading "writer never throws" XML doc was
the same code+comment my prior NO-004 await-the-writer fix already
rewrote.
2026-05-28 04:14:07 -04:00
Joseph Doherty 291274ae76 fix(notifications): close OAuth2 SMTP + dispatcher resilience gaps (5 findings)
NS-021/NO-001: thread FromAddress into XOAUTH2 so M365 stops rejecting
sends with 535 5.7.3. Added an additive oauth2UserName parameter on
ISmtpClientWrapper.AuthenticateAsync; both NotificationService and
NotificationOutbox now pass config.FromAddress.

NO-002: clamp non-positive SmtpConfiguration.MaxRetries/RetryDelay to the
1-min / 10-attempt fallback with a Warning so a misconfigured row no
longer parks transient failures on the first attempt or burn-loops.

NO-003: route a lifecycle-scoped CancellationToken from the
NotificationOutboxActor through the dispatch sweep into the adapter so
in-flight SMTP sends abort on PostStop instead of blocking
CoordinatedShutdown for the full SMTP timeout per row.

NO-004: await the central audit writer inside the existing try/catch
instead of fire-and-forget so the audit task can't outlive the per-sweep
DI scope and writer faults reach the operator log instead of being
silently dropped.

Two AuditLog integration tests seeded RetryDelay = TimeSpan.Zero to force
immediate re-claim on the second tick; updated them to 1 ms so they keep
the same intent without tripping the NO-002 clamp.
2026-05-28 03:54:43 -04:00
Joseph Doherty e536178323 fix(security): close auth & site-scoping gaps across 8 findings
Resolves the auth-theme batch from the 2026-05-28 baseline review (8 findings
across Security/CentralUI/ManagementService/CLI). The most consequential gaps:
NotificationReport + SiteCallsReport now route through SiteScopeService so a
site-scoped Deployment user cannot see or act on other sites' rows (CUI-028);
QueryAuditLogCommand is no longer "any authenticated user" — gated Admin-only
to match /api/audit/query's strictness (MS-018); RoleMapper preserves the
broader grant when a user is in both an unscoped and scoped Deployment LDAP
group, instead of silently narrowing to the scoped set (Sec-016); and the
dead SiteScopeRequirement/Handler are deleted so SiteScopeService is
unambiguously the sole site-scoping mechanism (Sec-017). Pending findings:
172 → 164.
2026-05-28 03:35:29 -04:00
Joseph Doherty bae75be2d2 fix(transport): stop scanning DataSourceReference for blocker references
DetectBlockersAsync was feeding TemplateAttribute.DataSourceReference
into the identifier scanner alongside script bodies, but that field is
an OPC UA node-address path (e.g. "ns=3;s=Tank.Level") owned by the
device, not script source. The dot delimiter inside the path tripped
the heuristic into flagging the address segment ("Tank", "Sensor",
"TestChildObject", "DevAppEngine") as a missing SharedScript or
ExternalSystem reference -- a 100% false-positive class on any
template catalog with OPC-UA-mapped attributes.

Drop the DataSourceReference scan entirely. Attribute.Value is still
scanned because it can carry a design-time default expression that
calls into runtime APIs. Add a regression test pinning the new behavior.
2026-05-24 07:52:31 -04:00
Joseph Doherty 6bdada7549 fix(transport): drop blocker false positives for stdlib + member access
The DetectBlockersAsync heuristic was catching every PascalCase
"Identifier(" or "Identifier." token in script bodies and treating it
as a candidate SharedScript or ExternalSystem reference. On a normal
template catalog this surfaced 30+ blocker rows for .NET stdlib
(DateTimeOffset, Convert, ToString, Dispose, UtcNow...), ScadaLink
runtime API roots (Notify, Database, ExternalSystem, Scripts...), and
SQL keywords inside string literals (COUNT), blocking the import.

Two surgical fixes:

1. Skip identifiers preceded by `.` so `obj.Method()` no longer flags
   `Method` as a top-level reference.
2. Maintain a `KnownNonReferenceNames` denylist for the small set of
   well-known stdlib / runtime / SQL tokens that can never be
   user-defined SharedScripts or ExternalSystems.

The documented use case -- a top-level free-standing call to a missing
SharedScript or ExternalSystem (e.g. `MissingHelper()` at the start of
an expression, or `ErpSystem.Call(...)` where ErpSystem is the
external-system identifier) -- still produces a blocker row, pinned
by the existing test plus a new noise-filter regression test.
2026-05-24 07:46:24 -04:00
Joseph Doherty a7141c704f test(centralui): remove stale LoginPage_RendersLdapCredentialHint test
The asserted 'LDAP credentials' tagline was deliberately removed from
Login.razor in f973f49 but the test was not updated alongside. Drop
the test — it asserts on UI text that no longer exists by design.
2026-05-24 06:51:37 -04:00
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 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 9f1bb81993 test(transport): integration conflict resolution + rollback 2026-05-24 05:50:11 -04:00
Joseph Doherty 623aa8d061 test(transport): integration round-trip export → wipe → import 2026-05-24 05:48:24 -04:00
Joseph Doherty ef025a325d feat(centralui): Bundle Import filter on ConfigurationAuditLog page 2026-05-24 05:44:21 -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 e099ed2038 feat(centralui): TreeView checkbox-selection mode with tri-state 2026-05-24 05:13:04 -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 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 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 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 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