802366c2c6
Foundation for surfacing per-driver runtime state from the Server process to the Admin UI. #152 shipped GetAutoProhibitedRanges() as an in-process accessor; #154 makes it reachable across processes. Server side (HealthEndpointsHost): - New URL family: /diagnostics/drivers/{driverInstanceId}/{driverType}/{topic} - First wired topic: /diagnostics/drivers/{id}/modbus/auto-prohibited - Driver-agnostic at the URL level — future driver types add their own segments[3] cases (e.g. /diagnostics/drivers/{id}/s7/dropped-pdus). - 404 when the driver instance doesn't exist; 400 when the driver exists but isn't a Modbus driver (the per-type endpoint is wrong for this row). - Response shape is flat JSON (unitId / region / startAddress / endAddress / lastProbedUtc / bisectionPending) so consumers don't have to reference the Driver.Modbus assembly's ModbusAutoProhibition record. - Re-uses the existing HttpListener bound to localhost:4841 — same auth / reachability story as /healthz and /readyz. Admin side: - DriverDiagnosticsClient (Services/) — HttpClient wrapper that fetches the per-driver Modbus prohibition list. Returns null on 404/400 (driver missing or wrong type); throws on transport failures. - ModbusAutoProhibitionsResponse + ModbusAutoProhibitionRow flat DTOs — client doesn't take a dep on Driver.Modbus. - ModbusDiagnostics.razor at /modbus/diagnostics/{driverInstanceId} — table view with BISECTING (warning yellow) / ISOLATED (danger red) badges, relative timestamps (e.g. "5m ago"), Refresh button. Errors surface inline rather than swallowing. - HttpClient registration in Program.cs reads DriverDiagnostics:ServerBaseUrl from appsettings.json (default http://localhost:4841/ for same-host deployments). Tests (3 new in HealthEndpointsHostTests): - Diagnostics_ReturnsModbusAutoProhibitions_ForLiveDriver — registers a Modbus driver with a programmable transport that protects register 102, records the prohibition via a coalesced ReadAsync, hits the endpoint, asserts the returned JSON matches (unitId / region / start / end / pending). - Diagnostics_404_When_Driver_Not_Found - Diagnostics_400_When_Driver_Is_Wrong_Type Architecture note: the Admin-side bUnit-style component test isn't included because Admin.Tests doesn't have bUnit set up. The DriverDiagnosticsClient is unit-testable on its own with a mock HandlerStub if needed — left as a follow-up alongside the broader bUnit setup task. The diagnostic page is now reachable at /modbus/diagnostics/{driverId} from any Admin instance pointing at a Server endpoint URL. Future driver types (S7, AbCip) plug into the same channel by adding their own URL segments in HealthEndpointsHost.WriteDriverDiagnosticsAsync.
121 lines
4.0 KiB
Plaintext
121 lines
4.0 KiB
Plaintext
@page "/modbus/diagnostics/{DriverInstanceId}"
|
|
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
|
@inject DriverDiagnosticsClient Diagnostics
|
|
|
|
@*
|
|
#154 — operator-facing view of the Server's auto-prohibition state for a Modbus driver.
|
|
Fetches via DriverDiagnosticsClient (HttpClient against the Server's HealthEndpointsHost).
|
|
Refreshes on demand; auto-refresh is a future task once a SignalR diag channel exists.
|
|
*@
|
|
|
|
<PageTitle>Modbus diagnostics — @DriverInstanceId</PageTitle>
|
|
|
|
<div class="container py-4">
|
|
<h1>Modbus auto-prohibitions</h1>
|
|
<p class="text-muted">
|
|
Driver instance <code>@DriverInstanceId</code>. Live snapshot of coalesced ranges
|
|
the planner has learned to read individually (#148 / #150 / #151 / #152).
|
|
</p>
|
|
|
|
<div class="mb-3">
|
|
<button class="btn btn-sm btn-outline-primary" @onclick="LoadAsync" disabled="@_loading">
|
|
@(_loading ? "Loading…" : "Refresh")
|
|
</button>
|
|
@if (_lastRefreshed is not null)
|
|
{
|
|
<span class="text-muted ms-3 small">Last refreshed @_lastRefreshed.Value.ToLocalTime().ToString("HH:mm:ss")</span>
|
|
}
|
|
</div>
|
|
|
|
@if (_error is not null)
|
|
{
|
|
<div class="alert alert-danger">@_error</div>
|
|
}
|
|
else if (_response is null)
|
|
{
|
|
<p class="text-muted">Click <strong>Refresh</strong> to load.</p>
|
|
}
|
|
else if (_response.Count == 0)
|
|
{
|
|
<div class="alert alert-success">No auto-prohibitions. The planner is coalescing freely.</div>
|
|
}
|
|
else
|
|
{
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Unit</th>
|
|
<th>Region</th>
|
|
<th>Start</th>
|
|
<th>End</th>
|
|
<th>Span</th>
|
|
<th>Status</th>
|
|
<th>Last probed</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var r in _response.Ranges.OrderBy(r => r.UnitId).ThenBy(r => r.Region).ThenBy(r => r.StartAddress))
|
|
{
|
|
<tr>
|
|
<td><code>@r.UnitId</code></td>
|
|
<td><code>@r.Region</code></td>
|
|
<td><code>@r.StartAddress</code></td>
|
|
<td><code>@r.EndAddress</code></td>
|
|
<td>@(r.EndAddress - r.StartAddress + 1)</td>
|
|
<td>
|
|
@if (r.BisectionPending)
|
|
{
|
|
<span class="badge bg-warning text-dark">BISECTING</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="badge bg-danger">ISOLATED</span>
|
|
}
|
|
</td>
|
|
<td class="small text-muted">@FormatTimeSince(r.LastProbedUtc)</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
}
|
|
</div>
|
|
|
|
@code {
|
|
[Parameter] public string DriverInstanceId { get; set; } = string.Empty;
|
|
|
|
private ModbusAutoProhibitionsResponse? _response;
|
|
private string? _error;
|
|
private bool _loading;
|
|
private DateTime? _lastRefreshed;
|
|
|
|
private async Task LoadAsync()
|
|
{
|
|
_loading = true;
|
|
_error = null;
|
|
try
|
|
{
|
|
_response = await Diagnostics.GetModbusAutoProhibitedRangesAsync(DriverInstanceId);
|
|
_lastRefreshed = DateTime.UtcNow;
|
|
if (_response is null)
|
|
_error = $"Server reports driver '{DriverInstanceId}' is not present or is not a Modbus driver.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_error = $"Fetch failed: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
}
|
|
}
|
|
|
|
private static string FormatTimeSince(DateTime utc)
|
|
{
|
|
var span = DateTime.UtcNow - utc;
|
|
if (span.TotalSeconds < 60) return $"{(int)span.TotalSeconds}s ago";
|
|
if (span.TotalMinutes < 60) return $"{(int)span.TotalMinutes}m ago";
|
|
if (span.TotalHours < 24) return $"{(int)span.TotalHours}h ago";
|
|
return $"{(int)span.TotalDays}d ago";
|
|
}
|
|
}
|