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,