From 1ea6f60ea290a631bd007a547baf51865082e428 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 18 Aug 2026 05:18:47 -0400 Subject: [PATCH] fix(dashboard): keep the '-' placeholder on failed /browse reads instead of '[redacted]' --- .../Dashboard/DashboardLiveDataService.cs | 19 +++++-- .../DashboardLiveDataServiceTests.cs | 49 +++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardLiveDataService.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardLiveDataService.cs index d5bea3c..2239c65 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardLiveDataService.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardLiveDataService.cs @@ -53,9 +53,12 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync /// /// MxGateway:Dashboard:ShowTagValues. False (the default) /// substitutes for every - /// value this service hands the Browse panel; quality, data type, source - /// timestamp, and any error still describe the real read, so the panel - /// remains a diagnostic surface without being a value-disclosure one. + /// successfully read value this service hands the Browse panel; quality, + /// data type, source timestamp, and any error still describe the real + /// read, so the panel remains a diagnostic surface without being a + /// value-disclosure one. Failed reads keep their existing "-" placeholder: + /// there was no value to suppress, so claiming one was withheld would + /// misreport the failure. /// private readonly bool _showTagValues; @@ -124,9 +127,17 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync // The only place the /browse live-value gate is evaluated: the page // renders whatever ValueText it is handed, so a second check in the // view could only ever disagree with this one. + // + // A failed read is left alone: it has no value to suppress, and its + // ValueText is already the "-" placeholder. Substituting "[redacted]" + // there would tell the operator a value was withheld when the read + // never produced one — the Error column says why it failed, and the + // two must not contradict each other. DashboardTagValue[] values = results .Select(DashboardTagValue.FromBulkReadResult) - .Select(value => _showTagValues ? value : value with { ValueText = DashboardTagValue.RedactedValueText }) + .Select(value => _showTagValues || !value.Ok + ? value + : value with { ValueText = DashboardTagValue.RedactedValueText }) .ToArray(); return new DashboardLiveReadResult(values, null, session.SessionId, session.WorkerProcessId); } diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardLiveDataServiceTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardLiveDataServiceTests.cs index 6764124..9079e5f 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardLiveDataServiceTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardLiveDataServiceTests.cs @@ -237,6 +237,38 @@ public sealed class DashboardLiveDataServiceTests Assert.Equal("Double", value.DataType); } + /// + /// Verifies redaction is scoped to reads that actually produced a value: a + /// failed read keeps the "-" placeholder rather than claiming a value was + /// withheld, and its diagnostic still reaches the panel. Redacting it would + /// contradict the error the same row displays. + /// + [Fact] + public async Task ReadAsync_WhenReadFailedAndShowTagValuesFalse_LeavesPlaceholderUnredacted() + { + RecordingWorkerClient worker = new() + { + ReadValue = new MxValue { DataType = MxDataType.Double, DoubleValue = 42.5 }, + }; + worker.FailReadFor.Add("Bad.PV"); + await using FakeSessionManager sessionManager = new(worker); + await using DashboardLiveDataService service = CreateService(sessionManager, showTagValues: false); + + DashboardLiveReadResult result = await service.ReadAsync( + ["Bad.PV", "Tank_001.PV"], + CancellationToken.None); + + DashboardTagValue failed = result.Values.Single(value => value.TagAddress == "Bad.PV"); + Assert.False(failed.Ok); + Assert.Equal("-", failed.ValueText); + Assert.Equal("Simulated read failure.", failed.Error); + Assert.False(failed.QualityGood); + + // The successful row in the same read is still redacted. + DashboardTagValue succeeded = result.Values.Single(value => value.TagAddress == "Tank_001.PV"); + Assert.Equal(DashboardTagValue.RedactedValueText, succeeded.ValueText); + } + private static DashboardLiveDataService CreateService( ISessionManager sessionManager, bool showTagValues = false) @@ -387,6 +419,9 @@ public sealed class DashboardLiveDataServiceTests /// public MxValue? ReadValue { get; set; } + /// Gets the tag addresses whose bulk read comes back unsuccessful. + public HashSet FailReadFor { get; } = new(StringComparer.OrdinalIgnoreCase); + /// Gets the item handle bound for a previously subscribed tag. /// Tag address to look up. /// The bound item handle. @@ -499,6 +534,20 @@ public sealed class DashboardLiveDataServiceTests BulkReadReply readReply = new(); foreach (string tagAddress in tagAddresses) { + if (FailReadFor.Contains(tagAddress)) + { + readReply.Results.Add(new BulkReadResult + { + ServerHandle = RegisteredServerHandle, + TagAddress = tagAddress, + ItemHandle = _itemHandles.TryGetValue(tagAddress, out int failedHandle) ? failedHandle : 0, + WasSuccessful = false, + Quality = 0, + ErrorMessage = "Simulated read failure.", + }); + continue; + } + BulkReadResult readResult = new() { ServerHandle = RegisteredServerHandle,