feat(inbound-api): surface method compile errors to operators — api-method validate/recompile + compile state on get (ScadaBridge#75)
The data plane answers a broken inbound method with the generic "Script compilation failed for this method"; the Roslyn diagnostic reached only the central log, and the known-bad cache could not be observed or reset per method. - InboundScriptExecutor keeps the diagnostics with each known-bad record and exposes GetCompileState(name, currentScript): IsCompiled / IsKnownBad / LastCompileError, judged against the current script text (a record for older text is reported as not compiled yet). - Management: ValidateApiMethodCommand (compile the stored script, return ApiMethodCompileResult, register nothing, cache untouched) and RecompileApiMethodCommand (CompileAndRegister without re-sending the entity, drops the node's ApiMethodCache row, audited "Recompile"). Both Designer, like UpdateApiMethod; dispatched through the shared ApiMethodCompileOperations. GetApiMethod keeps the entity fields top-level and adds isCompiled, isKnownBad, lastCompileError and compileStateNode. Compile state is per central node and every result names the node. - CLI: api-method validate --id, api-method recompile --id. - Central UI: the API-method edit page shows the saved script's compile state and last error, with Validate and Recompile actions (same operations, same Designer gate, fresh DI scope). - RequiredRoleMatrixTests entries; ManagementSurfaceParityTests covers the new CLI leaves; ApiMethodCompileDiagnosticsTests + an executor CompileState test. - Docs: CLI README, Component-CLI/ManagementService/InboundAPI, components/*. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -9,6 +9,9 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi
|
||||
@using ZB.MOM.WW.ScadaBridge.ManagementService
|
||||
@using Microsoft.Extensions.DependencyInjection
|
||||
@using ScriptAnalysis = ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
|
||||
@inject IInboundApiRepository InboundApiRepository
|
||||
@@ -123,6 +126,65 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@if (_existing != null)
|
||||
{
|
||||
@* ScadaBridge#75: the data plane answers a broken method with a generic
|
||||
"Script compilation failed for this method"; operators read the real
|
||||
diagnostics here (same operations as CLI api-method get/validate/recompile). *@
|
||||
<fieldset class="mb-4" data-test="api-method-compile">
|
||||
<legend class="h6">Compilation (saved script)</legend>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
|
||||
@if (_compileStatus is null)
|
||||
{
|
||||
<span class="badge bg-secondary" data-test="api-method-compile-status">Unavailable on this node</span>
|
||||
}
|
||||
else if (_compileStatus.IsKnownBad)
|
||||
{
|
||||
<span class="badge bg-danger" data-test="api-method-compile-status">Compile failed</span>
|
||||
}
|
||||
else if (_compileStatus.IsCompiled)
|
||||
{
|
||||
<span class="badge bg-success" data-test="api-method-compile-status">Compiled</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary" data-test="api-method-compile-status">Not compiled yet</span>
|
||||
}
|
||||
@if (_compileStatus?.Node is { } node)
|
||||
{
|
||||
<span class="text-muted small">on node @node</span>
|
||||
}
|
||||
<button class="btn btn-outline-secondary btn-sm" data-test="api-method-validate"
|
||||
@onclick="ValidateSavedScriptAsync" disabled="@_compiling">Validate</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" data-test="api-method-recompile"
|
||||
@onclick="RecompileSavedScriptAsync" disabled="@_compiling">Recompile</button>
|
||||
</div>
|
||||
@if (_compileStatus?.LastCompileError is { } lastError)
|
||||
{
|
||||
<pre class="border border-danger-subtle rounded p-2 small mb-2 font-monospace text-danger" style="white-space: pre-wrap;">@lastError</pre>
|
||||
}
|
||||
@if (_compileResult != null)
|
||||
{
|
||||
<div class="small @(_compileResult.Success ? "text-success" : "text-danger")" data-test="api-method-compile-result">
|
||||
@_compileAction: @(_compileResult.Success ? "the saved script compiles." : "the saved script does not compile.")
|
||||
</div>
|
||||
@if (_compileResult.Errors.Count > 0)
|
||||
{
|
||||
<ul class="small text-danger mt-1 mb-0">
|
||||
@foreach (var error in _compileResult.Errors)
|
||||
{
|
||||
<li>@error</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
<div class="form-text">
|
||||
Checks the script as saved, not unsaved edits. Recompile re-registers it on this
|
||||
central node, clearing a stale compile failure.
|
||||
</div>
|
||||
</fieldset>
|
||||
}
|
||||
|
||||
@if (_formError != null)
|
||||
{
|
||||
<div class="text-danger small mb-2">@_formError</div>
|
||||
@@ -245,6 +307,10 @@
|
||||
private HashSet<string> _initialKeyIds = new(StringComparer.Ordinal);
|
||||
|
||||
private bool _showTestRun;
|
||||
private ApiMethodCompileStatus? _compileStatus;
|
||||
private ApiMethodCompileResult? _compileResult;
|
||||
private string? _compileAction;
|
||||
private bool _compiling;
|
||||
private bool _running;
|
||||
private Dictionary<string, object?> _paramValues = new();
|
||||
private ScriptAnalysis.SandboxRunResult? _runResult;
|
||||
@@ -277,6 +343,7 @@
|
||||
var keysForMethod = await ApiKeyAdmin.GetKeysForMethodAsync(_existing.Name);
|
||||
_initialKeyIds = new HashSet<string>(keysForMethod, StringComparer.Ordinal);
|
||||
_selectedKeyIds = new HashSet<string>(_initialKeyIds, StringComparer.Ordinal);
|
||||
_compileStatus = ApiMethodCompileOperations.GetStatus(MutationServices, _existing);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { _formError = ex.Message; }
|
||||
@@ -284,6 +351,35 @@
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private Task ValidateSavedScriptAsync() =>
|
||||
RunCompileActionAsync("Validate", (sp, _) => ApiMethodCompileOperations.ValidateAsync(sp, _existing!.Id));
|
||||
|
||||
private Task RecompileSavedScriptAsync() =>
|
||||
RunCompileActionAsync("Recompile", (sp, user) => ApiMethodCompileOperations.RecompileAsync(sp, _existing!.Id, user));
|
||||
|
||||
/// <summary>
|
||||
/// Runs a compile action under the same Designer gate as the management commands, in a
|
||||
/// fresh DI scope (the recompile writes an audit row), then refreshes the status badge.
|
||||
/// </summary>
|
||||
private async Task RunCompileActionAsync(
|
||||
string action, Func<IServiceProvider, string, Task<ApiMethodCompileResult>> operation)
|
||||
{
|
||||
if (_existing is null) return;
|
||||
_formError = null;
|
||||
_compiling = true;
|
||||
try
|
||||
{
|
||||
var user = await MutationAuth.RequireRoleAsync(Roles.Designer);
|
||||
await using var scope = MutationServices.GetRequiredService<IServiceScopeFactory>().CreateAsyncScope();
|
||||
_compileResult = await operation(scope.ServiceProvider, user);
|
||||
_compileAction = action;
|
||||
var saved = await scope.ServiceProvider.GetRequiredService<IInboundApiRepository>().GetApiMethodByIdAsync(_existing.Id);
|
||||
_compileStatus = saved is null ? null : ApiMethodCompileOperations.GetStatus(scope.ServiceProvider, saved);
|
||||
}
|
||||
catch (Exception ex) { _formError = ex.Message; }
|
||||
finally { _compiling = false; }
|
||||
}
|
||||
|
||||
private void ToggleKey(string keyId, bool isChecked)
|
||||
{
|
||||
if (isChecked) _selectedKeyIds.Add(keyId);
|
||||
|
||||
Reference in New Issue
Block a user