# Secured Writes — Tag Selector + Data-Type Auto-Fill (Design)

**Date:** 2026-06-27
**Component(s):** Central UI (#9), Data Connection Layer (#4); Security & Auth (#10) for the Secured Writes UX.
**Status:** Approved design — ready for implementation plan.

## Problem

On the Central UI Secured Writes page (`/operations/secured-writes`), an Operator submits a
two-person MxGateway secured write. Today the **Tag path** is a free-text input and the **Data
type** is a manually-chosen dropdown defaulting to `Boolean`. Operators must know the exact
MxGateway tag reference and the correct data type by hand — error-prone, and a wrong data type
only surfaces at *approval* time (the value can't be decoded/written).

We want:
1. The **Tag path** field to be backed by a **tag selector** (browse the connection's tags).
2. The **Data type** to be **auto-selected from the chosen tag**.

## Key insight — the data type is already available

Secured writes are **MxGateway-only** (the page filters connections to `Protocol == "MxGateway"`,
and the ManagementActor enforces the same on submit). The MxGateway gateway *already returns* a
per-attribute `data_type_name` (free-form Galaxy text, e.g. `"Float"`, `"Integer"`, `"Boolean"`)
on its browse response (`GalaxyAttribute.data_type_name` in `galaxy_repository.proto`). ScadaBridge
currently **discards** it: `RealMxGatewayClient.BrowseChildrenAsync` reads only
`FullTagReference` + `AttributeName` when building `MxBrowseChild`.

So the feature is mostly *plumbing an existing value through* + reusing the existing
`NodeBrowserDialog`. No new gRPC, no DB/schema/migration, no new cross-cluster message contracts.

Reused as-is:
- `NodeBrowserDialog` / `TreeRow` tag browser (already used by `InstanceConfigure`,
  `TestBindingsDialog`).
- The full browse call path: `IBrowseService` → `CommunicationService.BrowseNodeAsync` →
  `SiteCommunicationActor` → `DeploymentManager` → `DataConnectionManagerActor` →
  `DataConnectionActor` → adapter.
- `BrowseNode` already carries an optional `DataType` field.

## UX decisions (confirmed)

- **Tag field:** editable text field **+ a "Browse…" button** (hybrid, matching
  `InstanceConfigure`). Operators can still paste a known reference; the browser populates the field.
- **Data type:** **auto-filled from the tag but left editable**. The Galaxy type name is free-form;
  the map to ScadaBridge's `DataType` enum is best-effort, so an unmapped type must leave the
  operator able to choose.

## Design

### Layer 1 — Surface `DataType` from MxGateway browse (DCL)

- `Adapters/IMxGatewayClient.cs`: extend the seam record additively —
  `record MxBrowseChild(string NodeId, string DisplayName, BrowseNodeClass NodeClass, bool HasChildren, string? DataType = null)`.
- `Adapters/RealMxGatewayClient.cs` (`BrowseChildrenAsync`): when projecting
  `parentObject.Attributes`, pass `attr.DataTypeName` (normalise empty → `null`) into `MxBrowseChild`.
- `Adapters/MxGatewayDataConnection.cs`: forward it —
  `new BrowseNode(c.NodeId, c.DisplayName, c.NodeClass, c.HasChildren, DataType: c.DataType)`.
- Test fake `FakeMxGatewayClient`: update to the new record shape and emit a sample `DataType` so
  tests can assert end-to-end surfacing.

`ValueRank`/`Writable` remain `null` for MxGateway (not needed here). Array attributes
(`is_array`/`array_dimension`) are **explicitly out of scope** — a secured write to an array via
this form is an edge case; the operator can pick `List` manually.

### Layer 2 — Let the browser emit the full node (shared dialog)

`NodeBrowserDialog` currently emits only `OnSelected(string nodeId)`. Add, additively:
- `EventCallback<BrowseNode> OnNodeSelected` — invoked alongside the existing `OnSelected` in
  `SelectBrowseNode()`. The internal tree node retains its source `BrowseNode` so the emit is
  faithful (NodeId + DataType). Existing callers are unaffected (the callback is optional).
- `bool ShowSearch = true` — set `false` from Secured Writes. MxGateway does **not** implement
  `IAddressSpaceSearchable`, so the search box would always return a `NotBrowsable` failure;
  hiding it avoids a guaranteed-broken affordance. Default `true` keeps OPC UA callers unchanged.

### Layer 3 — Wire the form (`Components/Pages/Operations/SecuredWrites.razor`)

- Tag path becomes an input-group: the existing editable `<input>` + a `Browse…` button,
  `disabled` unless a site **and** an MxGateway connection are selected
  (`CanBrowse`, mirroring `InstanceConfigure`'s `canBrowse`).
- Render one `<NodeBrowserDialog @ref="_browserRef" SiteId="@_formSiteIdentifier"
  ConnectionName="@_formConnectionName" ShowSearch="false" OnNodeSelected="OnTagPicked" />`.
- `OpenBrowser()` → `await _browserRef.ShowAsync(_formSiteIdentifier, _formConnectionName, _formTagPath)`
  (seeds with whatever is already typed).
- `OnTagPicked(BrowseNode node)`:
  ```csharp
  _formTagPath = node.NodeId;
  if (SecuredWriteDataTypeMapper.TryMap(node.DataType, out var dt))
      _formValueType = dt.ToString();
  // else: leave _formValueType unchanged — operator picks
  ```
- The Data type `<select>` is unchanged (no `disabled`): pre-filled but editable. `CanSubmit`
  is unchanged.

### Layer 4 — Galaxy type → `DataType` mapping

A small internal, unit-testable static class in CentralUI. Case-insensitive, whitespace-trimmed:

| Galaxy `data_type_name` | ScadaBridge `DataType` |
| --- | --- |
| `boolean`, `bool` | `Boolean` |
| `integer`, `int`, `int32` | `Int32` |
| `float` | `Float` |
| `double` | `Double` |
| `string` | `String` |
| `time` | `DateTime` |
| anything else / null / empty | *no map — operator's selection kept* |

```csharp
internal static class SecuredWriteDataTypeMapper
{
    public static bool TryMap(string? galaxyTypeName, out DataType dataType)
    {
        dataType = default;
        if (string.IsNullOrWhiteSpace(galaxyTypeName)) return false;
        switch (galaxyTypeName.Trim().ToLowerInvariant())
        {
            case "boolean": case "bool":              dataType = DataType.Boolean;  return true;
            case "integer": case "int": case "int32": dataType = DataType.Int32;    return true;
            case "float":                             dataType = DataType.Float;    return true;
            case "double":                            dataType = DataType.Double;   return true;
            case "string":                            dataType = DataType.String;   return true;
            case "time":                              dataType = DataType.DateTime; return true;
            default:                                  return false;
        }
    }
}
```

`Binary` and `List` are intentionally not auto-mapped (no clean Galaxy source); operator picks them.

## Error handling / edge cases

- Browse button disabled until site + MxGateway connection chosen.
- Gateway returns no type (older gateway / read gap) → `DataType` null → mapper returns false →
  dropdown keeps its value; operator picks. Graceful.
- Unmapped Galaxy type → same graceful fallback; auto-fill stays editable so a mis-map is fixable.
- MxGateway search box hidden (`ShowSearch=false`) — tree browse + expand only.

## Testing

- **Unit:** `SecuredWriteDataTypeMapper` — every known name (incl. casing/whitespace) → expected
  enum; `null` / `""` / exotic → `false`.
- **DCL:** assert a browsed MxGateway attribute's `DataTypeName` survives to `BrowseNode.DataType`
  through `MxGatewayDataConnection` (via the updated `FakeMxGatewayClient`).
- **Build + targeted tests** for touched projects (DCL, DCL tests, CentralUI) — not the full suite.
- **Live-smoke:** rebuild the image (`bash docker/deploy.sh`) and exercise the page against an
  MxGateway connection.

## Docs to update (spec travels with code)

- Secured Writes section under **Security & Auth** — tag selector + data-type auto-fill UX.
- MxGateway browse **type-info** note under **Data Connection Layer** — `DataType` now surfaced
  for MxGateway (previously OPC-UA-only).

## Out of scope

- Array attribute handling (`is_array`) → `List`.
- OPC UA / non-MxGateway connections (Secured Writes is MxGateway-only).
- MxGateway address-space search (adapter does not implement `IAddressSpaceSearchable`).
- Any DB schema, migration, or new message contract (`ValueType` already exists on
  `SubmitSecuredWriteCommand`).

## Files touched

- `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IMxGatewayClient.cs`
- `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealMxGatewayClient.cs`
- `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs`
- `FakeMxGatewayClient` (DCL test fake)
- `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/NodeBrowserDialog.razor`
- `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Operations/SecuredWrites.razor`
- new `SecuredWriteDataTypeMapper` + unit test
- Component design docs (Security & Auth, Data Connection Layer)
