feat(scripts): realign Test Run with runtime API, add anonymous-object calls and instance binding

The Test Run sandbox and Monaco analysis modelled a script API that had
drifted from the site runtime's ScriptGlobals, so real scripts failed to
compile in Test Run. Realign both to the runtime surface
(Instance/Scripts/ExternalSystem/Attributes/Children/Parent) and drop the
duplicate ScriptHost stub so the two cannot diverge again.

- Script calls (Scripts.CallShared, Instance.CallScript, Route.To().Call)
  accept an anonymous object instead of a hand-built dictionary, via a
  shared ScriptArgs normalizer; existing dictionary calls still compile.
- Test Run can optionally bind to a deployed instance, so Instance/
  Attributes/CallScript route to it cross-site; adds site-side
  RouteToGetAttributes/RouteToSetAttributes handlers.
- Adds Test Run panels to the API method and template script editors.
- Fixes the TestDatabaseQuery seed script, which queried a table that
  never existed.

Also commits unrelated in-progress work already in the tree: the health
monitoring report loop, site streaming changes, and the Admin/Design
data-connection and SMTP page reorganization.
This commit is contained in:
Joseph Doherty
2026-05-16 03:37:56 -04:00
parent d7b05b40e9
commit 295150751f
50 changed files with 2926 additions and 550 deletions
@@ -14,8 +14,6 @@
<div class="container-fluid mt-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">Integration Definitions</h4>
<a class="btn btn-outline-secondary btn-sm"
href="/design/smtp">Email configuration →</a>
</div>
<ToastNotification @ref="_toast" />
@@ -67,15 +65,6 @@
Inbound API Methods <span class="badge bg-secondary">@_apiMethods.Count</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @(_tab == "apikeys" ? "active" : "")"
role="tab"
aria-selected="@(_tab == "apikeys" ? "true" : "false")"
aria-controls="int-tab-apikeys"
@onclick='() => _tab = "apikeys"'>
API Keys <span class="badge bg-secondary">@_apiKeys.Count</span>
</button>
</li>
</ul>
@if (_tab == "extsys")
@@ -94,10 +83,6 @@
{
<div role="tabpanel" id="int-tab-inbound">@RenderInboundApiMethods()</div>
}
else if (_tab == "apikeys")
{
<div role="tabpanel" id="int-tab-apikeys">@RenderApiKeys()</div>
}
}
</div>
@@ -122,14 +107,6 @@
? _dbConnections
: _dbConnections.Where(dc => dc.Name?.Contains(_dbConnSearch, StringComparison.OrdinalIgnoreCase) ?? false);
// API Keys
private List<ApiKey> _apiKeys = new();
private string _apiKeySearch = "";
private IEnumerable<ApiKey> FilteredApiKeys =>
string.IsNullOrWhiteSpace(_apiKeySearch)
? _apiKeys
: _apiKeys.Where(k => k.Name?.Contains(_apiKeySearch, StringComparison.OrdinalIgnoreCase) ?? false);
// Notification Lists
private List<NotificationList> _notificationLists = new();
private Dictionary<int, List<NotificationRecipient>> _recipients = new();
@@ -171,7 +148,6 @@
}
_apiMethods = (await InboundApiRepository.GetAllApiMethodsAsync()).ToList();
_apiKeys = (await InboundApiRepository.GetAllApiKeysAsync()).ToList();
}
catch (Exception ex) { _errorMessage = ex.Message; }
_loading = false;
@@ -478,67 +454,4 @@
catch (Exception ex) { _toast.ShowError(ex.Message); }
}
// ==== API Keys ====
private RenderFragment RenderApiKeys() => __builder =>
{
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">API Keys</h5>
</div>
@if (_apiKeys.Count == 0)
{
<div class="text-center py-5 text-muted">
<p class="mb-3">No API keys configured. Add your first API key from the Admin section.</p>
</div>
}
else
{
<div class="mb-3" style="max-width: 320px;">
<input class="form-control form-control-sm"
placeholder="Filter by name…"
@bind="_apiKeySearch" @bind:event="oninput" />
</div>
@if (!FilteredApiKeys.Any())
{
<p class="text-muted small">No API keys match the filter.</p>
}
<div class="row g-3">
@foreach (var key in FilteredApiKeys)
{
<div class="col-lg-6 col-12" @key="key.Id">
<div class="card h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="card-title mb-0">@key.Name</h5>
<span class="badge @(key.IsEnabled ? "bg-success" : "bg-secondary")">
@(key.IsEnabled ? "Enabled" : "Disabled")
</span>
</div>
<div class="d-flex gap-1">
<button class="btn btn-outline-primary btn-sm"
@onclick="() => ToggleApiKeyEnabled(key)">
@(key.IsEnabled ? "Disable" : "Enable")
</button>
</div>
</div>
</div>
</div>
}
</div>
}
};
private async Task ToggleApiKeyEnabled(ApiKey key)
{
try
{
key.IsEnabled = !key.IsEnabled;
await InboundApiRepository.UpdateApiKeyAsync(key);
await InboundApiRepository.SaveChangesAsync();
_toast.ShowSuccess($"API key '{key.Name}' {(key.IsEnabled ? "enabled" : "disabled")}.");
}
catch (Exception ex) { _toast.ShowError(ex.Message); }
}
}