diff --git a/docs/plans/2026-06-27-secured-writes-tag-selector.md b/docs/plans/2026-06-27-secured-writes-tag-selector.md new file mode 100644 index 00000000..48ad2849 --- /dev/null +++ b/docs/plans/2026-06-27-secured-writes-tag-selector.md @@ -0,0 +1,553 @@ +# 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 + { + 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; + +/// +/// Best-effort map from the MxGateway/Galaxy free-form attribute type name +/// (GalaxyAttribute.data_type_name, surfaced on BrowseNode.DataType) +/// to a ScadaBridge . 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. +/// +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(); + + public NodeBrowserDialogSelectionTests() + { + Services.AddSingleton(_browse); + _browse.BrowseChildrenAsync( + Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()) + .Returns(new BrowseNodeResult(Array.Empty(), Truncated: false, Failure: null)); + _browse.SearchAsync( + Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()) + .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(p => p + .Add(c => c.SiteId, "plant-a") + .Add(c => c.ConnectionName, "PLC-OPC") + .Add(c => c.OnNodeSelected, EventCallback.Factory.Create(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(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 `
` containing `node-search-input`/`node-search-button`) in: +```razor + @if (ShowSearch) + { +
+ + +
+ } +``` + +(b) Add parameters after line 114 (`[Parameter] public EventCallback OnCancelled …`): +```csharp + /// Additive: receives the full selected (incl. + /// DataType) on confirm, alongside the existing string-id . + /// Optional — existing callers that only wire OnSelected are unaffected. + [Parameter] public EventCallback OnNodeSelected { get; set; } + + /// 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. + [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 + /// The originating BrowseNode, retained so selection can emit the + /// full node (incl. DataType) via OnNodeSelected. + 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 +
+ +
+ + +
+
+``` + +**Step 3: Render the dialog once** — insert between line 91 (`` closing the `mt-3` submit-button div) and line 92 (`` closing `card-body`), inside the operator `Authorized` region: +```razor + +``` + +**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. diff --git a/docs/plans/2026-06-27-secured-writes-tag-selector.md.tasks.json b/docs/plans/2026-06-27-secured-writes-tag-selector.md.tasks.json new file mode 100644 index 00000000..8444baf4 --- /dev/null +++ b/docs/plans/2026-06-27-secured-writes-tag-selector.md.tasks.json @@ -0,0 +1,12 @@ +{ + "planPath": "docs/plans/2026-06-27-secured-writes-tag-selector.md", + "tasks": [ + {"id": 1, "subject": "Task 1: Surface DataType through MxGateway browse seam (DCL)", "status": "pending", "classification": "standard", "parallelizableWith": [2, 3, 5]}, + {"id": 2, "subject": "Task 2: Galaxy-type to DataType mapper (CentralUI)", "status": "pending", "classification": "small", "parallelizableWith": [1, 3, 5]}, + {"id": 3, "subject": "Task 3: NodeBrowserDialog additive OnNodeSelected + ShowSearch (CentralUI)", "status": "pending", "classification": "standard", "parallelizableWith": [1, 2, 5]}, + {"id": 4, "subject": "Task 4: Wire Browse button + data-type auto-fill into Secured Writes page", "status": "pending", "classification": "standard", "blockedBy": [1, 2, 3]}, + {"id": 5, "subject": "Task 5: Update component design docs", "status": "pending", "classification": "trivial", "parallelizableWith": [1, 2, 3]}, + {"id": 6, "subject": "Task 6: Build, targeted tests, live-smoke", "status": "pending", "classification": "standard", "blockedBy": [1, 2, 3, 4, 5]} + ], + "lastUpdated": "2026-06-27" +}