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:
@@ -218,18 +218,60 @@ public sealed class StatusSnapshotBuilderTests
|
||||
result.Service.ConfigReloadCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
// ── 7. BuildDebug: unknown PLC → empty, disarmed snapshot ────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDebug_UnknownPlc_ReturnsEmptyDisarmedSnapshot()
|
||||
{
|
||||
var (host, builder) = await BuildAsync([]);
|
||||
await using var _ = new AsyncHostDispose(host);
|
||||
|
||||
var debug = builder.BuildDebug("no-such-plc");
|
||||
|
||||
debug.CaptureArmed.ShouldBeFalse();
|
||||
debug.Tags.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
// ── 8. BuildDebug: configured PLC → one row per BCD tag, no traffic ──────
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDebug_ConfiguredPlc_ReturnsTagRows_DisarmedByDefault()
|
||||
{
|
||||
int port = PickFreePort();
|
||||
var (host, builder) = await BuildAsync([("PLC-A", port)], bcd16Address: 1072);
|
||||
await using var _ = new AsyncHostDispose(host);
|
||||
|
||||
await WaitForAsync(() => CanConnect(port), TimeSpan.FromSeconds(5), "PLC-A should bind");
|
||||
|
||||
var debug = builder.BuildDebug("PLC-A");
|
||||
|
||||
debug.CaptureArmed.ShouldBeFalse(); // no detail page open
|
||||
var tag = debug.Tags.ShouldHaveSingleItem();
|
||||
tag.Address.ShouldBe(1072);
|
||||
tag.Width.ShouldBe(16);
|
||||
tag.HasValue.ShouldBeFalse();
|
||||
tag.RawHex.ShouldBe("—");
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static async Task<(IHost host, StatusSnapshotBuilder builder)> BuildAsync(
|
||||
(string name, int port)[] plcs,
|
||||
int startupWaitMs = 200,
|
||||
int backendPort = 502)
|
||||
int backendPort = 502,
|
||||
int? bcd16Address = null)
|
||||
{
|
||||
var config = new Dictionary<string, string?>
|
||||
{
|
||||
["Mbproxy:AdminPort"] = "0", // disable admin for unit tests
|
||||
};
|
||||
|
||||
if (bcd16Address is { } addr)
|
||||
{
|
||||
config["Mbproxy:BcdTags:Global:0:Address"] = addr.ToString();
|
||||
config["Mbproxy:BcdTags:Global:0:Width"] = "16";
|
||||
}
|
||||
|
||||
for (int i = 0; i < plcs.Length; i++)
|
||||
{
|
||||
config[$"Mbproxy:Plcs:{i}:Name"] = plcs[i].name;
|
||||
|
||||
Reference in New Issue
Block a user