MXAccess silently no-ops a whole-array write unless the item reference
ends in "[]" (e.g. "<object>.MoveInWorkOrderNumbers[]") — the COM Write
returns success but the value never commits. Reads work either way, so
the bug surfaced only on writes. Mirror the AVEVA MES Camstar API, which
registers array tags as "<object>.<attr>[]" (scalars have no brackets).
WriteAsync now resolves/advises/writes array values against tag + "[]"
(scalars unchanged), keeping the original tag for result mapping. Adds
IsArrayValue matching the ToMxValue/PadArrayToDeclaredSizeAsync array set.
Verified live via mxwrtest against the deployed gateway: bare ref write
ok but read-back unchanged; "[]" ref write commits (read-back changes,
fresh source timestamp). No RealMxGatewayClient unit harness exists (the
gRPC session is concrete) — consistent with how the sibling supervisory/
pad/encode fixes are verified.
An inbound /api array parameter was materialized as List<object?> whose
elements were raw System.Text.Json.JsonElement. When such a value is routed
Central->Site and a template script assigns it to a List-typed Galaxy
attribute (recv.Attributes[name] = Parameters[name]), the script-side encode
stalls (the attribute codec JSON-serializing JsonElement items) and the array
write never reaches the DCL — the Ipsen MoveIn array writes hung 30s while
scalars succeeded.
ParameterValidator.MaterializeArray now builds a strongly-typed list per the
declared element schema (List<string>/long/double/bool); arrays with no
declared scalar element type materialize each element to its CLR value
(MaterializeJsonValue) so no raw JsonElement survives. Typed lists serialize
cleanly across nodes and encode to a canonical JSON array, which the
InstanceActor decodes back to the typed list for the device write.
Even with correct array encoding (30d07b9), Ipsen MoveIn array writes still
hung: the Galaxy MES-receiver arrays are fixed-size SAFEARRAYs (e.g.
MoveInWorkOrderNumbers = SAFEARRAY(VT_BSTR) dimensions:[50]) and MXAccess only
accepts a write that supplies ALL slots. ScadaBridge sent just the N elements
the MES provided (1-2), so the COM write blocked. Verified on the live gateway:
a full-size (50) constructed array writes via WriteBulk in ~34ms; a short one
does not.
RealMxGatewayClient.WriteAsync now, for a list value, reads the tag's current
array to learn its slot count and pads the value to that length with
element-type defaults (empty string / 0 / false / default) — the caller's
values fill slots 0..N-1, the rest are cleared. The PLC reads the valid count
from a separate scalar (MoveInNumberWorkOrders). If the size can't be
determined (read fails / not an array) the value is written unpadded and a
warning is logged. Scalars are unaffected.
The Ipsen MoveIn e2e (after the supervisory-advise fix landed scalar writes)
exposed a second blocker: writes to List-typed attributes
(MoveInWorkOrderNumbers / MoveInPartNumbers, List<string>) hung at the 30s
device-write timeout while scalar writes succeeded.
InstanceActor.HandleSetDataAttribute already decodes a List attribute's
canonical JSON into a typed List<T> before the write (so the DCL can push a
real array), but RealMxGatewayClient.ToMxValue only had scalar cases — a
List<T> fell through to Convert.ToString and wrote the garbage string
"System.Collections.Generic.List`1[System.String]" to the array Galaxy
node, which the gateway's COM write rejected/blocked.
Add IReadOnlyList<bool|int|long|float|double|string|DateTimeOffset|DateTime>
cases that call the client package's typed array encoders
(VT_ARRAY|VT_BSTR etc.); List<DateTime> is mapped to DateTimeOffset. Covers
every element type AttributeValueCodec produces.
Writes through the MxGateway data connection (e.g. the Ipsen MoveIn flow
writing MES-receiver attributes) hung ~30s and changed nothing, while reads
of the same attributes worked. Root cause: MXAccess only accepts a write on
an item that holds a SUPERVISORY advise; the write path did AddItem +
WriteBulk with no advise and the monitoring subscription used a plain Advise,
so the worker's synchronous COM Write blocked until the gateway command
timeout. (Plain, non-secured writes need no user/login.) Verified live: with a
supervisory advise the write returns ok in ~22ms; without it it does not.
When the connection has no MXAccess write-user context (WriteUserId == 0) it
now behaves as a supervisory client: every advise defaults to
AdviseSupervisory — both the monitoring subscription (SubscribeAsync) and the
write path — so one connection can read and write. A supervisory advise still
delivers OnDataChange (the worker treats either advice kind as sufficient for
updates) so monitoring is unaffected, and the worker's UnAdvise tears down
either kind, so unsubscribe is unchanged. AdviseSupervisory is issued as a raw
MxCommandKind.AdviseSupervisory via the session's Invoke (the client package
exposes only plain Advise). The advise runs at most once per handle via a
Lazy<Task> so a concurrent first-time subscribe+write on the same new handle
both await the same advise (neither writes before it completes); a faulted
advise is evicted so the next write retries. Dropped on unsubscribe. A
configured non-zero WriteUserId keeps the prior plain-advise behaviour.
SAPID(+side) -> BTDB Machine.SAPID -> Code -> instance; inbound script does
the lookup via a new scoped read-only DB helper, then routes to a new T1
template script that gates on MoveInReadyFlag and writes the MoveIn to the
correct Left/Right MES receiver. -LT deferred.
- SiteCallAudit/ServiceCollectionExtensions.cs: drop "still deferred" note on relay; point to SiteCallAuditActor where it lives
- Transport/Import/BundleImporter.cs: update "Only LoadAsync implemented" to reflect all three phases shipped
- SiteRuntime/Scripts/AuditingDbCommand.cs: replace two M5-deferred redaction comments with accurate references to AuditLogOptions.PerTargetOverrides
- SiteRuntime/Scripts/ScriptRuntimeContext.cs: replace "M5 will layer redaction" note with accurate description of shipped redactor
- CentralUI/AuditLogPage.razor.cs: replace "Bundle C wires… no-op seam" with accurate description of HandleRowSelected implementation
- docs/plans/2026-05-24-transport-design.md §13: update from "CLI Deferred / not built in v1" to reflect shipped BundleCommands.cs; update Open Questions entry
- docs/plans/2026-05-24-transport.md: convert Out-of-Scope "Do NOT build CLI" reminder to a factual note that it shipped
- docs/plans/2026-05-24-transport.md.tasks.json: flip all 30 tasks from pending → done (entire Transport feature shipped)
Surface native (Galaxy/MxGateway) alarms by their object-relative reference
(e.g. "Z28061.HeartbeatTimeoutAlarm") instead of the gateway's full provider
reference ("Galaxy!<area>.<object>.<alarm>"). The area is already preserved in
Category and the object reference is globally unique within the galaxy, so the
full provider prefix added only noise to the alarm identity operators see.
MxGatewayAlarmMapper.MapTransition/MapSnapshot now set SourceReference from
SourceObjectReference, falling back to AlarmFullReference only when the gateway
omits the object reference. +2 mapper tests; full DCL suite green (158).
The MxGateway alarm mapper emits the SnapshotComplete framing sentinel with
empty SourceReference/SourceObjectReference. HandleAlarmTransitionReceived
routed every transition by prefix match against the subscriber's source, so
the empty-ref sentinel ('' .StartsWith("<src>.") == false) was dropped for
any specific source. The NativeAlarmActor buffers snapshot conditions and only
flushes them on SnapshotComplete, so statically-active native alarms delivered
only in the initial snapshot (no later live transition) never surfaced.
Broadcast the SnapshotComplete sentinel to all alarm subscribers (bypassing the
source match + type filter) so each NativeAlarmActor's snapshot swap completes.
Adds a regression test using the real empty-ref sentinel against a specific
(prefix) source.
DataConnectionActor seeded a tag's initial value by Tell-ing TagValueReceived
from HandleSubscribe's background task, which runs BEFORE HandleSubscribeCompleted
registers the instance's tags in _subscriptionsByInstance. HandleTagValueReceived's
fan-out then found no subscriber and dropped the value. A tag that soon gets a
data-change notification recovers, but a STATIC tag (e.g. an idle MES field that
never changes) was left Uncertain forever — the dropped seed was its only value.
Seeds now ride back on SubscribeCompleted and are delivered after registration,
reusing HandleTagValueReceived's generation guard, fan-out and quality accounting.
+1 regression test (DCL026).
List values now encode as native-typed JSON ([10,20], [true,false], ISO dates;
strings stay quoted) via AttributeValueCodec; Decode reads both native and the
earlier array-of-strings form for every element type. Already-persisted old-form
data is normalized on the fly: idempotent central startup normalizer
(ListValueNormalizer), active site-SQLite normalization on InstanceActor
override-load, and normalize-on-import in the bundle importer. Instance-override
writes now stamp ElementDataType (#93/M3). Full solution 0/0; feature-targeted
tests green. Plan: docs/plans/2026-06-16-native-typed-json.md.
NJ-6: full solution builds 0/0; feature-targeted tests green (Commons codec 38,
TemplateEngine InstanceService 17, ConfigDB normalizer 8, Transport serializer 12,
SiteRuntime InstanceActor 47). Component-Commons now describes the native-typed
List encoding + read-both decode + the three normalization paths. #93/M3 folded in.
- NJ-3: widen per-row catch to Exception (an STJ encode failure can't abort startup); drop dead null-guard already excluded by the SQL filter
- NJ-4: capture logger/instanceName in locals for the fire-and-forget normalize continuation (match the sibling pattern in this actor)
- NJ-5: emit a warn-log when a malformed List value is imported verbatim; thread an optional ILogger<BundleImporter> to the sync re-import site
Set existingOverride.ElementDataType and newOverride.ElementDataType from
templateAttr.ElementDataType in both the update and create branches of
SetAttributeOverrideAsync, so the persisted InstanceAttributeOverride row
always carries the element type for later central normalizer use (#93/M3).
DisableLogin only swapped the cookie auth scheme (AutoLoginAuthenticationHandler),
which covers the interactive UI. The CLI authenticates POST /management, the audit
REST endpoints, and the SignalR debug-stream hub with HTTP Basic, and each ran its
own hardcoded Basic->LDAP check that ignored DisableLogin. In a login-disabled (e.g.
no-LDAP) deployment that locked the CLI out: every call returned 401 AUTH_FAILED.
Add ManagementAuthenticator, which centralizes the management/CLI auth flow:
when ScadaBridge:Security:Auth:DisableLogin is true it synthesizes the same dev
principal as AutoLoginAuthenticationHandler (configured user, all roles, system-wide)
and bypasses Basic->LDAP; otherwise the unchanged Basic->LDAP flow runs. Wired into
ManagementEndpoints (delegates), AuditEndpoints (delegates), and DebugStreamHub
(bypass branch). +6 unit tests; ManagementService.Tests green (140).
Encode emits native-typed JSON ([10,20], [true,false], ISO dates); Decode reads
both old (array-of-strings) and new forms. Existing data normalized via an
idempotent central MS SQL startup normalizer, active site SQLite normalization in
the InstanceActor override-load path, and normalize-on-import for bundles.
Approved via brainstorming (Approach B, thorough).
First-class DataType.List (homogeneous list of a scalar ElementDataType) round-tripping
through authoring, flatten, site runtime, OPC UA read+write, gRPC streaming, validation,
management API, CLI, Transport bundles, and Central UI (TemplateEdit + InstanceConfigure).
Canonical AttributeValueCodec (JSON, invariant culture); in-memory typed List<T> vs
persisted/streamed JSON; idempotent migration; element type fixed by base. 255
feature-targeted tests; full solution builds 0/0. Plan: docs/plans/2026-06-16-multivalue-attribute.md.
When overriding a List attribute, render the shared AttributeListEditor
(whole-list replacement; element type fixed by the base, shown read-only via
ShowElementType=false) instead of the single-line input. Loading an existing
override decodes its JSON into rows (malformed -> empty); saving encodes rows to
canonical JSON with a pre-submit Decode round-trip guard surfacing element
errors inline. Clearing removes the InstanceAttributeOverride row
(repository-direct, mirroring native-alarm-source overrides). Non-List override
UX unchanged.