Commit Graph

55 Commits

Author SHA1 Message Date
Joseph Doherty f33f25a10b fix(auth+cli): fail-closed site-scope gate (row 98); case-insensitive audit filter values with a strict server-side check (row 99)
Register row 98 — the site-scope authorization gate was FAIL-OPEN on an
unresolvable target. The resolve-then-check shape was written as
`if (target != null) EnforceSiteScope(...)`, so a null resolve skipped the
check entirely and a site-scoped caller passed the gate for any id in the
system. Authorization now fails CLOSED: a target that cannot be PROVEN to sit
inside the caller's scope is treated as outside it.

The defect turned out to have THREE more instances than the register recorded.
Besides `EnforceSiteScopeForInstance` and `EnforceSiteScopeForIdentifier`, the
same inline shape sat in `HandleGetInstance`, `HandleGetSite` and
`HandleGetDataConnection` — read handlers that resolve their own target and so
never call the helpers. Those were the sharper half of the bug: they answered
success-with-null for a nonexistent id and Unauthorized for an out-of-scope
one, making the pair an id-enumeration oracle over the whole fleet. All five
now route through one shared `DenyUnresolvedTargetIfScoped` arm.

Two properties are load-bearing and pinned by tests:
  - System-wide callers (Administrator, or a Deployer with no site grants) are
    untouched — they return at the guards above the lookup and keep the
    handler's ordinary "not found" / null answer.
  - The denial is indistinguishable from an out-of-scope denial: same
    ManagementUnauthorized shape, and a message that never states whether the
    target exists.

Register row 99 — CLI `audit query --status delivered` failed client-side while
the server would have accepted it: `AcceptOnlyFromAmong` matches ordinally
against the capitalised literals. Same treatment row 88 gave `deploy status` —
the client-side gate is removed, the valid set moves into the option help text,
and the server becomes the single authoritative check.

That required ADDING the server check, not just relying on one: the shared
audit filter parser was LAX, silently dropping an unrecognised value and
leaving the dimension unconstrained — so removing the CLI gate alone would have
turned a loud client-side failure into a plausible, complete-looking page that
did not answer the question asked. New `ParseEnumListStrict` (case-insensitive,
rejects ordinals — `Enum.TryParse` accepts "3", and a filter is a name-based
contract) backs the ManagementService /api/audit/query + /api/audit/export
endpoints, which now return 400 BAD_REQUEST naming the valid set.

Other AcceptOnlyFromAmong sites surveyed in the audit group: `--channel`,
`--kind`, `--status` on BOTH `query` and `export` (6 uses) plus `export
--format` (rejected `--format CSV` though the server lowercases it) — all seven
fixed here. Deliberately left alone: `template folder reorder --direction`
(pure client-side literal, no server-side parse to defer to) and the global
`--format json|table` (CLI-local output selection, never sent to a server).

The CentralUI AuditLogPage drill-in parser and the CentralUI export endpoint it
feeds deliberately KEEP the lax parse — they read a query string a human may
have bookmarked, where a stale fragment must still render the page. Both
contracts now live side by side in AuditQueryParamParsers and are documented as
non-reconcilable.

Docs: Component-ManagementService.md gains a "Site-scope gate: fail-closed on
an unresolvable target" section and a strict-filter note on the audit
endpoints; Component-Security.md records the fail-closed shape under Deployer
site scoping; the CLI README marks the audit filter values case-insensitive and
states the no-silent-drop / no-ordinals contract.

Tests: 5 new ManagementActorTests fail before the row-98 fix (verified by
reverting ManagementActor.cs) and cover scoped+nonexistent -> denied,
system-wide+nonexistent -> unchanged (both Administrator and no-grant Deployer),
scoped+in-scope -> allowed, and the indistinguishability property. 21 CLI tests
fail before the row-99 fix. Four pre-existing tests that pinned the removed
client-side gate were rewritten to pin the new contract.
2026-08-17 06:05:29 -04:00
Joseph Doherty 8da60f57ac feat(health): KPI history + sustained-backlog badge for LocalDb replication (row 33)
LocalDbOplogBacklog was reported and Prometheus-exported with nothing consuming
it: not sampled into KPI history, so a purge-burst spike could not be reviewed
after the fact, and no threshold treatment anywhere. Both halves shipped.

KPI SAMPLING. SiteHealthKpiSampleSource now emits localDbOplogBacklog and
localDbReplicationConnected (0/1), and the backlog is charted on the Health
dashboard's per-site trend panel. Charting is the point: a point-in-time tile
can never answer 'did that spike drain?', which is the only thing separating an
expected purge burst from a stalled peer.

Both are CONDITIONALLY emitted rather than coerced with ?? 0 like the
auditBacklogPending metric beside them. Both health-report fields are nullable
and null means 'no reading' — replication not configured (rig site-b/site-c) or
a failed poll — never zero. A fabricated 0 would render a pair that cannot read
its own oplog, or one deliberately unreplicated, as perfectly healthy once a
minute forever, burying the real series. So a site emits 12 metrics when
unreplicated and 14 when replicated, and a GAP in these two series truthfully
means 'not replicated / not reporting'. Note false and null differ for the
connected flag: a configured link that is DOWN samples as 0; no link samples as
nothing. Both fold as Gauge (summing an oplog depth, or a 0/1 flag, per hour is
meaningless).

THRESHOLD SEMANTICS — sustained, not instantaneous. This is the design, not a
refinement. The site_events retention purge is CDC-captured like any other
write, so the backlog jumps by the deleted batch and drains as the peer acks
it; a bare 'backlog > threshold' badge would fire on every replicated site
every day and be ignored within a week. Healthy is defined by the backlog
RETURNING to ~0, which only a dwell can express. CentralHealthAggregator holds
LocalDbBacklogAboveSince — the instant the backlog first rose above
LocalDbBacklogThreshold (default 1000) — and the badge appears only after
LocalDbBacklogSustainedFor (default 15 min). A report at or below the threshold
clears the anchor; one above leaves the ORIGINAL instant intact, so the window
is sustained rather than sliding.

The null case is deliberately a third branch: a null backlog leaves the anchor
UNCHANGED rather than clearing it. Null is evidence neither of a drain nor of a
backlog, so a node going blind mid-climb must not silently reset its own dwell,
and an unreplicated site (null forever) never accumulates an anchor and can
never be badged.

Validation rejects a non-positive threshold (0 would badge every replicated site
permanently, since an idle backlog already sits at it) and a dwell shorter than
one ReportInterval (satisfiable by a single sample, which collapses the check
back into the bare threshold it replaces).

DOC CORRECTION worth flagging: this row's premise was 'mirror the SiteAuditBacklog
configurable threshold badge'. That badge does not exist — Component-HealthMonitoring.md
described it in two places but no option, no comparison and no markup were ever
implemented. The two stale claims are corrected here rather than left to send the
next reader looking for it; the LocalDb badge is built fresh on the Metrics-stale
badge's pattern, which is a real configurable-threshold badge.

Tests: HealthMonitoring 115 (9 new aggregator tests incl. purge-burst-drains,
unreplicated-never-anchors, null-mid-climb, exact-threshold, stale-report; 5 new
validator tests; 4 new sampler tests incl. the null-semantics and false-vs-null
cases); CentralUI 1042 (5 new badge/chart tests incl. the purge-burst negative
control); Commons Kpi 93; KpiHistory 54.
2026-08-15 13:02:35 -04:00
Joseph Doherty b5f120c370 feat(observability): honest stream-subscriber name + real data-connection gauges (row 54)
scadabridge_site_connection_up never counted data connections. It counts open
central->site gRPC subscriber streams (Debug View + the site-wide alarm feed),
so it correctly reads 0 on a healthy site nobody is watching — and the name
invited reading it as device health. That misreading has already cost a live
debugging session: register row 48's 'connection_up is 0 on all six site
nodes' was healthy steady state, not the fault it was diagnosed as.

Two halves, per the row:

RENAME. scadabridge.site.connection.up -> scadabridge.site.stream.subscribers.active
(helpers SiteConnectionOpened/Closed -> SiteStreamSubscriberOpened/Closed), and
the instrument gains the unit:"1" the other gauges already carried. Every
reference updated: the gRPC server, both test suites, Component-Communication.md,
components/Commons.md, docker/README.md and docker/seed-live-probe.sh. A test
now pins the new name AND asserts the old one is gone, so it cannot creep back.

NEW. scadabridge.site.data_connections.connected / .total — the first exported
data-connection signal (the state existed on the health report and in the CLI,
but never as a metric). connected < total is the alert condition; neither
number alone separates 'a device is down' from 'nothing is configured', which
is why the denominator ships with the numerator.

Sourcing is deliberate on two counts. It is fed by the DCL at its four state
transitions rather than derived from the health report, so the metric survives
a dead report pipeline — the exact failure the 'metrics stale' flag exists to
catch, and a derived gauge would go blind at the same moment. And it uses a
static registry reached through ScadaBridgeTelemetry (the SiteConnectionOpened
precedent) rather than a DI-injected holder: the DCL actors already take
ISiteHealthCollector through their constructors, and threading a second
dependency through both would ripple into every test that builds them. It also
keeps SiteHealthCollector untouched, which matters right now — another agent is
concurrently editing that file for the tag-resolution path.

Removal is wired too: an undeployed connection leaves the registry, so it stops
counting toward the total instead of latching connected < total on forever.

Both gauges read 0 on the standby (connections open on the active node only) —
healthy, documented, and called out in the docs so it does not become the next
site_connection_up.

No ObservedMeters allowlist change: all three instruments ride the existing
ZB.MOM.WW.ScadaBridge meter. (An unlisted meter exports nothing, silently.)

Tests: Commons 711 passed (9 new, delta-based against a baseline with unique
per-test connection names, so they do not inherit the register row 32
QueueDepthGaugeTests static-state flake); Communication 717; DCL 319.
2026-08-15 12:53:12 -04:00
Joseph Doherty 01bcca992c feat(alarms): thread an additive AckTime through the native-alarm mirror
MES alarm-status API §6.4 (docs/plans/2026-06-30-mes-alarm-status-api.md,
Phase 1 task 1). MES needs a real AckDT for a triggered alarm, and the mirror
carried acked-vs-unacked but never WHEN. AckTime now rides the whole path:
DCL transition -> AlarmStateChanged -> gRPC AlarmStateUpdate -> site SQLite.

Stamping rule, identical on both protocols: non-null ONLY while the condition
is active AND acknowledged. That single predicate yields all three required
behaviours -- null while unacked, cleared on re-raise (a re-raise arrives
unacknowledged), and no phantom ack on a return-to-normal. The last one is
load-bearing for MxGateway, which maps INACTIVE to Acknowledged = true; without
the active check every clear would claim an ack the system never observed.

Provenance is honest, never fabricated:
  - OPC UA A&C supplies a TRUE ack instant, so we now select it:
    AcknowledgeableConditionType/AckedState/TransitionTime at SelectClause
    index 18, APPENDED so the positional reads at 0-17 keep their meaning.
    Servers that omit the field fall back to the event's own Time.
  - MxAccess Gateway supplies none, so the ack transition's own timestamp is
    used -- accurate to when the system SAW the ack. An ACTIVE_ACKED
    re-subscribe snapshot restores one from LastTransitionTimestamp rather
    than dropping it.
The decision lives in pure mappers (Opc/Mx AlarmMapper.DeriveAckTime), so it is
unit-tested with no live server or gateway.

Additive-only throughout: init-only property on AlarmStateChanged, trailing
optional positional on NativeAlarmTransition (all 14-arg call sites untouched),
proto field 24 (never reusing a number) regenerated via docker/regen-proto.sh
sitestream with the csproj diff verified empty.

Persistence rides native_alarm_state's existing metadata_json blob rather than
a new column -- deliberately. That table is RegisterReplicated in
SiteLocalDbSetup and LocalDb builds its CDC triggers from the column list at
registration time, so an additive JSON property changes no schema, no triggers
and no replication contract; metadata_json is exactly the extension point UA4
introduced for this. Rows written before the field deserialize it as null.

Tests: 4 OPC UA + 6 MxGateway mapper cases, 3 NativeAlarmActor (emit,
failover rehydrate, pre-AckTime row), 1 proto round-trip incl. the null case,
4 Commons additive/back-compat. The OPC UA SelectClause count lock-in moves
18 -> 19 with an index-18 assertion -- intended, the clause is appended, which
is precisely what that guard exists to make visible.
2026-08-01 13:12:04 -04:00
Joseph Doherty 63c16d6912 refactor(comm): retire ClusterClient naming after the gRPC cutover
Phase 4 of the ClusterClient -> gRPC migration deleted the Akka transports
but left the naming behind: `ClusterClientSiteAuditClient` was transport-
agnostic and worked unchanged, so it survived the deletion under a name
that now describes a transport the repo no longer has. Same for a scatter
of doc-comments still framing gRPC as "the new transport" beside an Akka
one that is gone.

Renames it to `SiteCommunicationAuditClient` (and its test file) and
rewrites the stale comments to describe the single transport that exists.
Also tightens CLAUDE.md: drops the self-describing directory listing and
the 27-component enumeration in favour of the non-obvious parts only.

Behaviour-neutral: names and prose only. Recorded as the Phase 4
follow-up in docs/plans/2026-07-22-clusterclient-to-grpc-plan.md.
2026-07-27 15:40:01 -04:00
Joseph Doherty 0eb44314cb feat(dcl): add OpcUaReferenceForm.IsDurable — flags bare ns= bindings (v3 cutover #14) 2026-07-23 15:16:47 -04:00
Joseph Doherty f679d5c749 feat(cluster): manual central failover service — graceful Leave of the oldest Up member
Admin-triggered failover of the central pair. IManualFailoverService is declared in
CentralUI (plain strings, so that project stays Akka-free); AkkaManualFailoverService
implements it in the Host and is registered only in the Central branch.

- Leave, never Down: singletons hand over instead of being killed.
- Target = oldest Up member with the Central role, mirroring ActiveNodeEvaluator, so
  the node acted on is exactly the one hosting the singletons (never the leader,
  whose address-ordered definition diverges from singleton placement after a restart).
- Peer guard: returns null when fewer than 2 Up Central members — failing over a lone
  node is an outage, not a failover.
- Audited BEFORE the Leave is issued via ICentralAuditWriter: the acting node can be
  the one that goes away, and an audit written after could be lost to the shutdown it
  describes. Best-effort — audit failure never blocks the failover.

New audit taxonomy: AuditChannel.Cluster + AuditKind.ManualFailover (operator-initiated
topology actions are not script trust-boundary crossings, but are exactly what an audit
log exists to attribute). Lock-in tests updated 5->6 channels, 16->17 kinds.

alog.md §4 updated per the lock-in tests' contract. Both tables were already stale --
the Channel row omitted SecuredWrite and the Kind table claimed 10 while the code had
16 -- so they are completed here, not merely appended to.

ManualFailoverTests: 3 real-cluster tests (oldest leaves + survivor takes over, peer
guard refuses with a positive still-running assert, dry-run probe does not perturb).
2026-07-22 06:55:15 -04:00
Joseph Doherty cd93ad3976 fix(kpi): promote catalog metric literals to shared KpiMetrics constants — drift becomes a compile error (plan R2-04 T8) 2026-07-13 10:15:40 -04:00
Joseph Doherty 54417e1ae4 fix(kpi): per-metric bucket reduction — Rate series sum per bucket instead of keeping one delta (plan R2-04 T5) 2026-07-13 10:07:56 -04:00
Joseph Doherty ccdc649641 feat(kpihistory): classify KPI metrics gauge-vs-rate for hourly rollups (plan #22 T2)
Add KpiRollupAggregation { Gauge, Rate } enum + KpiMetricAggregationCatalog.Resolve(source, metric)
that the hourly rollup fold (T3) consults. Rate = sum-per-hour for per-interval delta counters;
Gauge = last-value-per-hour default (also the safe fallback for unknown/new metrics, never throws).
totalEventsLastHour/errorEventsLastHour classified Gauge (rolling trailing-hour counts, not
per-interval deltas) per authoritative source semantics.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 11:45:54 -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 e156c7fa8a test(commons): contract-lock tests for top ClusterClient records — enforce additive-only evolution under version skew (arch-review 08 §1.8)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 06:53:30 -04:00
Joseph Doherty 884f9127b2 test(commons): update AuditKind lock-in test to 16 members (SecuredWriteExpire from T15)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 06:16:07 -04:00
Joseph Doherty fb491a5342 feat(commons): additive SiteIdentifier on browse/verify/cert-trust commands for management-API routing (arch-review C4)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 05:04:18 -04:00
Joseph Doherty 2767e4bca1 feat(esg): per-system TimeoutSeconds column on ExternalSystemDefinition (spec Component-ExternalSystemGateway Timeout) — entity + EF + migration
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 03:57:49 -04:00
Joseph Doherty 22d5239039 test(commons): update AuditKind lock-in test for 15th member (ReconciliationAbandoned)
Task 17 (durable record for permanently-abandoned reconciliation rows) added
AuditKind.ReconciliationAbandoned but did not update the enum member-count
lock-in test AuditEnumTests, which asserted exactly 14 members. The per-task
filtered test runs did not include this Commons enum guard, so the gap surfaced
only in the PLAN-04 Task 26 full-solution sweep — exactly what the sweep exists
to catch. Rename to *FifteenExpectedMembers, add ReconciliationAbandoned to the
expected set, bump the count assertion to 15.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-09 14:57:28 -04:00
Joseph Doherty b58d810dbe fix(purge): short first tick on all three purge timers so daily-recycled nodes still purge 2026-07-09 09:26:42 -04:00
Joseph Doherty a61865daa0 feat(deploy): fetch options + per-deployment token helper 2026-06-26 12:28:48 -04:00
Joseph Doherty cdd65beb6c feat(cli+templateengine+deploymanager): resolve follow-ups #4/#5/#6/#8 — CLI ergonomics + structured deploy validation error
Closes the four remaining items in the 2026-06-24 template-inheritance/CLI
follow-up tracker.

#4 — CLI `instance set-bindings` can now set DataSourceReferenceOverride.
  `--bindings` accepts an optional 3rd element per entry:
  [attributeName, dataConnectionId, dataSourceReferenceOverride]. A string
  sets the override; a JSON null or an omitted 3rd element leaves it unset
  (template default). TryParseBindings accepts 2- or 3-element entries and
  rejects a non-string/non-null 3rd element or 4+ elements with a clean
  error. Previously the CLI sent the override as null and silently wiped any
  existing one (only a raw POST /management could set it).

#5 — `template update` is partial, not full-replace (fixed server-side so all
  clients benefit). UpdateTemplateAsync now uses leave-unchanged semantics:
  a null description keeps the stored value (pass "" to clear); a null
  parentTemplateId keeps the existing parent. Parent stays immutable — a
  non-null differing value is still rejected — but omitting --parent-id is
  now a no-op instead of failing every derived-template update.

#6 — compact `template list`/`get` table output + `--detail`. Table output is
  now id/name/description/parent/derived + member counts (#attrs/#alarms/
  #scripts/#comps/#nativeAlarms) via TemplateTableProjection, fed through a
  new optional tableProjector seam on CommandHelpers. `--detail` restores the
  full dump. JSON output is left untouched (always full) so machine consumers
  are unaffected — the projector only runs on the table path.

#8 — structured deploy-time validation error. New ValidationResult.SummarizeErrors()
  (Commons) returns a grouped, capped summary: leading total count, one line
  per ValidationCategory, and a per-module rollup (canonical name up to its
  last dot) with counts + "... and N more module(s)" caps. DeploymentService
  uses it for the "Pre-deployment validation failed" message and logs the full
  per-entry list via LogWarning. Replaces the flat semicolon-joined dump that
  became a wall of text for instances with 50-194 unbound attributes.

Tests: +8 Commons (SummarizeErrors), +8 CLI (4 binding 3-element / 4 table
projection), +2 net TemplateEngine (partial-update). Affected suites green:
Commons 587, CLI 341, TemplateEngine 447, DeploymentManager 101,
ManagementService 230, CentralUI 866; full solution builds 0/0.

Docs: Component-DeploymentManager.md "Validation Error Reporting"; CLI README
(set-bindings 3-element form, template update leave-unchanged, list/get
--detail); UpdateTemplateCommand doc; known-issues tracker #4/#5/#6/#8 resolved
(all 8 items now closed).
2026-06-24 18:27:42 -04:00
Joseph Doherty fd618cf1dc fix(review): full code-review remediation — 5 High + Medium/Low across 16 modules
Remediation from the full per-module code review at 4307c381 (findings recorded
separately in code-reviews/).

Highs fixed:
- DeploymentManager-025/SiteRuntime-031: stop broadcasting notification lists + SMTP
  configs (incl. credentials) to sites; site purges already-persisted rows on apply
  (enforces the central-only delivery design; clears plaintext SMTP creds at rest).
- DataConnectionLayer-023: guard the native-alarm subscribe path against the
  mid-flight-unsubscribe adapter-feed leak (mirrors the DCL-021 tag-path fix).
- SiteEventLogging-024: normalize From/To query bounds to UTC (the -016 fix the
  audit trail claimed but never committed).
- KpiHistory-001: add an in-flight guard to the recorder sample tick.
- ScriptAnalysis-001: harden the trust analyzer's TPA-absent fallback (resolve
  forbidden anchors in the minimal reference set; warn on degraded mode) — anchors
  added to validation references only, never the compile gate.
(InboundAPI-026 left to the feat/ipsen-movein effort per owner decision.)

Medium/Low: DM-026 deterministic deploy-status tiebreaker; SR-027/028/029/030
native-alarm leak/phantom-active/delete-during-redeploy fixes; AL-013/014/016;
TE-024 (folder-mutation audit rows now persisted)/025; SF-025 gauge-provider
clear-on-stop; ESG-025/026; SEC-023/024/025; SCA-007/008/009; plus doc/test
accuracy COM-023/024, HOST-025/026, HM-024/025, NS-027/028.

Full-solution build 0 warnings; ~3560 tests across 18 touched suites green.
2026-06-20 17:55:12 -04:00
Joseph Doherty a9393c8913 test(sms): regression tests for code-review fixes
Lock the behaviors changed by the review-fix commit + the security invariants:

- ManagementActorTests: UpdateSms/SmtpConfig now require Administrator (updated the
  existing success cases from Designer); + UpdateSmsConfig_WithDesignerRole_Returns
  Unauthorized and _WithEmptyAuthToken_PreservesExistingToken regression tests.
- SecretEncryptionTests: SmsConfiguration.AuthToken stored-encrypted round-trip +
  null round-trip (AccountSid stays plaintext) — guards ApplySecretColumnEncryption.
- ArtifactDiffTests: CompareSmsConfiguration New/Identical/Modified + the secret
  presence-only invariant (value never echoed, presence-flip shows <present> only).
- UpdateCommandContractTests: notification sms update core fields Required, --auth-token optional.
- NotificationListsPageTests: SMS recipient badge shows phone, not "Name <>".
- NotificationOutboxActorDispatchTests: SMS-typed notification routes to the SMS
  adapter (StubAdapter.Type made configurable), not the Email adapter.
- NotificationRecipientTests (new): ForEmail/ForSms + public-ctor invariants.
2026-06-19 15:09:47 -04:00
Joseph Doherty c5378f8723 feat(sms): NotificationType.Sms + recipient phone + SmsConfiguration + repo iface (S1) 2026-06-19 09:42:27 -04:00
Joseph Doherty 47f5ca687c refactor(commons): consolidate List element-type/coercion into AttributeValueCodec; InstanceActor + CLI reuse it (#93) 2026-06-19 02:03:09 -04:00
Joseph Doherty 5585d7ba51 test(#53): follow inline→Commons dedup — relocate OpcUa/MxGateway literal coverage to AlarmCapableProtocolsTests; pin TemplateEdit delegation 2026-06-19 01:42:37 -04:00
Joseph Doherty ddafc5c811 test(commons): realign enum-count guards (AuditKind/AuditChannel/DataType) + derace StaleTagMonitor timer tests (#228, pre-existing) 2026-06-19 00:33:39 -04:00
Joseph Doherty 71a2bca4df feat(m9/T32c): schema-library CRUD commands + handlers + Central UI page + read-accessor 2026-06-18 12:32:31 -04:00
Joseph Doherty 71d5722692 fix(m9/T32b): resolve $ref in InboundAPI runtime validators (no deploy-passes/runtime-400); diamond test; ref-annotation message 2026-06-18 12:16:39 -04:00
Joseph Doherty b3d99248fa feat(m9/T32b): JSON Schema $ref resolver (lib seam, cycle/depth-guarded) + deploy-time dangling-ref block 2026-06-18 11:54:19 -04:00
Joseph Doherty 4f1925870f feat(transport): name-map types + preview/selection/summary extensions (M8 A1) 2026-06-18 05:40:24 -04:00
Joseph Doherty 6a6f8949b9 fix(commons): OverrideCsvParser — preserve literal mid-field quotes, error on unterminated quoted field (T16 CSV) 2026-06-18 02:13:10 -04:00
Joseph Doherty 77a31ba994 feat(commons): quote-aware OverrideCsvParser (T16 CSV) 2026-06-18 02:01:38 -04:00
Joseph Doherty cb2a516187 refactor(kpi): K4/K10/K12 review fixups — test data-race + faulted-tick liveness, dead-branch/unused removal, NaN-guard assertions, value clamp + doc 2026-06-17 20:15:47 -04:00
Joseph Doherty 76f5ed72e4 feat(kpi): K10 — KpiSeriesBucketer last-per-bucket downsampler 2026-06-17 20:04:51 -04:00
Joseph Doherty 460777bffa feat(kpi): K1 — KpiSample + IKpiSampleSource + IKpiHistoryRepository contracts (Commons) 2026-06-17 19:40:17 -04:00
Joseph Doherty bf80ca1388 test(commons): NJ-1 review — backward-compat decode tests for old-form Float/DateTime + assert DateTime list is quoted-string array 2026-06-16 17:38:57 -04:00
Joseph Doherty 180d55482b feat(commons): native-typed JSON for List values; Decode reads both forms 2026-06-16 17:32:40 -04:00
Joseph Doherty 492b41f0fd fix(multivalue): Wave-2 review fixes (MV-2/MV-4/MV-12)
- MV-2: guard unsupported element type before parse (no misleading re-wrap); add Float round-trip test
- MV-4: carry ElementDataType through the two validation-flatten ResolvedAttribute sites (ManagementActor.HandleValidateTemplate, BundleImporter.BuildFlattenedConfigForValidation) so MV-5 validation sees element type via every entry point
- MV-12: include ElementDataType in TemplateAttribute add/update audit payloads + fix stale docstring
2026-06-16 15:33:27 -04:00
Joseph Doherty 8bd8079a7f feat(commons): AttributeValueCodec for canonical list value encode/decode 2026-06-16 15:21:56 -04:00
Joseph Doherty b3de8408fa feat(audit): ScadaBridge IAuditActorAccessor + wire audit Actor from Auth principal at authenticated emit sites (Phase 3) 2026-06-02 15:33:01 -04:00
Joseph Doherty db707bb0de feat(audit)!: ScadaBridge C3 — swap to canonical ZB.MOM.WW.Audit.AuditEvent across seams/emitters/DTO/redactor wiring; transitional 24-col storage shim (Task 2.5) 2026-06-02 12:37:50 -04:00
Joseph Doherty 3d77dc003c feat(audit): ScadaBridge C1 — AuditDetails codec (deterministic) + AuditOutcome projection + canonical field builders + ZB.MOM.WW.Audit ref (Task 2.5)
Additive foundation only — no existing type/interface/emitter changed.
Commons now references ZB.MOM.WW.Audit 0.1.0 (Gitea feed, central PM pin).
Adds four pure new types in Commons/Types/Audit/:
  AuditDetails (sealed record, 17 domain fields, declaration-order = JSON key order)
  AuditDetailsCodec (static; single cached JsonSerializerOptions: camelCase, no-indent,
    WhenWritingNull, UnsafeRelaxedJsonEscaping — byte-deterministic across calls)
  AuditOutcomeProjector (static; InboundAuthFailure→Denied first, then Delivered→Success,
    Failed/Parked/Discarded→Failure, all others→Success)
  AuditFieldBuilders (static; BuildAction="{channel}.{kind}", BuildCategory=channel.ToString())
56 new tests in Commons.Tests/Types/Audit/ covering codec round-trip, byte-determinism
(hand-pinned expected JSON string), null/empty sentinel, full projection table,
InboundAuthFailure-Denied precedence, and Action/Category builders. All pass.
2026-06-02 10:42:51 -04:00
Joseph Doherty afa55981d5 feat(auth)!: ScadaBridge retire SQL Server ApiKey entity + ApprovedApiKeyIds + legacy hashing; EF migration RetireInboundApiKeyStore; re-issue runbook + CHANGELOG (re-arch C5/E) — BREAKING: X-API-Key -> Bearer sbk_, keys re-issued 2026-06-02 05:39:59 -04:00
Joseph Doherty 731cfd3bfc feat(auth): ScadaBridge TransportExport excludes inbound API keys (re-arch C4; methods-only, import ignores legacy key sections); keys re-issued per environment 2026-06-02 05:06:40 -04:00
Joseph Doherty b1df6d5beb feat(commons): management command contracts for native alarm sources 2026-05-31 02:18:37 -04:00
Joseph Doherty 913441972e feat(commons): native alarm source entities + ResolvedNativeAlarmSource 2026-05-29 15:43:24 -04:00
Joseph Doherty ea14ace150 feat(commons): IAlarmSubscribableConnection seam + DCL native alarm messages 2026-05-29 15:41:10 -04:00
Joseph Doherty edc2dacf6c feat(commons): enrich AlarmStateChanged with unified condition state (additive) 2026-05-29 15:40:20 -04:00
Joseph Doherty 696da92c3a feat(commons): native alarm core types (AlarmConditionState, NativeAlarmTransition, enums) 2026-05-29 15:39:20 -04:00
Joseph Doherty 9b7916bb2e refactor(browse): rename BrowseOpcUaNode* to protocol-agnostic BrowseNode*
Renames BrowseOpcUaNodeCommand/Result -> BrowseNodeCommand/Result and
CommunicationService.BrowseOpcUaNodeAsync -> BrowseNodeAsync across Commons,
Communication, SiteRuntime, DCL actors, and CentralUI. Wire manifest name
follows (BrowseOpcUaNode -> BrowseNode). Browse regression tests green.
2026-05-29 07:57:36 -04:00
Joseph Doherty 19223a08cf feat(commons): MxGatewayEndpointConfig validator + tests 2026-05-29 07:46:28 -04:00