chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
@page "/drivers/focas/{InstanceId}"
|
||||
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
||||
@inject FocasDriverDetailService DetailSvc
|
||||
|
||||
<h1 class="mb-3">FOCAS driver <code>@InstanceId</code></h1>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<p>Loading…</p>
|
||||
}
|
||||
else if (_detail is null)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
No FOCAS driver instance with id <code>@InstanceId</code> was found.
|
||||
<div class="small text-muted mt-1">
|
||||
Either the id is wrong, or the instance's <code>DriverType</code> is not "Focas". The list of drivers per cluster draft is on the <a href="/clusters">Clusters</a> page.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3"><div class="card"><div class="card-body">
|
||||
<h6 class="text-muted mb-1">Name</h6>
|
||||
<div class="fs-5">@_detail.Instance.Name</div>
|
||||
</div></div></div>
|
||||
<div class="col-md-3"><div class="card"><div class="card-body">
|
||||
<h6 class="text-muted mb-1">Cluster</h6>
|
||||
<div class="fs-5"><code>@_detail.Instance.ClusterId</code></div>
|
||||
</div></div></div>
|
||||
<div class="col-md-3"><div class="card"><div class="card-body">
|
||||
<h6 class="text-muted mb-1">Namespace</h6>
|
||||
<div class="fs-5"><code>@_detail.Instance.NamespaceId</code></div>
|
||||
</div></div></div>
|
||||
<div class="col-md-3"><div class="card @(_detail.Instance.Enabled ? "border-success" : "border-secondary")"><div class="card-body">
|
||||
<h6 class="text-muted mb-1">Enabled</h6>
|
||||
<div class="fs-5">@(_detail.Instance.Enabled ? "Yes" : "No")</div>
|
||||
</div></div></div>
|
||||
</div>
|
||||
|
||||
@if (_detail.ParseError is not null)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>DriverConfig JSON failed to parse:</strong> @_detail.ParseError
|
||||
<div class="small text-muted mt-1">
|
||||
Falling back to raw-JSON view below; the per-section tables are hidden because the shape couldn't be projected.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (_detail.Config is not null)
|
||||
{
|
||||
<h2 class="h5 mt-4">Devices</h2>
|
||||
@if (_detail.Config.Devices is null || _detail.Config.Devices.Count == 0)
|
||||
{
|
||||
<p class="text-muted">No devices configured.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>HostAddress</th><th>DeviceName</th><th>Series</th></tr></thead>
|
||||
<tbody>
|
||||
@foreach (var d in _detail.Config.Devices)
|
||||
{
|
||||
<tr>
|
||||
<td><code>@d.HostAddress</code></td>
|
||||
<td>@(d.DeviceName ?? "—")</td>
|
||||
<td>@(string.IsNullOrEmpty(d.Series) ? "Unknown" : d.Series)</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
<h2 class="h5 mt-4">Tags</h2>
|
||||
@if (_detail.Config.Tags is null || _detail.Config.Tags.Count == 0)
|
||||
{
|
||||
<p class="text-muted">No tags configured.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="small text-muted">@_detail.Config.Tags.Count tag(s) configured.</p>
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>Name</th><th>Device</th><th>Address</th><th>DataType</th><th>Writable</th></tr></thead>
|
||||
<tbody>
|
||||
@foreach (var t in _detail.Config.Tags)
|
||||
{
|
||||
<tr>
|
||||
<td>@t.Name</td>
|
||||
<td><code class="small">@t.DeviceHostAddress</code></td>
|
||||
<td><code>@t.Address</code></td>
|
||||
<td>@t.DataType</td>
|
||||
<td>@(t.Writable ? "Yes" : "No")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
<h2 class="h5 mt-4">Driver behaviour</h2>
|
||||
<table class="table table-sm align-middle" style="max-width: 640px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width: 30%;">Probe</th>
|
||||
<td>
|
||||
@if (_detail.Config.Probe is { } probe)
|
||||
{
|
||||
<span class="badge @(probe.Enabled ? "bg-success" : "bg-secondary")">@(probe.Enabled ? "Enabled" : "Disabled")</span>
|
||||
<span class="ms-2 small text-muted">Interval: @(probe.Interval ?? "default")</span>
|
||||
}
|
||||
else { <span class="text-muted">default (enabled)</span> }
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Alarm projection</th>
|
||||
<td>
|
||||
@if (_detail.Config.AlarmProjection is { } ap)
|
||||
{
|
||||
<span class="badge @(ap.Enabled ? "bg-success" : "bg-secondary")">@(ap.Enabled ? "Enabled" : "Disabled")</span>
|
||||
<span class="ms-2 small text-muted">PollInterval: @(ap.PollInterval ?? "default")</span>
|
||||
}
|
||||
else { <span class="text-muted">disabled (default)</span> }
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Handle recycling</th>
|
||||
<td>
|
||||
@if (_detail.Config.HandleRecycle is { } hr)
|
||||
{
|
||||
<span class="badge @(hr.Enabled ? "bg-warning text-dark" : "bg-secondary")">@(hr.Enabled ? "Enabled" : "Disabled")</span>
|
||||
<span class="ms-2 small text-muted">Interval: @(hr.Interval ?? "default (01:00:00)")</span>
|
||||
}
|
||||
else { <span class="text-muted">disabled (default)</span> }
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
<h2 class="h5 mt-4">Host status</h2>
|
||||
@if (_detail.HostStatuses.Count == 0)
|
||||
{
|
||||
<div class="alert alert-secondary small">
|
||||
No <code>DriverHostStatus</code> rows yet for this instance. The Server publishes its first
|
||||
tick ~2 s after the driver starts — if this stays empty after a minute, check that the Server is running and the instance is in a published generation.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Node</th>
|
||||
<th>Host</th>
|
||||
<th>State</th>
|
||||
<th class="text-end" title="Consecutive failures">Fail#</th>
|
||||
<th>Breaker last opened</th>
|
||||
<th>Last recycled</th>
|
||||
<th>Last seen</th>
|
||||
<th>Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var r in _detail.HostStatuses)
|
||||
{
|
||||
<tr class="@(IsStale(r) ? "table-warning" : "")">
|
||||
<td><code>@r.NodeId</code></td>
|
||||
<td>@r.HostName</td>
|
||||
<td><span class="badge @StateBadge(r.State)">@r.State</span></td>
|
||||
<td class="text-end small">@r.ConsecutiveFailures</td>
|
||||
<td class="small">@FormatUtc(r.LastCircuitBreakerOpenUtc)</td>
|
||||
<td class="small">@FormatUtc(r.LastRecycleUtc)</td>
|
||||
<td class="small @(IsStale(r) ? "text-warning" : "")">@FormatAge(r.LastSeenUtc)</td>
|
||||
<td class="text-truncate small" style="max-width: 240px;" title="@r.Detail">@r.Detail</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
<h2 class="h5 mt-4">Raw DriverConfig JSON</h2>
|
||||
<pre class="small bg-light border p-3"><code>@_detail.Instance.DriverConfig</code></pre>
|
||||
|
||||
<div class="mt-4 small text-muted">
|
||||
Docs: <code>docs/drivers/FOCAS.md</code> (getting started) · <code>docs/v2/focas-deployment.md</code> (NSSM + pipe ACL) · <code>docs/drivers/FOCAS-Test-Fixture.md</code> (test coverage).
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public string InstanceId { get; set; } = string.Empty;
|
||||
|
||||
private FocasDriverDetail? _detail;
|
||||
private bool _loading = true;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
_loading = true;
|
||||
try { _detail = await DetailSvc.GetAsync(InstanceId, CancellationToken.None); }
|
||||
finally { _loading = false; }
|
||||
}
|
||||
|
||||
private static bool IsStale(FocasHostStatusRow r) =>
|
||||
DateTime.UtcNow - r.LastSeenUtc > TimeSpan.FromSeconds(30);
|
||||
|
||||
private static string StateBadge(string state) => state switch
|
||||
{
|
||||
"Running" => "bg-success",
|
||||
"Faulted" => "bg-danger",
|
||||
"Starting" => "bg-info",
|
||||
"Stopped" => "bg-secondary",
|
||||
_ => "bg-secondary",
|
||||
};
|
||||
|
||||
private static string FormatUtc(DateTime? utc) =>
|
||||
utc is null ? "—" : utc.Value.ToString("yyyy-MM-dd HH:mm 'UTC'");
|
||||
|
||||
private static string FormatAge(DateTime utc)
|
||||
{
|
||||
var age = DateTime.UtcNow - utc;
|
||||
if (age.TotalSeconds < 60) return $"{(int)age.TotalSeconds}s ago";
|
||||
if (age.TotalMinutes < 60) return $"{(int)age.TotalMinutes}m ago";
|
||||
if (age.TotalHours < 48) return $"{(int)age.TotalHours}h ago";
|
||||
return utc.ToString("yyyy-MM-dd HH:mm 'UTC'");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user