mbproxy: replace status page with a live SignalR web dashboard
The single auto-refreshing zero-JS status page gave operators a 25-column wall and no way to drill into one connection. This adds a Bootstrap fleet dashboard (filterable/sortable KPI table) and a per-PLC detail page with a real-time debug view of raw PLC-side BCD vs. decoded client-side values, streamed live over a SignalR feed. The debug view is fed by an on-demand per-tag value capture, armed only while a detail page is open. All assets (Bootstrap, SignalR client, fonts) are embedded so the UI works unchanged on firewalled networks; GET /status.json is untouched for scrapers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,17 +19,60 @@ internal sealed class StatusSnapshotBuilder
|
||||
private readonly ServiceCounters _serviceCounters;
|
||||
private readonly AssemblyVersionAccessor _version;
|
||||
private readonly ProxyWorker _proxyWorker;
|
||||
private readonly TagCaptureRegistry _captureRegistry;
|
||||
|
||||
public StatusSnapshotBuilder(
|
||||
IOptionsMonitor<MbproxyOptions> options,
|
||||
ServiceCounters serviceCounters,
|
||||
AssemblyVersionAccessor version,
|
||||
ProxyWorker proxyWorker)
|
||||
ProxyWorker proxyWorker,
|
||||
TagCaptureRegistry captureRegistry)
|
||||
{
|
||||
_options = options;
|
||||
_options = options;
|
||||
_serviceCounters = serviceCounters;
|
||||
_version = version;
|
||||
_proxyWorker = proxyWorker;
|
||||
_version = version;
|
||||
_proxyWorker = proxyWorker;
|
||||
_captureRegistry = captureRegistry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the connection-detail debug snapshot for one PLC: the last value observed
|
||||
/// for every configured BCD tag. Returns an empty, disarmed snapshot when
|
||||
/// <paramref name="plcName"/> is unknown (e.g. a detail page open for a PLC removed
|
||||
/// by hot-reload).
|
||||
/// </summary>
|
||||
public PlcDebugSnapshot BuildDebug(string plcName)
|
||||
{
|
||||
if (!_captureRegistry.TryGet(plcName, out var capture))
|
||||
return new PlcDebugSnapshot(CaptureArmed: false, Tags: Array.Empty<TagValueDto>());
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var tags = capture.Snapshot()
|
||||
.Select(o => ToTagDto(o, now))
|
||||
.ToList();
|
||||
|
||||
return new PlcDebugSnapshot(capture.IsArmed, tags);
|
||||
}
|
||||
|
||||
private static TagValueDto ToTagDto(TagValueObservation o, DateTimeOffset now)
|
||||
{
|
||||
bool hasValue = o.UpdatedAtUtc.HasValue;
|
||||
|
||||
string rawHex = !hasValue
|
||||
? "—"
|
||||
: o.Width == 32
|
||||
? $"0x{o.RawHigh:X4}{o.RawLow:X4}"
|
||||
: $"0x{o.RawLow:X4}";
|
||||
|
||||
return new TagValueDto(
|
||||
Address: o.Address,
|
||||
Width: o.Width,
|
||||
HasValue: hasValue,
|
||||
Direction: o.Direction == CaptureDirection.Write ? "write" : "read",
|
||||
RawHex: rawHex,
|
||||
DecodedValue: o.DecodedValue,
|
||||
UpdatedAtUtc: o.UpdatedAtUtc?.ToString("o"),
|
||||
AgeSeconds: o.UpdatedAtUtc is { } at ? (now - at).TotalSeconds : null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user