fix(dashboard): keep the '-' placeholder on failed /browse reads instead of '[redacted]'

This commit is contained in:
Joseph Doherty
2026-08-18 05:18:47 -04:00
parent bf7b44c2d7
commit 1ea6f60ea2
2 changed files with 64 additions and 4 deletions
@@ -53,9 +53,12 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync
/// <summary>
/// <c>MxGateway:Dashboard:ShowTagValues</c>. False (the default)
/// substitutes <see cref="DashboardTagValue.RedactedValueText"/> 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.
/// </summary>
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);
}
@@ -237,6 +237,38 @@ public sealed class DashboardLiveDataServiceTests
Assert.Equal("Double", value.DataType);
}
/// <summary>
/// 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.
/// </summary>
[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
/// </summary>
public MxValue? ReadValue { get; set; }
/// <summary>Gets the tag addresses whose bulk read comes back unsuccessful.</summary>
public HashSet<string> FailReadFor { get; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>Gets the item handle bound for a previously subscribed tag.</summary>
/// <param name="tagAddress">Tag address to look up.</param>
/// <returns>The bound item handle.</returns>
@@ -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,