@page "/script-log" @* Live script-log tail via SignalR. Subscribes to /hubs/script-log and shows entries from VirtualTagActor / ScriptedAlarmActor script execution. Engine emit lands with F8 + F9. *@ @attribute [Microsoft.AspNetCore.Authorization.Authorize] @rendermode RenderMode.InteractiveServer @using Microsoft.AspNetCore.SignalR.Client @using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs @using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging @inject NavigationManager Nav @implements IAsyncDisposable

Script log

@(_connected ? "live" : "disconnected")
Live tail of script-logs DPS topic, capped at @Capacity entries. Filter by minimum level + script ID. Sources: VirtualTagActor (F8), ScriptedAlarmActor (F9).
@if (VisibleRows.Count == 0) {
@if (_rows.Count == 0) { No script-log entries yet. Engine emit (F8/F9) is pending. } else { No entries match the current filter (@_rows.Count entries available). }
} else {
Showing @VisibleRows.Count of @_rows.Count
@foreach (var e in VisibleRows) { }
Time Level Script Context Message
@e.TimestampUtc.ToString("HH:mm:ss.fff") @e.Level @e.ScriptId @if (!string.IsNullOrEmpty(e.VirtualTagId)) { vtag=@e.VirtualTagId } @if (!string.IsNullOrEmpty(e.AlarmId)) { alarm=@e.AlarmId } @if (!string.IsNullOrEmpty(e.EquipmentId)) { eq=@e.EquipmentId } @e.Message
} @code { private const int Capacity = 500; private readonly List _rows = new(); private HubConnection? _hub; private bool _connected; private string _levelFilter = ""; private string _scriptFilter = ""; private static readonly Dictionary LevelRank = new(StringComparer.OrdinalIgnoreCase) { ["Trace"] = 0, ["Debug"] = 1, ["Information"] = 2, ["Warning"] = 3, ["Error"] = 4, ["Critical"] = 5, }; private List VisibleRows { get { IEnumerable q = _rows; if (!string.IsNullOrWhiteSpace(_levelFilter) && LevelRank.TryGetValue(_levelFilter, out var minRank)) { q = q.Where(e => LevelRank.TryGetValue(e.Level, out var r) && r >= minRank); } if (!string.IsNullOrWhiteSpace(_scriptFilter)) { q = q.Where(e => e.ScriptId.Contains(_scriptFilter, StringComparison.OrdinalIgnoreCase)); } return q.ToList(); } } protected override async Task OnInitializedAsync() { _hub = new HubConnectionBuilder() .WithUrl(Nav.ToAbsoluteUri(ScriptLogHub.Endpoint)) .WithAutomaticReconnect() .Build(); _hub.On(ScriptLogHub.MethodName, entry => { _rows.Insert(0, entry); if (_rows.Count > Capacity) _rows.RemoveAt(_rows.Count - 1); InvokeAsync(StateHasChanged); }); _hub.Closed += _ => { _connected = false; return InvokeAsync(StateHasChanged); }; _hub.Reconnected += _ => { _connected = true; return InvokeAsync(StateHasChanged); }; try { await _hub.StartAsync(); _connected = true; } catch { // Connection error — page shows "disconnected". } } private async Task ClearAsync() { _rows.Clear(); await InvokeAsync(StateHasChanged); } private static string LevelChipClass(string level) => level switch { "Critical" or "Error" => "chip-alert", "Warning" => "chip-caution", "Information" => "chip-idle", _ => "chip-idle", }; public async ValueTask DisposeAsync() { if (_hub is not null) await _hub.DisposeAsync(); } }