mxaccessgw 488649c+ acknowledges by AuthenticateUser as the operator and
writing <alarm>.AckMsg, so AcknowledgeAlarmRequest now needs a real Galaxy
operator_user + operator_domain + operator_password. The OPC UA data plane is
API-key/anonymous and carries no Galaxy password, so the driver resolves the
account per ack:
1. the session's API key's Galaxy ack identity, set on AdminUI /apikeys and
held ONLY in the encrypted Secrets store
(otopcua/apikeys/<keyId>/galaxy-ack-{user,password}); a key with a user but
no password secret is refused, never silently downgraded;
2. else the driver instance's AlarmAck fallback account (DriverConfig
FallbackUser/FallbackDomain/FallbackPasswordSecretRef; secret:/${secret:}/
env:/file:/dev: only — a literal is refused);
3. else DriverOperationRefusedException: nothing is sent, never an empty
operator_user. Refusals are excluded from the resilience retry/breaker.
- Pins ZB.MOM.WW.MxGateway.Client/Contracts 0.2.1 -> 0.3.0 (vendored into
nuget-packages/ from the Gitea feed, per the local-mxgw source mapping).
- The session ApiKeyId (not a secret) rides NativeAlarmAck ->
RouteNativeAlarmAck -> RouteAlarmAck -> AlarmAcknowledgeRequest; the password
is resolved in the driver and never travels through actor messages.
- The OPC UA principal + key id are appended to the AckMsg comment
("[OPC UA: <principal>, key <id>]") and logged, so a shared fallback
account does not lose who acknowledged.
- Password never logged: GalaxyAckCredential.ToString omits it, the
acknowledger scrubs it from every gateway message it echoes and withholds
any exception that echoes it; audit rows carry only the key id.
- AdminUI: /apikeys "Ack identity" editor (set/remove; delete-key clears it),
Galaxy driver form gains the fallback section.
- Synchronous OPC UA status for a refusal is follow-up #760 (the native ack
path is fire-and-forget after the Part 9 method returns).
Tests: request mapping (password populated), password absent from logs and
exception text (mutation-checked), key-mapped / fallback / refused paths at
resolver and driver level, breaker non-trip, key-id propagation through node
manager and DriverHostActor, AdminUI service + form round-trip.
Closes#749
Part 1 — rewire the three DriverType dispatch maps from hand-authored string
literals to the canonical DriverTypeNames constants (single source of truth):
- TagConfigEditorMap, TagConfigValidator (AdminUI)
- EquipmentTagConfigInspector (ControlPlane)
This FIXES the live drift ("TwinCat"->TwinCAT, "Focas"->FOCAS). The maps are
OrdinalIgnoreCase so behavior is identical; the value is no future drift.
Calculation is not added to the inspector (no CalculationTagDefinitionFactory.Inspect).
Added TagConfigDriverTypeNameGuardTests pinning the maps to the constants.
Part 2 — retire the routed /clusters/{id}/drivers driver-authoring flow (/raw
Waves A-C now cover driver/device/tag authoring end-to-end):
- Deleted DriverTypePicker, DriverEditRouter, the 8 *DriverPage shells, and the
ClusterDrivers list page.
- Removed the "Drivers" tab from ClusterNav. Routes /clusters/{id}/drivers* now
simply do not exist -> clean 404.
- The Forms/DeviceForms bodies, shared driver sections, and address pickers used
by the /raw modals are untouched.
Part 3 — updated PageAuthorizationGuardTests census: removed the 10 deleted
ConfigEditor pages + ClusterDrivers, updated group-count comments (ConfigEditor
21->11, AuthenticatedRead 17->16). Removed the now-dead Clusters.Drivers usings
from the 4 *DriverPageFormSerializationTests + the guard test.
Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
Extract the 8 typed driver pages into embeddable <Driver>DriverForm bodies
(channel/protocol config) and add per-driver <Driver>DeviceForm bodies
(connection endpoint -> Device.DeviceConfig, v3 endpoint split). New
DriverConfigModal + DeviceModal dispatch by DriverType (DriverTypeNames) for
the /raw tree; Test-connect inside DeviceModal builds the merged Driver+Device
config transiently via DriverDeviceConfigMerger. Routed pages now host the
form bodies and keep working. Round-trip + enum-serialization guard tests for
the Modbus driver form + Modbus device model; retargeted the existing
page-form serialization tests + the _jsonOpts converter guard to the forms.
Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
GalaxyDriverPage deserialized DriverConfig with case-sensitive camelCase opts, but the
persisted/seeded config is PascalCase (the runtime reads it case-insensitively). So all four
nested option records read as null -> FromRecord NRE (HTTP 500) on edit, and the form would
have shown defaults instead of the real config (risking a clobber on save). Fix: add
PropertyNameCaseInsensitive=true (matches the runtime) so real values load, plus null-coalesce
the nested records in FromRecord as defense-in-depth. Regression test asserts the seeded
PascalCase config loads its real values.