mbproxy: close out the dashboard code-review minor findings

Resolves the remaining Minor items from the 2026-05-15 review so the
web-UI dashboard work has no open follow-ups: a real-HubConnection
end-to-end test for the SignalR feed, stable mbproxy.admin.broadcast.*
log-event names, keyboard/aria accessibility on the fleet table,
frontend JS hardening (URL-decode guard, NaN guards, shared util.js),
reconciler<->capture-registry coverage, throwing-sink and embedded-asset
tests, broadcaster polish, and a soft upper bound on AdminPushIntervalMs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-16 16:36:39 -04:00
parent 374eecd205
commit 0308490aef
21 changed files with 576 additions and 67 deletions
+2 -2
View File
@@ -119,9 +119,9 @@ Server-push cadence (milliseconds) for the admin dashboard's SignalR feed. Every
| Field | Type | Default | Range |
|-------|------|---------|-------|
| `AdminPushIntervalMs` | int | `1000` | `> 0` |
| `AdminPushIntervalMs` | int | `1000` | `1``60000` |
`MbproxyOptionsValidator` and `ReloadValidator` both reject values `<= 0`. The broadcaster additionally floors the effective interval at 100 ms. Source: `MbproxyOptions.AdminPushIntervalMs`.
`MbproxyOptionsValidator` and `ReloadValidator` both reject values outside `1``60000` ms — the upper bound is a soft guard against a typo (e.g. a seconds value pasted as milliseconds) that would make the "live" feed effectively non-live. The broadcaster additionally floors the effective interval at 100 ms. Source: `MbproxyOptions.AdminPushIntervalMs`.
## `Mbproxy.Plcs[]`
+1 -1
View File
@@ -302,7 +302,7 @@ The UI is a Bootstrap 5 single-page app served from embedded assets under `src/M
1. **App bar** — service version, formatted uptime, accepted-reload count, and a live SignalR connection-state pill.
2. **Aggregate strip** — six cards: listeners bound/configured, total connected clients, fleet PDU/s (rate derived client-side from successive snapshots), PLCs in `recovering`, total backend exceptions, fleet cache hit ratio. The recovering / exceptions cards highlight when non-zero.
3. **KPI table** — one row per configured PLC, Tier-1 columns only: PLC name, backend `host:listenPort`, state chip (`bound` green / `recovering` amber / `stopped` grey), clients, PDU/s, RTT ms, exception total, coalesce %, cache %, keepalive. The table is client-side filterable (name/host search, state, "problems only") and sortable. Clicking a row opens that PLC's detail page in a new tab.
3. **KPI table** — one row per configured PLC, Tier-1 columns only: PLC name, backend `host:listenPort`, state chip (`bound` green / `recovering` amber / `stopped` grey), clients, PDU/s, RTT ms, exception total, coalesce %, cache %, keepalive. The table is client-side filterable (name/host search, state, "problems only") and sortable — column headers are keyboard-operable (Tab to focus, Enter/Space to sort) and carry `aria-sort`. The PLC name is a link that opens that PLC's detail page in a new tab.
### Connection detail (`GET /plc/{name}`)
+43 -1
View File
@@ -138,6 +138,48 @@ Fires when the admin endpoint cannot bind its configured `AdminPort`. The servic
**Operator action:** change `Mbproxy:AdminPort` in `appsettings.json` to a free port. Hot-reload picks up the change; the admin endpoint rebinds without a service restart.
### mbproxy.admin.broadcast.snapshot.failed
**Level:** Error &middot; **EventId:** 72 &middot; **Source:** `src/Mbproxy/Admin/StatusBroadcaster.cs`
No structured properties; the exception is attached.
Fires when the live-dashboard push loop cannot build a status snapshot. The current push cycle is skipped; the loop retries on the next interval. The proxy data path is unaffected.
**Operator action:** none if isolated. A sustained rate means the status-snapshot builder is consistently throwing — capture the attached exception and investigate.
### mbproxy.admin.broadcast.fleet.failed
**Level:** Error &middot; **EventId:** 73 &middot; **Source:** `src/Mbproxy/Admin/StatusBroadcaster.cs`
No structured properties; the exception is attached.
Fires when the push loop fails to deliver the fleet snapshot to dashboard subscribers (a SignalR transport fault). The loop continues; per-PLC detail pushes are still attempted.
**Operator action:** none if isolated. Sustained occurrences mean the SignalR feed is unhealthy — the dashboard's "live" feed is stale even though the proxy is fine.
### mbproxy.admin.broadcast.detail.failed
**Level:** Error &middot; **EventId:** 74 &middot; **Source:** `src/Mbproxy/Admin/StatusBroadcaster.cs`
| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name whose detail push failed. |
Fires when the push loop fails to deliver a per-PLC detail snapshot to that PLC's detail-page subscribers. The loop continues with the remaining PLCs.
**Operator action:** none if isolated. Sustained occurrences for one `Plc` mean that PLC's detail page is not receiving live updates.
### mbproxy.admin.broadcast.loop.terminated
**Level:** Error &middot; **EventId:** 75 &middot; **Source:** `src/Mbproxy/Admin/StatusBroadcaster.cs`
No structured properties; the exception is attached.
Fires when the live-dashboard push loop itself terminates on an unhandled exception (not the expected cancellation at shutdown). The dashboard's live feed stops entirely until the admin endpoint is rebound (an `AdminPort` hot-reload restarts the loop).
**Operator action:** alert. The live feed is dead; capture the attached exception and restart the admin endpoint (toggle `Mbproxy:AdminPort`) or the service.
### mbproxy.shutdown.complete
**Level:** Information &middot; **EventId:** 80 &middot; **Source:** `src/Mbproxy/Diagnostics/ShutdownCoordinator.cs`
@@ -531,7 +573,7 @@ Each subsystem owns a single `*LogEvents.cs` static partial class with `[LoggerM
- `src/Mbproxy/Proxy/Cache/CacheLogEvents.cs` — response cache.
- `src/Mbproxy/Proxy/RewriterLogEvents.cs` — BCD rewriting and exception passthrough.
Lifecycle events (`startup.*`, `listener.*`, `admin.*`, `shutdown.*`, `config.reload.*`) live as private `[LoggerMessage]` declarations next to the class that emits them — see `ProxyWorker.cs`, `PlcListener.cs`, `PlcListenerSupervisor.cs`, `AdminEndpointHost.cs`, `ShutdownCoordinator.cs`, and `ConfigReconciler.cs`. New subsystems should follow the `*LogEvents.cs` pattern when they accumulate more than two events.
Lifecycle events (`startup.*`, `listener.*`, `admin.*`, `shutdown.*`, `config.reload.*`) live as private `[LoggerMessage]` declarations next to the class that emits them — see `ProxyWorker.cs`, `PlcListener.cs`, `PlcListenerSupervisor.cs`, `AdminEndpointHost.cs`, `StatusBroadcaster.cs` (the `admin.broadcast.*` family), `ShutdownCoordinator.cs`, and `ConfigReconciler.cs`. New subsystems should follow the `*LogEvents.cs` pattern when they accumulate more than two events.
## Related Documentation