# Secured Writes — Tag Selector + Data-Type Auto-Fill Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.

**Goal:** On `/operations/secured-writes`, back the Tag path with the existing tag browser and auto-fill the Data type from the selected MxGateway tag.

**Architecture:** Plumb the MxGateway gateway's already-returned per-attribute `data_type_name` through the DCL browse seam into `BrowseNode.DataType` (currently dropped); make the shared `NodeBrowserDialog` emit the full selected node additively; wire a Browse button + a best-effort Galaxy-type→`DataType` mapper into the page. No new gRPC, DB/schema/migration, or message contracts.

**Tech Stack:** C#/.NET, Akka.NET (untouched here), Blazor Server (Bootstrap), bUnit + xUnit + NSubstitute for tests.

**Design doc:** `docs/plans/2026-06-27-secured-writes-tag-selector-design.md`

---

## Task ordering & parallelism

- **Tasks 1, 2, 3, 5** are mutually independent (different files) — dispatchable in parallel.
- **Task 4** depends on Tasks 1, 2, 3 (needs the dialog callback, the mapper, and the surfaced DataType).
- **Task 6** (verify) depends on everything.

---

### Task 1: Surface `DataType` through the MxGateway browse seam (DCL)

**Classification:** standard
**Estimated implement time:** ~4 min
**Parallelizable with:** Task 2, Task 3, Task 5

**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IMxGatewayClient.cs:21`
- Modify: `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealMxGatewayClient.cs:263-270`
- Modify: `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs:269-271`
- Test: `tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Adapters/MxGatewayDataConnectionTests.cs` (add one test after line 246)

**Step 1: Write the failing test**

Add after the existing `BrowseChildrenAsync_maps_children_and_truncated` test (after line 246) in `MxGatewayDataConnectionTests.cs`:

```csharp
    [Fact]
    public async Task BrowseChildrenAsync_surfaces_attribute_datatype()
    {
        var fake = new FakeMxGatewayClient
        {
            BrowseHandler = _ => (new List<MxBrowseChild>
            {
                new("Area1.Pump.Speed", "Speed", BrowseNodeClass.Variable, false, "Double"),
            }, false)
        };
        var adapter = NewAdapter(fake);
        await adapter.ConnectAsync(Details());

        var result = await adapter.BrowseChildrenAsync(null);

        Assert.Equal("Double", result.Children[0].DataType);
    }
```

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests --filter "FullyQualifiedName~BrowseChildrenAsync_surfaces_attribute_datatype"`
Expected: FAIL — compile error (the 5-arg `MxBrowseChild` ctor does not exist yet).

**Step 3: Write minimal implementation**

(a) `IMxGatewayClient.cs:21` — add the additive field:
```csharp
public record MxBrowseChild(string NodeId, string DisplayName, BrowseNodeClass NodeClass, bool HasChildren, string? DataType = null);
```

(b) `MxGatewayDataConnection.cs:269-271` — forward the new field:
```csharp
        var nodes = children
            .Select(c => new BrowseNode(c.NodeId, c.DisplayName, c.NodeClass, c.HasChildren, DataType: c.DataType))
            .ToList();
```

(c) `RealMxGatewayClient.cs:265-270` — pass the gateway's Galaxy type name (empty → null) when projecting attributes:
```csharp
                children.Add(new MxBrowseChild(
                    attr.FullTagReference,
                    attr.AttributeName,
                    BrowseNodeClass.Variable,
                    false,
                    string.IsNullOrEmpty(attr.DataTypeName) ? null : attr.DataTypeName));
```

(The object loop at lines 252-256 is left unchanged — objects are not leaves and carry no type.)

**Step 4: Run test to verify it passes**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests --filter "FullyQualifiedName~BrowseChildrenAsync"`
Expected: PASS (both the new test and the existing `_maps_children_and_truncated`, whose 4-arg ctors stay valid because `DataType` defaults to null).

**Step 5: Commit**

```bash
git add 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 \
        tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Adapters/MxGatewayDataConnectionTests.cs
git commit -m "feat(dcl): surface MxGateway attribute DataType through browse seam"
```

---

### Task 2: Galaxy-type → `DataType` mapper (CentralUI)

**Classification:** small
**Estimated implement time:** ~4 min
**Parallelizable with:** Task 1, Task 3, Task 5

**Files:**
- Create: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Operations/SecuredWriteDataTypeMapper.cs`
- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Components/SecuredWriteDataTypeMapperTests.cs` (create)

**Step 1: Write the failing test**

Create `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Components/SecuredWriteDataTypeMapperTests.cs`:

```csharp
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Operations;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components;

public class SecuredWriteDataTypeMapperTests
{
    [Theory]
    [InlineData("Boolean", DataType.Boolean)]
    [InlineData("bool", DataType.Boolean)]
    [InlineData("Integer", DataType.Int32)]
    [InlineData("int", DataType.Int32)]
    [InlineData("Int32", DataType.Int32)]
    [InlineData("Float", DataType.Float)]
    [InlineData("Double", DataType.Double)]
    [InlineData("String", DataType.String)]
    [InlineData("Time", DataType.DateTime)]
    [InlineData("  double  ", DataType.Double)] // trimmed + case-insensitive
    public void TryMap_known_galaxy_types_map(string galaxy, DataType expected)
    {
        Assert.True(SecuredWriteDataTypeMapper.TryMap(galaxy, out var dt));
        Assert.Equal(expected, dt);
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData("ElapsedTime")]
    [InlineData("SomeVendorType")]
    public void TryMap_unknown_or_blank_returns_false(string? galaxy)
    {
        Assert.False(SecuredWriteDataTypeMapper.TryMap(galaxy, out _));
    }
}
```

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests --filter "FullyQualifiedName~SecuredWriteDataTypeMapperTests"`
Expected: FAIL — compile error (`SecuredWriteDataTypeMapper` does not exist).

**Step 3: Write minimal implementation**

Create `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Operations/SecuredWriteDataTypeMapper.cs`:

```csharp
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Operations;

/// <summary>
/// Best-effort map from the MxGateway/Galaxy free-form attribute type name
/// (<c>GalaxyAttribute.data_type_name</c>, surfaced on <c>BrowseNode.DataType</c>)
/// to a ScadaBridge <see cref="DataType"/>. Used by the Secured Writes page to
/// pre-fill the data-type dropdown when a tag is picked. Unknown or blank names
/// return false so the operator's existing selection is left untouched. Galaxy
/// type names are free-form text, not a stable enum — keep this tolerant.
/// </summary>
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;
        }
    }
}
```

**Step 4: Run test to verify it passes**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests --filter "FullyQualifiedName~SecuredWriteDataTypeMapperTests"`
Expected: PASS (14 cases).

**Step 5: Commit**

```bash
git add src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Operations/SecuredWriteDataTypeMapper.cs \
        tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Components/SecuredWriteDataTypeMapperTests.cs
git commit -m "feat(central-ui): add Galaxy-type to DataType mapper for secured writes"
```

---

### Task 3: `NodeBrowserDialog` — additive `OnNodeSelected` + `ShowSearch` (CentralUI)

**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 1, Task 2, Task 5

**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/NodeBrowserDialog.razor` (params, search-box gate, selected-node tracking, Confirm emit)
- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Components/NodeBrowserDialogSelectionTests.cs` (create)

**Background (do not skip):** Selection is confirmed in `Confirm()` (the footer "Select" button), which fires `OnSelected(_selectedNodeId)`. `Select`/`SelectBrowseNode`/`UseManual` only set `_selectedNodeId`. So the new `OnNodeSelected` must fire from `Confirm()`, and the dialog must track the selected node (for its `DataType`).

**Step 1: Write the failing test**

Create `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Components/NodeBrowserDialogSelectionTests.cs` (mirrors the existing `NodeBrowserDialogSearchTests` harness; reuses the search path to select a typed node):

```csharp
using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Dialogs;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components;

public class NodeBrowserDialogSelectionTests : BunitContext
{
    private readonly IBrowseService _browse = Substitute.For<IBrowseService>();

    public NodeBrowserDialogSelectionTests()
    {
        Services.AddSingleton(_browse);
        _browse.BrowseChildrenAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(),
                Arg.Any<string?>(), Arg.Any<CancellationToken>())
            .Returns(new BrowseNodeResult(Array.Empty<BrowseNode>(), Truncated: false, Failure: null));
        _browse.SearchAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(new SearchAddressSpaceResult(
                Matches: new[]
                {
                    new AddressSpaceMatch(
                        new BrowseNode("ns=2;s=Pump1.Speed", "Speed", BrowseNodeClass.Variable, HasChildren: false, DataType: "Double"),
                        Path: "Devices/Pump1/Speed"),
                },
                CapReached: false, Failure: null));
    }

    [Fact]
    public void Confirm_emits_full_node_with_datatype_on_OnNodeSelected()
    {
        BrowseNode? captured = null;
        var cut = Render<NodeBrowserDialog>(p => p
            .Add(c => c.SiteId, "plant-a")
            .Add(c => c.ConnectionName, "PLC-OPC")
            .Add(c => c.OnNodeSelected, EventCallback.Factory.Create<BrowseNode>(this, n => captured = n)));
        cut.InvokeAsync(() => cut.Instance.ShowAsync("plant-a", "PLC-OPC", null));
        cut.Render();

        cut.Find("[data-test=node-search-input]").Input("Pump1");
        cut.Find("[data-test=node-search-button]").Click();
        cut.FindAll("[data-test=node-search-result] a")[0].Click();
        cut.Find(".modal-footer .btn-primary").Click();

        Assert.NotNull(captured);
        Assert.Equal("ns=2;s=Pump1.Speed", captured!.NodeId);
        Assert.Equal("Double", captured.DataType);
    }

    [Fact]
    public void ShowSearch_false_hides_search_box()
    {
        var cut = Render<NodeBrowserDialog>(p => p
            .Add(c => c.SiteId, "plant-a")
            .Add(c => c.ConnectionName, "PLC-MX")
            .Add(c => c.ShowSearch, false));
        cut.InvokeAsync(() => cut.Instance.ShowAsync("plant-a", "PLC-MX", null));
        cut.Render();

        Assert.Empty(cut.FindAll("[data-test=node-search-input]"));
    }
}
```

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests --filter "FullyQualifiedName~NodeBrowserDialogSelectionTests"`
Expected: FAIL — `OnNodeSelected` / `ShowSearch` parameters do not exist yet (compile error).

**Step 3: Write minimal implementation** — edit `NodeBrowserDialog.razor`:

(a) Gate the search input-group. Wrap lines 25-31 (the `<div class="input-group mb-3">…</div>` containing `node-search-input`/`node-search-button`) in:
```razor
                    @if (ShowSearch)
                    {
                        <div class="input-group mb-3">
                            <input class="form-control" data-test="node-search-input"
                                   @bind="_searchQuery" @bind:event="oninput"
                                   @onkeydown="SearchOnEnter"
                                   placeholder="Search the address space by name or path…" />
                            <button class="btn btn-outline-primary" data-test="node-search-button" @onclick="SearchAsync">Search</button>
                        </div>
                    }
```

(b) Add parameters after line 114 (`[Parameter] public EventCallback OnCancelled …`):
```csharp
    /// <summary>Additive: receives the full selected <see cref="BrowseNode"/> (incl.
    /// DataType) on confirm, alongside the existing string-id <see cref="OnSelected"/>.
    /// Optional — existing callers that only wire OnSelected are unaffected.</summary>
    [Parameter] public EventCallback<BrowseNode> OnNodeSelected { get; set; }

    /// <summary>When false, hides the address-space search box. Set false for protocols
    /// that don't implement IAddressSpaceSearchable (e.g. MxGateway), where search would
    /// always fail. Default true preserves OPC UA behaviour.</summary>
    [Parameter] public bool ShowSearch { get; set; } = true;
```

(c) Add a tracking field next to `_selectedNodeId` (after line 117):
```csharp
    private BrowseNode? _selectedNode;
```

(d) `TreeNode` — add a source reference (after the `DataType` property, ~line 149):
```csharp
        /// <summary>The originating BrowseNode, retained so selection can emit the
        /// full node (incl. DataType) via OnNodeSelected.</summary>
        public BrowseNode? Source { get; init; }
```

(e) `ToTreeNode` (lines 197-198) — capture the source:
```csharp
    private static TreeNode ToTreeNode(BrowseNode c) =>
        new(c.NodeId, c.DisplayName, c.NodeClass, c.HasChildren) { DataType = c.DataType, Source = c };
```

(f) `ShowAsync` — reset the tracker. After `_selectedNodeId = initialNodeId;` (line 176) add:
```csharp
        _selectedNode = null;
```

(g) `SelectBrowseNode` (line 309-314) — track the node. After `_manualNodeId = node.NodeId;` add:
```csharp
        _selectedNode = node;
```

(h) `Select` (line 316-321) — track via the tree node's source. After `_manualNodeId = node.NodeId;` add:
```csharp
        _selectedNode = node.Source;
```

(i) `UseManual` (line 349-352) — manual entry has no known type. After `_selectedNodeId = _manualNodeId.Trim();` add:
```csharp
        _selectedNode = null;
```

(j) `Confirm` (lines 354-359) — also emit the full node:
```csharp
    private async Task Confirm()
    {
        _isVisible = false;
        if (!string.IsNullOrWhiteSpace(_selectedNodeId))
        {
            await OnSelected.InvokeAsync(_selectedNodeId!);
            if (OnNodeSelected.HasDelegate)
                await OnNodeSelected.InvokeAsync(
                    _selectedNode ?? new BrowseNode(_selectedNodeId!, _selectedNodeId!, BrowseNodeClass.Variable, HasChildren: false));
        }
    }
```

**Step 4: Run test to verify it passes**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests --filter "FullyQualifiedName~NodeBrowserDialog"`
Expected: PASS — both the new `NodeBrowserDialogSelectionTests` and the existing `NodeBrowserDialogSearchTests` (search box still present by default; OnSelected still fires).

**Step 5: Commit**

```bash
git add src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Dialogs/NodeBrowserDialog.razor \
        tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Components/NodeBrowserDialogSelectionTests.cs
git commit -m "feat(central-ui): NodeBrowserDialog emits full node + optional ShowSearch"
```

---

### Task 4: Wire the Browse button + data-type auto-fill into the page (CentralUI)

**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** none
**Depends on:** Task 1, Task 2, Task 3

**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Operations/SecuredWrites.razor`

No new automated test: the auto-fill logic is unit-tested in Task 2 and the dialog emit in Task 3; the page itself is gated behind `AuthorizeView`/policies with several injected services, so it is verified by build (this task) + live-smoke (Task 6). Do NOT add a heavyweight bUnit page harness.

**Step 1: Add usings** — after line 7 (`@using …Commons.Types.Enums`) add:
```razor
@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Dialogs
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol
```

**Step 2: Replace the Tag path field (lines 59-63)** with an input-group + Browse button:
```razor
                        <div class="col-md-4">
                            <label class="form-label form-label-sm">Tag path</label>
                            <div class="input-group input-group-sm">
                                <input class="form-control form-control-sm" @bind="_formTagPath"
                                       placeholder="e.g. PLC1.Pump.Setpoint" data-test="secured-write-tagpath" />
                                <button class="btn btn-outline-secondary" type="button"
                                        disabled="@(!CanBrowse)"
                                        title="@(CanBrowse ? "Browse tags" : "Pick a site and MxGateway connection first")"
                                        @onclick="OpenBrowser" data-test="secured-write-browse">Browse…</button>
                            </div>
                        </div>
```

**Step 3: Render the dialog once** — insert between line 91 (`</div>` closing the `mt-3` submit-button div) and line 92 (`</div>` closing `card-body`), inside the operator `Authorized` region:
```razor
                    <NodeBrowserDialog @ref="_browserRef"
                                       SiteId="@_formSiteIdentifier"
                                       ConnectionName="@_formConnectionName"
                                       ShowSearch="false"
                                       OnNodeSelected="OnTagPicked" />
```

**Step 4: Add the `@code` members** (place with the other submit-form fields, e.g. just after `_formComment` around line 227):
```csharp
    private NodeBrowserDialog? _browserRef;

    private bool CanBrowse =>
        !string.IsNullOrWhiteSpace(_formSiteIdentifier)
        && !string.IsNullOrWhiteSpace(_formConnectionName);

    private async Task OpenBrowser()
    {
        if (!CanBrowse || _browserRef is null) return;
        await _browserRef.ShowAsync(_formSiteIdentifier, _formConnectionName, _formTagPath);
    }

    private void OnTagPicked(BrowseNode node)
    {
        _formTagPath = node.NodeId;
        if (SecuredWriteDataTypeMapper.TryMap(node.DataType, out var dt))
            _formValueType = dt.ToString();
    }
```

(`SecuredWriteDataTypeMapper` is in the same `…Pages.Operations` namespace — no using needed.)

**Step 5: Build to verify it compiles**

Run: `dotnet build src/ZB.MOM.WW.ScadaBridge.CentralUI/ZB.MOM.WW.ScadaBridge.CentralUI.csproj`
Expected: Build succeeded, 0 errors.

**Step 6: Commit**

```bash
git add src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Operations/SecuredWrites.razor
git commit -m "feat(central-ui): tag selector + data-type auto-fill on Secured Writes"
```

---

### Task 5: Update component design docs

**Classification:** trivial
**Estimated implement time:** ~3 min
**Parallelizable with:** Task 1, Task 2, Task 3

**Files:**
- Modify: `docs/requirements/Component-Security.md` (Secured Writes section)
- Modify: `docs/requirements/Component-DataConnectionLayer.md` (MxGateway browse type-info)

**Step 1: Security doc.** Find the Secured Writes section: `grep -n "Secured Write" docs/requirements/Component-Security.md`. Add a concise sentence to the operator-submit description: the Tag path is backed by the shared tag browser (`NodeBrowserDialog`, search hidden for MxGateway), and the Data type is auto-filled (best-effort, still editable) from the selected tag's Galaxy type via `SecuredWriteDataTypeMapper`.

**Step 2: DCL doc.** Find the browse type-info note: `grep -n "DataType\|type-info\|BrowseNode\|MxGateway" docs/requirements/Component-DataConnectionLayer.md`. Update the type-info note so it states that `BrowseNode.DataType` is now surfaced for **MxGateway** too (from `GalaxyAttribute.data_type_name`), not OPC-UA-only. Note `ValueRank`/`Writable` remain OPC-UA-only.

**Step 3: Commit**

```bash
git add docs/requirements/Component-Security.md docs/requirements/Component-DataConnectionLayer.md
git commit -m "docs: secured-writes tag selector + MxGateway browse DataType"
```

---

### Task 6: Build, targeted tests, and live-smoke

**Classification:** standard
**Estimated implement time:** ~6 min (+ image rebuild)
**Parallelizable with:** none
**Depends on:** Task 1, Task 2, Task 3, Task 4, Task 5

**Step 1: Build the touched projects**

```bash
dotnet build src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.csproj
dotnet build src/ZB.MOM.WW.ScadaBridge.CentralUI/ZB.MOM.WW.ScadaBridge.CentralUI.csproj
```
Expected: Build succeeded, 0 errors.

**Step 2: Run targeted tests** (the touched test projects only — per the targeted-tests convention, not the full suite)

```bash
dotnet test tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests --filter "FullyQualifiedName~MxGatewayDataConnectionTests"
dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests --filter "FullyQualifiedName~SecuredWriteDataTypeMapperTests|FullyQualifiedName~NodeBrowserDialog"
```
Expected: all PASS.

**Step 3: Rebuild the cluster image and live-smoke**

```bash
bash docker/deploy.sh
```
Then in the browser (Traefik LB at `http://localhost:9000`), as a user holding the Operator role, open `/operations/secured-writes`:
- Pick a site + MxGateway connection → the **Browse…** button enables.
- Click Browse → the dialog opens with **no search box**; expand an object, pick an attribute → the Tag path fills with the tag reference and the Data type pre-selects the mapped type (still editable).
- Tag path remains hand-editable; Submit still works.

**Step 4: Final commit (if any smoke-fix needed)** — otherwise nothing to commit. Report results.

---

## Done criteria

- DCL test surfaces `BrowseNode.DataType` for MxGateway; existing browse test still green.
- Mapper unit tests green (known types map; unknown/blank → false).
- Dialog test: `OnNodeSelected` emits the typed node; `ShowSearch=false` hides search; existing dialog tests green.
- CentralUI + DCL build clean.
- Live-smoke: Browse populates tag path and auto-fills data type on an MxGateway connection.
- Design docs updated.
