diff --git a/mbproxy/src/Mbproxy/Proxy/Multiplexing/PlcMultiplexer.cs b/mbproxy/src/Mbproxy/Proxy/Multiplexing/PlcMultiplexer.cs
index d216eed..99e0761 100644
--- a/mbproxy/src/Mbproxy/Proxy/Multiplexing/PlcMultiplexer.cs
+++ b/mbproxy/src/Mbproxy/Proxy/Multiplexing/PlcMultiplexer.cs
@@ -913,17 +913,18 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi
// A cache hit bypasses the BCD pipeline, so the debug-view capture
// would otherwise never see cache-served reads. Replay the
- // observations captured when this entry was stored — re-stamped now,
- // since the client receives this value right now — so the detail
- // page reflects what the client actually gets. Entries stored while
- // no viewer was armed carry no observations; those tags self-heal on
- // the next cache miss.
+ // observations captured when this entry was stored, preserving each
+ // observation's ORIGINAL timestamp — the value the client receives is
+ // cache-aged (up to CacheTtlMs old), so the debug view must show its
+ // true age, not a misleading "just now". Entries stored while no
+ // viewer was armed carry no observations; those tags self-heal on the
+ // next cache miss.
if (_ctx.Capture is { IsArmed: true } capture &&
cached.CapturedTags is { Count: > 0 } cachedTags)
{
foreach (var obs in cachedTags)
capture.Record(obs.Address, obs.RawLow, obs.RawHigh,
- obs.DecodedValue, CaptureDirection.Read);
+ obs.DecodedValue, CaptureDirection.Read, obs.UpdatedAtUtc);
}
byte[] hitFrame = BuildCacheHitFrame(originalTxId, unitId, cached.PduBytes);
diff --git a/mbproxy/src/Mbproxy/Proxy/TagValueCapture.cs b/mbproxy/src/Mbproxy/Proxy/TagValueCapture.cs
index c1b97a0..77c125d 100644
--- a/mbproxy/src/Mbproxy/Proxy/TagValueCapture.cs
+++ b/mbproxy/src/Mbproxy/Proxy/TagValueCapture.cs
@@ -126,7 +126,15 @@ internal sealed class TagValueCapture
/// BCD-encoded low word as it sits on the PLC wire.
/// BCD-encoded high word (0 for a 16-bit tag).
/// Decoded binary integer the client reads/wrote.
- public void Record(ushort address, ushort rawLow, ushort rawHigh, int decoded, CaptureDirection direction)
+ ///
+ /// When the value was actually observed from the PLC. Defaults to "now" for a live
+ /// pipeline observation. The response-cache hit path passes the cached observation's
+ /// original timestamp so a cache-served read shows its true age in the debug view
+ /// rather than appearing freshly read.
+ ///
+ public void Record(
+ ushort address, ushort rawLow, ushort rawHigh, int decoded, CaptureDirection direction,
+ DateTimeOffset? observedAtUtc = null)
{
if (!_armed)
return;
@@ -137,7 +145,7 @@ internal sealed class TagValueCapture
ref _slots[idx],
new TagValueObservation(
_addresses[idx], _widths[idx], _names[idx], rawLow, rawHigh, decoded, direction,
- DateTimeOffset.UtcNow));
+ observedAtUtc ?? DateTimeOffset.UtcNow));
// A concurrent Disarm() may have flipped _armed (and cleared the slots) between
// the _armed check above and the write just made — which would strand a stale