Commit Graph

476 Commits

Author SHA1 Message Date
Joseph Doherty 846cc5d96d fix(dcl): route native OPC UA alarms by binding identity, not event name
A native OPC UA alarm source's SourceReference has to be two things at once:
it is parsed as a NodeId to open the monitored item, and matched as a plain-name
prefix against the event's SourceName to route the transition to an instance. No
string is both, so a NodeId-form binding subscribed correctly and then silently
dropped every transition — "pymodbus/plc/HR200".StartsWith("nsu=...;s=pymodbus/plc/HR200")
is false. Only the empty (Server-object) binding worked, because StartsWith("")
matches everything, which is why the sole live smoke test never caught it.

Each OPC UA alarm feed is opened for exactly one binding, so every transition on
it belongs to that binding. The adapter now tags each transition's routing
identity (SourceObjectReference) with the binding string verbatim via the pure
OpcUaAlarmMapper.BuildIdentity, making DataConnectionActor's routing key an exact
match regardless of whether the binding is stored as ns=<index> or the durable
nsu=<uri> form. The Server-object aggregate feed keeps an empty routing identity,
so it reaches only "mirror everything" subscribers and never leaks into a
specific-node binding. The per-condition SourceReference key stays the readable
SourceName.ConditionName, so persistence and display are unchanged, and MxGateway
is untouched — its bindings are names and its mapper already emits matching names.

Unblocked by lmxopcua#473 (OtOpcUa now populates SourceNode/SourceName/EventType
on conditions); SourceName is the RawPath, so the per-condition key is unique.
Live end-to-end verification against native alarms still needs a v3 rig.

Fixes: Gitea #17
2026-07-17 14:50:50 -04:00
Joseph Doherty d32050da24 feat(dcl): bind OPC UA nodes by namespace URI, not baked index
A stored ns=<index> reference is only meaningful against one server's namespace
table. A server that adds, removes or reorders a namespace silently re-points
every binding: best case BadNodeIdUnknown, worst case the index now names a
different namespace holding a colliding identifier and the binding resolves to
the wrong node with Good quality. Nothing could detect that, because ScadaBridge
stored no namespace URI anywhere.

OtOpcUa v3.0 makes this concrete: v2's sole custom namespace and v3's raw
namespace both sit at index 2, so v2-era bindings resolve against v3 without
error while meaning something else entirely.

Adds OpcUaNodeReference as the single translation seam between stored references
and the wire. Resolve() accepts both ns=<index>;s=<id> (existing bindings, which
keep working unchanged) and the durable nsu=<uri>;s=<id>, mapping the URI to the
live index at use time; it is wired into every parse site — subscribe, read,
write, alarm-subscribe and browse. An unpublished URI now throws naming the URI
rather than binding to whatever occupies that index, and a svr= reference to
another server is rejected instead of being resolved against the wrong address
space.

The browser emits ToDurable(), so what the picker shows is what gets stored and
newly-authored bindings are index-proof from the start. That also closes a
round-trip gap: browse previously emitted ExpandedNodeId.ToString(), which for a
URI- or server-index-carrying reference produced a string NodeId.Parse could not
read back — the same method already resolved it correctly 57 lines later.

Bindings stored before this change keep their ns= form and keep working; they are
only as durable as the server's namespace order. Re-authoring against the picker
is what makes them durable, and that re-bind still needs a live v3 rig.

Refs: Gitea #14
2026-07-17 14:50:50 -04:00
Joseph Doherty 56b428d2ef docs(secrets): clustered master-key posture runbook + Central pointer (ScadaBridge G-5 T8) 2026-07-16 15:21:54 -04:00
Joseph Doherty ccee558ca7 merge(r2): r2-plan08
# Conflicts:
#	src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationOptionsValidator.cs
2026-07-13 11:10:15 -04:00
Joseph Doherty ca4b0dd849 merge(r2): r2-plan04 2026-07-13 11:09:36 -04:00
Joseph Doherty 699af8fc16 merge(r2): r2-plan06
# Conflicts:
#	CLAUDE.md
2026-07-13 11:09:24 -04:00
Joseph Doherty 84fdea60cf merge(r2): r2-plan05 2026-07-13 11:08:27 -04:00
Joseph Doherty 3ad5ad0d7e merge(r2): r2-plan03 2026-07-13 11:08:27 -04:00
Joseph Doherty 774e3918db merge(r2): PLAN-R2-07 UI/Management/Security 2026-07-13 11:08:10 -04:00
Joseph Doherty 5b16429635 fix(sitestream): deathwatch resets live-cache liveness on aggregator termination (plan R2-07 T10) 2026-07-13 11:01:54 -04:00
Joseph Doherty 4f39cfd6c6 docs(plans): re-consolidate deferral tracking — SBR gap + wonder-app overlay rows into the register, prune landed Fix-now rows, repoint the fired perf-envelope trigger at PLAN-R2-01 (plan R2-08 T10, arch-review 08r2 NF5) 2026-07-13 11:00:11 -04:00
Joseph Doherty 925c869826 fix(ui): Alarm Summary poll defers row rebuilds to the live path while IsLive (plan R2-07 T8) 2026-07-13 10:57:09 -04:00
Joseph Doherty 1fcfa4fb16 fix(security): scope-filter secured-write listing + align spec with enforced site scoping (plan R2-07 T6) 2026-07-13 10:53:16 -04:00
Joseph Doherty 11efe4be05 fix(health): flag metrics-stale for never-reported sites via FirstSeenAt anchor — null LastReportReceivedAt no longer skips the check (plan R2-01 T8) 2026-07-13 10:45:22 -04:00
Joseph Doherty 24c2097ec3 fix(security): ship trusted-proxy ForwardedHeaders config for Traefik topologies + document lockout-DoS trade-off (plan R2-07 T3) 2026-07-13 10:42:37 -04:00
Joseph Doherty e1a692016e fix(security): throttle DebugStreamHub LDAP bind via ManagementAuthenticator + correct coverage doc (plan R2-07 T1) 2026-07-13 10:36:32 -04:00
Joseph Doherty d536450611 docs(cluster): per-direction failover truth — first-seed bootstrap constraint + operator recovery actions; drop the undeliverable ~25s active-crash promise (plan R2-01 T2) 2026-07-13 10:33:56 -04:00
Joseph Doherty 41ffe42de3 docs(comm): accept + document standby aggregators and deleted-site viewer behavior; fix aggregator e2e ctor call for new params (plan R2-02 T14) 2026-07-13 10:25:10 -04:00
Joseph Doherty 3d6973cc89 perf(sitecallaudit): filtered IX_SiteCalls_Terminal index backs the retention purge predicate (plan R2-04 T10) 2026-07-13 10:23:19 -04:00
Joseph Doherty 439dee52d3 fix(kpi): classify the failover fold-race pass loss as a self-healing Information event (plan R2-04 T9) 2026-07-13 10:21:02 -04:00
Joseph Doherty 1c8b2bc745 docs(kpi): trend charts state per-bucket-total semantics for rate metrics (plan R2-04 T7) 2026-07-13 10:12:58 -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 6e1ab62d27 feat(saf): resync ack confirmation + completed/ack-missing telemetry; doc resync rewrite (plan R2-02 T7) 2026-07-13 10:05:33 -04:00
Joseph Doherty 7b95ef3752 fix(kpi): backfill skips or shrinks to the rollup watermark on failover restarts (plan R2-04 T4) 2026-07-13 10:03:33 -04:00
Joseph Doherty 91bde0fae6 fix(inbound): 415 guard and lenient JSON parse cover chunked (no Content-Length) bodies (plan R2-06 T6) 2026-07-13 10:01:22 -04:00
Joseph Doherty fb9dc7ad14 fix(inbound): type SITE_UNREACHABLE on AskTimeoutException at the router await instead of message-substring sniffing (plan R2-06 T5) 2026-07-13 09:57:22 -04:00
Joseph Doherty 3b9f6b9572 docs(siteruntime): compile gate is synchronous by design; Expression alarms share the bounded script pool (plan R2-03 T7) 2026-07-13 09:56:45 -04:00
Joseph Doherty 26dce8b69f fix(comm): heartbeat IsActive uses the shared oldest-Up predicate; Host wires one delegate everywhere (plan R2-02 T4) 2026-07-13 09:50:44 -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 1429ddaa0e docs(plans): add MES alarm-status API draft plan (2026-06-30)
Ports the legacy WWSupport/APIServer MES alarm-status endpoints onto the
Inbound API, mirroring the MesMoveIn/MesMoveOut pattern; draft for review,
not yet executed. Committed ahead of PLAN-R2-08 T13's hygiene pass.
2026-07-13 01:45:50 -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 84112db344 docs(plans): add implementation plans for deferred #10 (aggregated live alarm stream) and #22 (KPI hourly rollups)
Two draft task-by-task plans for the two Scale/YAGNI deferrals whose revisit
triggers are now in view:

- #10 aggregated live alarm stream: transient per-site in-memory central live
  cache seeded by the snapshot fan-out + additive SubscribeSite alarm-only gRPC
  stream, pushed via the IDeploymentStatusNotifier pattern; honors the [PERM]
  no-central-alarm-store rule. 8 tasks.
- #22 KPI hourly rollups: separate KpiRollupHourly table, recorder hourly fold
  with failover-safe lookback re-fold, per-metric gauge-vs-rate aggregation,
  range-threshold routing in the query service, longer rollup retention, one-shot
  backfill, plus 30d/90d window buttons so rollups have a caller. 8 tasks.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 11:36:35 -04:00
Joseph Doherty 22136d36d9 feat(testrun): route WaitForAttribute in the Test-Run sandbox (deferred #14)
Closes deferred-work register item #14 with full fidelity. The Central UI Test-Run
sandbox previously threw ScriptSandboxException for Attributes.WaitAsync/WaitForAsync;
now the value-equality forms route to the bound deployed instance over the same
cross-site path inbound Route.To().WaitForAttribute() uses.

- Commons: additive trailing RouteToWaitForAttributeRequest.RequireGoodQuality
  (default false; message-evolution safe) so quality-gated waits route too.
- SiteRuntime: DeploymentManagerActor's routed-wait handler now threads
  request.RequireGoodQuality into the InstanceActor WaitForAttributeRequest
  (previously hard-coded to default).
- CentralUI sandbox: ISandboxInstanceGateway.WaitForAttributeAsync +
  SandboxInstanceGateway impl (via CommunicationService.RouteToWaitForAttributeAsync);
  SandboxAttributeAccessor value-equality WaitAsync/WaitForAsync route through it
  (codec-encoded target, scope-resolved name). Predicate overloads stay unsupported
  (an in-process lambda can't be routed) and throw a clearer, dedicated message.
- Tests: sandbox accessor routing + predicate/unbound rejection (CentralUI);
  site-handler RequireGoodQuality threading (SiteRuntime). Register + the historical
  waitfor-deferred plan doc updated.

Full slnx build 0/0; CentralUI 59, SiteRuntime routed-wait 4, InboundAPI wait 32 green.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 09:29:38 -04:00
Joseph Doherty 4cd21b342b feat(auditlog): make SiteAuditBacklogReporter cadence configurable (deferred #21)
Consolidates the reporter's poll cadence onto SqliteAuditWriterOptions instead of
the hard-coded 30 s constant, closing deferred-work register item #21 (the config-
shape cleanup its own XML doc flagged as a follow-up).

- SqliteAuditWriterOptions.BacklogPollIntervalSeconds (default 30).
- Reporter reads it via IOptions; precedence explicit-override (tests) > configured
  > 30 s default; a non-positive value falls back to the default. Corrected the
  stale "hard-code / tunable in a follow-up" class-doc.
- Cadence tests + register updated (row 21 -> Resolved).

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 08:56:32 -04:00
Joseph Doherty f480a56737 feat(instance): native-alarm-source-override CSV bulk import (deferred #12) + doc fixes
Closes the operator parity gap the deferred-work register tracked as #12: native
alarm source overrides could only be set one-at-a-time, while attribute overrides
had a CSV bulk path. Adds an all-or-nothing CSV import for native sources.

- Commons: extract the RFC-4180 line splitter into a shared CsvLineSplitter
  (refactor OverrideCsvParser onto it — no behavior change, pinned by its tests);
  new NativeAlarmSourceOverrideCsvParser (header SourceName,Connection,
  SourceReference,Filter; blank = inherited).
- Commons: NativeAlarmSourceOverrideEntry + bulk SetInstanceNativeAlarmSource-
  OverridesCommand (auto-registered via the reflection command registry).
- ManagementService: Deployer-gated handler — flattens once, validates every
  source resolves + is unlocked + no duplicates up front, then upserts the whole
  batch under a single SaveChanges (true all-or-nothing txn). Added to the frozen
  authorization matrix; dispatch-coverage guard passes.
- CLI: `instance native-alarm-source import --instance-id --file` (parity with
  `instance import-overrides`) + README.
- Tests: native parser (Commons), CLI parse/entry mapping, 3 bulk-handler tests
  (happy path single-commit, locked-source reject, unresolved-source reject).

Also corrects a stale XML-doc line in ScriptRuntimeContext (WaitForAttribute
quality-gated mode is shipped, not "planned") and updates the deferred-work
register: marks #7/#13/#15/#16/#20 verified-resolved, #12 as CLI/API-shipped with
only the Central UI upload affordance still pending.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 08:52:12 -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 d61881ffaf refactor(site-runtime): excise vestigial site-side notification-list surface — repo, DI registration, dead write paths (arch-review 08 §1.3/#23)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 07:00:09 -04:00
Joseph Doherty 7ff1263ac0 docs(plans): deferred-work register — triage the 23-item arch-review inventory into fix-now (plan-owned) vs deferred-with-rationale
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 06:53:30 -04:00
Joseph Doherty 433c6db4ec feat(cli): browse/search/verify-endpoint/cert-trust commands — actor parity (arch-review C4)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:57:58 -04:00
Joseph Doherty d8a39f3c35 feat(ui): secured-write history paging + submission age in approve dialog (arch-review S2/P3)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:57:58 -04:00
Joseph Doherty 655834f0b8 feat(management): additive Skip/Take paging on template/instance lists (arch-review P2)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:57:14 -04:00
Joseph Doherty 8221ee797e fix(management): canonicalize role-mapping casing at write path, closing UI/actor case split (arch-review C5)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:52:45 -04:00
Joseph Doherty 83e09fc210 fix(management): honor MgmtDeployArtifactsCommand.SiteId + enforce site scope (arch-review C2)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:51:03 -04:00
Joseph Doherty 8d4ffa7ef1 feat(management): secured-write list paging with TotalCount (arch-review P3)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:42:35 -04:00
Joseph Doherty 03295e91bb feat(management): opportunistic expiry sweep on secured-write list (arch-review S2)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:39:29 -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 b3e2294e1d chore(deploy): ship ManagementService command-timeout config to all central-node topologies (arch-review S1)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:30:39 -04:00
Joseph Doherty 5415e6566f feat(management): Admin-gated actor dispatch for cert-trust commands (arch-review C4)
Made CommunicationService.TrustServerCertAsync/RemoveServerCertAsync/ListServerCertsAsync virtual to support the test seam (mirrors existing virtual WriteTagAsync).

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:29:57 -04:00