Commit Graph

59 Commits

Author SHA1 Message Date
Joseph Doherty 34227991ea docs(claude): correct five understatements about the inter-cluster boundary
Found while OtOpcUa researched this repo as the model for its own per-cluster
mesh work. Each was verified against the code, not inferred from the doc.

Three transports cross the boundary, not two: ClusterClient, gRPC, and plain
token-gated HTTP for the deploy config fetch (DeploymentConfigEndpoints,
X-Deployment-Token, AllowAnonymous with the per-deployment token as the entire
security boundary).

The gRPC direction is inverted from the data flow. Data moves site to central,
but each SITE hosts the gRPC server and central dials in — MapGrpcService appears
only in the Site branch of Program.cs. There is no gRPC server on central, which
is why the two Ingest* RPCs are dead in practice. Also records the 6-RPC surface
(the doc implied 2), the (site, endpoint) factory key that fixed an arch-review
High, and the vendored-generated-code caveat.

Active/standby is ActiveNodeEvaluator.SelfIsOldestUp — the OLDEST Up member, and
explicitly never cluster.State.Leader, because leadership diverges from singleton
placement permanently after a restart-and-rejoin and both sides claim it during a
partition. The equivalence oldest-Up == singleton placement is the design, and it
was not stated anywhere in this file.

All clusters share one ActorSystem name ("scadabridge", hardcoded at
AkkaHostedService.cs:191); they are separate clusters only by seed partitioning.
Required, not incidental — Akka.Remote address matching means ClusterClient could
not reach a differently-named system. Site nodes also carry two roles, base plus
site-{SiteId}, with singletons scoped to the site-specific one.

Neither inter-cluster transport is authenticated or encrypted: no Akka TLS or
secure cookie, gRPC is h2c, and LocalDbSyncAuthInterceptor gates only the LocalDb
sync path — so the whole SiteStreamService surface, including the two Pull RPCs
returning audit rows, is reachable by anyone who can hit :8083. The boundary
assumes a trusted network; that assumption deserves to be explicit.

Also promotes two things from docs/ into CLAUDE.md because they change decisions:
the default 128 KB Akka frame size with log-frame-size-exceeding off and no custom
serializer (a silent single-message drop that leaves the association healthy), and
the registered two-node keep-oldest total-outage gap, whose own drill has a mode
existing "to make the registered gap observable — not to pretend it is covered."

Committed to a branch rather than main; merge at your discretion.
2026-07-21 10:00:01 -04:00
Joseph Doherty 7b5a5a6f34 docs(localdb): phase 2 truth pass across both repos
Normative first: Component-StoreAndForward.md:83 specified the whole chunked,
ack-confirmed SfBufferSnapshotChunk resync protocol, which Phase 2 deleted. It
is rewritten rather than removed — the failure modes it reasoned about still
exist, they are just bounded differently — and it now states the
duplicate-delivery bound explicitly: a message can be delivered twice only when
the OLD primary delivered it and the status change had not yet replicated when
the gate flipped. One flush interval plus the in-flight ack, and unlike the old
model it does NOT grow with backlog depth or with how long a node was absent.
The N1 directional-authority and N5 orphan-row hazards are recorded as
structurally gone, not merely unguarded.

Frame-size known-issue amended: its 2026-06-26 resolution replaced the
intra-site hop with notify-and-fetch; Phase 2 then deleted notify-and-fetch
itself, so the 128 KB Akka frame constraint no longer applies to that hop in
any form. Successor ceiling recorded (4 MB gRPC cap via MaxBatchSize, which
batches by ROW COUNT), including that the failure mode differs — an oversized
gRPC message is rejected, not silently dropped.

Deployment docs gain the two operational constraints that have no home in code:
a site pair must be stopped and started TOGETHER (the SfBufferSnapshot compat
handler that made a mixed-version pair converge went with the replicator, and a
mixed pair now diverges silently), and a node offline beyond TombstoneRetention
can resurrect deleted rows on rejoin.

Both CLAUDE.md files corrected — each still said Phase 2 was NOT started.

Definition of done closed: build 0 warnings, all 10 suites green (3509 tests,
0 failures, 0 skips). Two DoD items needed amending rather than ticking: the
stale-symbol grep still matches 4 lines, all deliberate comment prose recording
what was deleted (a literal zero would delete the explanations that stop the
old design coming back), and the live gate has 10 evidence items, not the 9 the
checklist claimed.

Deletes the phase2 resume-state scratch doc, which said to delete it on landing.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-20 05:37:48 -04:00
Joseph Doherty b64857c8d6 docs(localdb): Phase 2 gate + final Phase 1 task record
Task 14. A GATE document, not an implementation plan - the plan explicitly wants
Phase 2 planned against post-Phase-1 reality, which means after this record
exists rather than from the original design alone.

Records what Phase 1 actually established (ordering constraints, where
registration lives and why, the fail-closed interceptor, the silent meter
allowlist), what must be ported before anything is deleted (both bespoke
replicator test files, treated as specifications, plus the accepted N5
bounded-duplicate race), and why Phase 2 is a harder risk class than Phase 1:
it REPLACES a working mechanism rather than adding one where none existed,
sf_messages has external side effects, config tables drive deployment, there is
no dual-mechanism period, and the migration is real this time rather than the
usual no-op.

The most important line is an open question, not an answer: choosing
MaxOplogRows / MaxOplogAge / snapshot-resync thresholds for sf_messages write
rates needs a Phase-1 rig SOAK that has not been run. The live gate proved
correctness, not steady-state behaviour over time. Phase 2 should not be planned
without it.

tasks.json finalized: all 15 tasks completed, every deviation from the plan
recorded with its reason.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-19 10:00:50 -04:00
Joseph Doherty 568735a41e docs(localdb): Phase 1 truth pass
Task 13.

SiteEventLogger's XML doc said 'Not replicated to standby. On failover, the new
active node starts a fresh log.' That is now false and was the exact behaviour
Phase 1 set out to fix.

OperationTrackingOptions.ConnectionString and SiteEventLogOptions.DatabasePath
marked MIGRATION-ONLY: nothing reads them but SiteLocalDbLegacyMigrator, and
they are safe to delete from a config once a node has migrated.

ScadaBridge CLAUDE.md gains a consolidated-site-database entry covering the
GUID/opaque-token contract change, the migration-only keys, the incidental
CWD-outside-the-volume data-loss fix, the fail-closed default-OFF replication
posture, and the fact that ZbTelemetryOptions.Meters is a silent allowlist.

scadaproj CLAUDE.md's LocalDb row goes from 'no app adoption yet' to the honest
current state, per the component-status-honesty convention: ScadaBridge Phase 1
only, on an UNMERGED branch, replication live-proven on the rig's site-a pair
ONLY, default-OFF everywhere else, Phase 2 not started, no other app adopted, no
production deployment replicating.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-19 09:51:55 -04:00
Joseph Doherty 4d869de9c2 docs: sister-repo index update + working notes
CLAUDE.md: the historian SDK is now owned inside HistorianGateway at
histsdk/ (the separate ~/Desktop/histsdk repo was folded in with history),
so the sister-project list no longer points at a standalone repo.

Also checks in working notes that were sitting untracked in the tree:
deferred.md (remaining deferred work snapshot), ScadaBridge-docs-fixed.md and
ScadaBridge-docs-issues.md (documentation-analysis report output).
2026-07-16 23:31:34 -04:00
Joseph Doherty 699af8fc16 merge(r2): r2-plan06
# Conflicts:
#	CLAUDE.md
2026-07-13 11:09:24 -04:00
Joseph Doherty 3ee98c8af6 docs: sync Transport/ScriptAnalysis/TemplateEngine specs + CLAUDE.md with the round-2 fix wave (plan R2-05 T9)
No ../scadaproj/CLAUDE.md change needed — no wire-relationship/stack/namespace change in this wave.
2026-07-13 10:08:51 -04:00
Joseph Doherty 9668600c38 docs(inbound): correct bus-consumer claims in tracker + invalidation-contract doc + CLAUDE.md (plan R2-06 T2) 2026-07-13 09:49:53 -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 5078d25302 docs(sitestream): document aggregated live alarm stream + resolve deferred #10 (plan #10 T8)
Alarm Summary is now live-cache-driven via a transient per-site central
live alarm cache (ISiteAlarmLiveCache) fed by a site-wide, alarm-only
SubscribeSite gRPC stream (seed-then-stream), with the 15s poll retained
as fallback + NotReporting authority. No persisted central alarm store
([PERM]). Updated Component-CentralUI, Component-Communication (new §6.1
+ SubscribeSite RPC), M7 design (T13 delivered note), native-alarms
[PERM] note, stillpending follow-up note, CLAUDE.md, and moved register
row #10 to Resolved (left #22 in place for the sibling branch).

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 12:49:44 -04:00
Joseph Doherty c9fa3b07f4 docs(kpihistory): document hourly rollups + resolve deferred #22 (plan #22 T8)
Update Component-KpiHistory.md with the KpiRollupHourly schema, the third
recorder rollup tick + one-shot backfill, dual raw/rollup purge retention,
raw-vs-rollup query routing by RollupThresholdHours, per-metric gauge-vs-rate
aggregation (KpiMetricAggregationCatalog), the four new options + bounds, and
30d/90d trend windows. Append a Delivered-2026-07-10 note to the m6 design
plan, move register row #22 from Deferred to Resolved, and note rollups in the
CLAUDE.md KPI History bullet. Docs-only; no code changed.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 12:18:46 -04:00
Joseph Doherty d8487eab2d docs: close arch-review 08 drift batch — Commons Observability folder, site-repo exception, csproj separator, docs/components scope, DelmiaNotifier convention exception
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 07:00:09 -04:00
Joseph Doherty 4887461b64 chore(management): log debug-stream push failures; fix event-log filter docs; refresh stale SourceNode note (arch-review S5/C7/C8)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 06:07:19 -04:00
Joseph Doherty 064a7d9415 feat(management): server-side secured-write TTL — stale pending writes expire, never execute (arch-review S2)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:38:19 -04:00
Joseph Doherty 178ae35308 feat(notifications): SMS notifications retry under SmsConfiguration.MaxRetries/RetryDelay (SMTP policy remains the Email + fallback policy) — wires the dead fields
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 04:07:44 -04:00
Joseph Doherty 8e5631075d docs: sync Transport/TemplateEngine/DeploymentManager specs + CLAUDE.md with the arch-review fix wave (plan 05)
#05-T25 (terminal doc sweep + deferred-decision records). Component-Transport:
scripts/ dir marked not-implemented; transported-entity list gains LockedInDerived
/ script cadence-timeout / real AreaName / native-alarm-sources (round-trip-guard
verified); LineDiffer input cap (T22); rename call-site limitation made load-bearing;
preview→apply no-optimistic-concurrency window + session cap recorded. Component-
TemplateEngine: alarm-override granularity corrected (only TriggerConfig+PriorityLevel
instance-overridable, NOT Description/OnTriggerScript). Component-DeploymentManager:
OperationLockManager per-node invariant (routing rejection owned by plans 01/07) +
post-success audit isolation (T17). CLAUDE.md #24 blurb + Transport bullet updated
(trust gate, severity split, ArtifactDiff identity fix, T8 fidelity fixes, T14
plan-06 handoff). Component-ScriptAnalysis fifth-call-site + deny-list already synced
in T20/T21. No ../scadaproj/CLAUDE.md change needed — no wire-relationship/stack shift.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 03:22:34 -04:00
Joseph Doherty 4bdd5f0e4a fix(notifications): Notify.Send enqueues unbounded (maxRetries 0) and defers to sweep — no 30s script-thread block, no stranding 2026-07-08 20:56:09 -04:00
Joseph Doherty c2230b7171 build(slnx): add CLI.Tests to the solution — retire the silently-skipped-tests gotcha (arch-review 08 §2.4) 2026-07-08 14:46:28 -04:00
Joseph Doherty b910f5ebcd docs(claude): note EF migrations --no-build empty-scaffold gotcha
`dotnet ef migrations add --no-build` scaffolds off the stale prebuilt DLL and
can emit an empty migration; build first and delete the empty Migrations files
if it happens (migrations remove needs a live DB). Placed next to the existing
EF migrations line in Code Organization.
2026-07-07 11:29:21 -04:00
Joseph Doherty 44db509eaf docs(claude): document test command + CLI.Tests slnx-exclusion gotcha
`dotnet test ZB.MOM.WW.ScadaBridge.slnx` silently skips
ZB.MOM.WW.ScadaBridge.CLI.Tests (the one test project not in the slnx), so a
green solution run can hide CLI tests that never compiled. Add the test
command and point at testing the CLI project directly.
2026-07-07 11:26:55 -04:00
Joseph Doherty bde883dfe3 docs(claude): frame scadaproj as the umbrella index + propagate-changes rule
Reconcile the Sister Projects section to mirror the peer repos
(MxAccessGateway/HistorianGateway): frame ~/Desktop/scadaproj as the parent
workspace whose CLAUDE.md is the canonical umbrella index that indexes this
repo, point there for the full family list + cross-project map, and add the
previously-missing peers (HistorianGateway, histsdk, scadaproj). Add the
reciprocal "propagate cross-repo changes to the umbrella index" Editing Rule
so an index-recorded fact change here updates the ScadaBridge entry in
../scadaproj/CLAUDE.md in the same change.
2026-07-07 11:11:54 -04:00
Joseph Doherty b58f3aeb61 docs: list DelmiaNotifier (#27) in component tables (standalone external client tool) 2026-06-26 05:25:09 -04:00
Joseph Doherty c5c8511379 docs(sms): reconcile Component/CLAUDE/README docs for SMS notifications (S11) 2026-06-19 11:21:56 -04:00
Joseph Doherty 4d888c63a3 docs(m8): Transport site/instance transport, name-mapping, Myers diff, stale enumeration (M8 INT) 2026-06-18 07:53:25 -04:00
Joseph Doherty 40928535fd docs(m7): reflect OPC UA / MxGateway UX (T13-T17) across component docs + CLAUDE/stillpending/completion-design 2026-06-18 04:13:21 -04:00
Joseph Doherty 241a792e7b docs(kpi): K17 — #26 KpiHistory component doc + README/CLAUDE + cross-component interactions + completion-design update 2026-06-17 20:52:12 -04:00
Joseph Doherty 639e331db1 test+docs(m5): M5.7 — de-date 2 EndToEnd purge tests (closes #52); document T3-T8 in Component-AuditLog/-CLI/README/CLAUDE
Tests: anchor SeedOccurredAt() to a fixed thresholdAnchor (2026-01-20) and compute
RetentionDays dynamically (UtcNow - anchor + 1d) so the threshold always sits near
Jan 20 2026, between the Jan-15 "old" seed (purged) and Apr-15/Jun-15 "kept" seeds.
Seed dates stay within the explicit pf_AuditLog_Month boundary range (Jan 2026 –
Dec 2027) — relative-from-now offsets landed before 2026-01-01 (the catch-all
partition, invisible to GetPartitionBoundariesOlderThanAsync). Both tests confirmed
passing; all 284 AuditLog tests green.

Docs:
- Component-AuditLog.md: per-channel retention overrides (T3, PerChannelRetentionDays
  + bounded DELETE + AuditLogPurge:ChannelPurgeBatchSize); ParentExecutionId tag-cascade
  now spans alarm-triggered + nested CallScript/CallShared + inbound→routed (T4, "no
  further spawn points deferred"); per-node stuck KPIs for Notification Outbox +
  Site Call Audit (T6); T7 structured response-capture increments (request headers in
  Extra.requestHeaders, AuditInboundCeilingHits counter, per-method SkipBodyCapture);
  T8 CLI audit tree; T1 hash-chain + T2 Parquet explicitly marked deferred to v1.x.
- Component-CLI.md + README.md: document audit tree --execution-id <guid> and
  audit backfill-source-node --sentinel/--before/--batch with exact options verified
  against AuditCommands.cs; update Interactions to list new endpoints.
- CLAUDE.md: update audit-log design-decision bullets for T3 per-channel retention,
  T4 tag-cascade complete, T6 per-node KPIs, T7 inbound capture increments, T8 tree
  command; clarify T1/T2 remain deferred to v1.x.
2026-06-16 22:26:09 -04:00
Joseph Doherty fb5f14e04f docs(m3): document Script Analysis component (#25); reconcile consumer specs + README/CLAUDE component list 2026-06-16 20:05:24 -04:00
Joseph Doherty 35a4a5bfea docs(glauth): dev/test LDAP is now the shared GLAuth on 10.100.0.35
infra/ no longer runs scadabridge-ldap (retired); central nodes bind the shared
zb-shared-glauth on 10.100.0.35:3893 (dc=zb,dc=local). Source of truth:
scadaproj/infra/glauth/. test_infra_ldap.md banner-marked SUPERSEDED.
2026-06-04 16:38:08 -04:00
Joseph Doherty d69031dd08 docs: reorganize CLAUDE.md project-structure section 2026-05-31 21:41:29 -04:00
Joseph Doherty 003e54c1fb docs: native alarm ingestion across component docs + CLAUDE.md 2026-05-31 02:55:00 -04:00
Joseph Doherty 7b0b9c7365 refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj,
namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated.
ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated.
SQL roles/logins, LDAP domains, CLI command name, and CLI config dir
(~/.scadalink → ~/.scadabridge) also renamed.

Build green; 5 Host.Tests fail awaiting SQL login rename in next commit.
Pre-existing StaleTagMonitor timing flakes unchanged.

Rename script committed at tools/rename-to-scadabridge.sh.
2026-05-28 09:37:45 -04:00
Joseph Doherty 1eb6e972b0 docs: add XML doc comments across src + Sister Projects section in CLAUDE.md
Bulk CommentChecker pass: fills in <param>/<inheritdoc> tags on public
APIs across all 23 src/ projects so the doc-coverage gate is green. Also
adds a Sister Projects section to CLAUDE.md pointing at the MxAccess
Gateway and OtOpcUa sibling repos, and gitignores local credential
captures (*login*.txt) and the wonder-app-vd03 deploy/ artifacts.
2026-05-28 01:55:24 -04:00
Joseph Doherty f63d379048 docs: cross-reference docker-env2 from root README, CLAUDE.md, and infra README 2026-05-24 07:17:41 -04:00
Joseph Doherty b1daf9abb8 docs: README + component cross-references for Transport (#24) 2026-05-24 04:52:55 -04:00
Joseph Doherty 9e5e32d0f2 docs(audit): add SourceNode column to AuditLog/Notifications/SiteCalls design + plan
- Adds SourceNode varchar(64) NULL to AuditLog, Notifications, and SiteCalls
  tables with role-name semantics: node-a/node-b for site rows (qualified by
  SourceSiteId), central-a/central-b for central direct-write rows.
- New IX_AuditLog_Node_Occurred (SourceNode, OccurredAtUtc) index.
- Reframes CLAUDE.md from documentation-only to implementation project.
- Adds docs/plans/2026-05-23-audit-source-node.md + tasks.json companion.
2026-05-23 15:34:44 -04:00
Joseph Doherty fb1312d0bf test(auditlog): end-to-end ParentExecutionId correlation + docs 2026-05-21 19:12:19 -04:00
Joseph Doherty fd76c19007 test(auditlog): end-to-end ExecutionId correlation + docs 2026-05-21 16:06:40 -04:00
Joseph Doherty 302380ef8e docs(audit): register Audit Log (#23) in CLAUDE.md component list and key decisions 2026-05-20 08:46:19 -04:00
Joseph Doherty 38b51ef894 docs: record cached-call tracking in CLAUDE.md 2026-05-19 12:03:17 -04:00
Joseph Doherty ba166bf503 docs(notification-outbox): split UI bullet, fix outbox-vs-S&F wording in CLAUDE.md 2026-05-18 23:41:32 -04:00
Joseph Doherty 7585612347 docs(notification-outbox): split overstuffed CLAUDE.md design-decision bullet 2026-05-18 23:40:01 -04:00
Joseph Doherty b9e0ef4650 docs(notification-outbox): update component list and design decisions in CLAUDE.md 2026-05-18 23:38:28 -04:00
Joseph Doherty 9dccf8e72f deprecate(lmxproxy): move all LmxProxy code, tests, and docs to deprecated/
LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL
adapter files, and related docs to deprecated/. Removed LmxProxy registration
from DataConnectionFactory, project reference from DCL, protocol option from
UI, and cleaned up all requirement docs.
2026-04-08 15:56:23 -04:00
Joseph Doherty 416a03b782 feat: complete gRPC streaming channel — site host, docker config, docs, integration tests
Switch site host to WebApplicationBuilder with Kestrel HTTP/2 gRPC server,
add GrpcPort/keepalive config, wire SiteStreamManager as ISiteStreamSubscriber,
expose gRPC ports in docker-compose, add site seed script, update all 10
requirement docs + CLAUDE.md + README.md for the new dual-transport architecture.
2026-03-21 12:38:33 -04:00
Joseph Doherty fd2e96fea2 feat: replace debug view polling with real-time SignalR streaming
The debug view polled every 2s by re-subscribing for full snapshots. Now a
persistent DebugStreamBridgeActor on central subscribes once and receives
incremental Akka stream events from the site, forwarding them to the Blazor
component via callbacks and to the CLI via a new SignalR hub at
/hubs/debug-stream. Adds `debug stream` CLI command with auto-reconnect.
2026-03-21 01:34:53 -04:00
Joseph Doherty d91aa83665 refactor(docs): move requirements and test infra docs into docs/ subdirectories
Organize documentation by moving requirements (HighLevelReqs, Component-*,
lmxproxy_protocol) to docs/requirements/ and test infrastructure docs to
docs/test_infra/. Updates all cross-references in README, CLAUDE.md,
infra/README, component docs, and 23 plan files.
2026-03-21 01:11:35 -04:00
Joseph Doherty 0a85a839a2 feat(infra): add Traefik load balancer with active node health check for central cluster failover
Add ActiveNodeHealthCheck that returns 200 only on the Akka.NET cluster
leader, enabling Traefik to route traffic to the active central node and
automatically fail over when the leader changes. Also fixes AkkaClusterHealthCheck
to resolve ActorSystem from AkkaHostedService (was always null via DI).
2026-03-21 00:44:37 -04:00
Joseph Doherty 1a540f4f0a feat: add HTTP Management API, migrate CLI from Akka ClusterClient to HTTP
Replace the CLI's Akka.NET ClusterClient transport with a simple HTTP client
targeting a new POST /management endpoint on the Central Host. The endpoint
handles Basic Auth, LDAP authentication, role resolution, and ManagementActor
dispatch in a single round-trip — eliminating the CLI's Akka, LDAP, and
Security dependencies.

Also fixes DCL ReSubscribeAll losing subscriptions on repeated reconnect by
deriving the tag list from _subscriptionsByInstance instead of _subscriptionIds.
2026-03-20 23:55:31 -04:00
Joseph Doherty 7740a3bcf9 feat: add JoeAppEngine OPC UA nodes, fix DCL auto-reconnect and quality push
- Add JoeAppEngine folder to OPC UA nodes.json (BTCS, AlarmCntsBySeverity, Scheduler/ScanTime)
- Fix DataConnectionActor: capture Self in PreStart for use from non-actor threads,
  preventing Self.Tell failure in Disconnected event handler
- Implement InstanceActor.HandleConnectionQualityChanged to mark attributes Bad on disconnect
- Fix LmxFakeProxy TagMapper to serialize arrays as JSON instead of "System.Int32[]"
- Allow DataType and DataSourceReference updates in TemplateService.UpdateAttributeAsync
- Update test_infra_opcua.md with JoeAppEngine documentation
2026-03-19 13:27:54 -04:00