@page "/script-log" @attribute [Microsoft.AspNetCore.Authorization.Authorize] @rendermode RenderMode.InteractiveServer @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.SignalR.Client @using ZB.MOM.WW.OtOpcUa.Admin.Hubs @inject NavigationManager Nav @inject AdminHubConnectionFactory HubFactory @implements IAsyncDisposable

Script log viewer

Live tail of the scripts-*.log file produced by the OPC UA Server's Roslyn script runtime. Useful for diagnosing virtual-tag and scripted-alarm script errors in production. Filter by script name to see only events from one script.

@if (_streaming) { Streaming } else if (_stopped) { Stopped } @if (_lines.Count > 0) { @_lines.Count line@(_lines.Count == 1 ? "" : "s") }
@if (_error is not null) {
@_error
} @if (_lines.Count == 0 && !_streaming && !_stopped) {
Press Start to begin tailing the script log. The last @ScriptLogHub.TailSeedLines lines are replayed first, then new lines appear as they are written by the OPC UA Server script runtime.
} else if (_lines.Count == 0 && (_streaming || _stopped)) {
No matching log lines found. Check that the OPC UA Server is running and has executed at least one script, and that the ScriptLog:Directory setting points to the correct log folder.
} else {
Script log Latest @_lines.Count entries — oldest first
@foreach (var line in _lines) { }
Level Script Message
@line.Level @(line.ScriptName ?? "—") @line.Raw
} @code { // Keep at most this many lines in-memory to avoid DOM growth. private const int MaxLines = 1000; private HubConnection? _hub; private CancellationTokenSource? _streamCts; private List _lines = []; private string _scriptNameFilter = string.Empty; private string _minLevel = "INF"; private bool _streaming; private bool _stopped; private string? _error; private ElementReference _tableContainer; private static readonly string[] LevelOrder = ["VRB", "DBG", "INF", "WRN", "ERR", "FTL"]; private async Task StartAsync() { _error = null; _streaming = false; _stopped = false; try { _hub ??= HubFactory.Create("/hubs/script-log"); if (_hub.State == HubConnectionState.Disconnected) await _hub.StartAsync(); _streamCts = new CancellationTokenSource(); _streaming = true; // Fire-and-forget into the background; updates come via StateHasChanged. _ = Task.Run(() => ConsumeStreamAsync(_streamCts.Token)); } catch (Exception ex) { _error = $"Failed to connect to script log hub: {ex.Message}"; _streaming = false; } } private async Task ConsumeStreamAsync(CancellationToken ct) { try { var stream = _hub!.StreamAsync( "TailLogAsync", _scriptNameFilter, ct); await foreach (var line in stream.WithCancellation(ct)) { if (!PassesLevelFilter(line.Level)) continue; await InvokeAsync(() => { _lines.Add(line); if (_lines.Count > MaxLines) _lines.RemoveRange(0, _lines.Count - MaxLines); StateHasChanged(); }); } } catch (OperationCanceledException) { /* normal stop */ } catch (Exception ex) { await InvokeAsync(() => { _error = $"Stream error: {ex.Message}"; _streaming = false; _stopped = true; StateHasChanged(); }); return; } await InvokeAsync(() => { _streaming = false; _stopped = true; StateHasChanged(); }); } private async Task StopAsync() { if (_streamCts is not null) { await _streamCts.CancelAsync(); _streamCts.Dispose(); _streamCts = null; } _streaming = false; _stopped = true; } private void ClearLines() { _lines.Clear(); _stopped = false; } private bool PassesLevelFilter(string level) { var minIdx = Array.IndexOf(LevelOrder, _minLevel); var lineIdx = Array.IndexOf(LevelOrder, level); return lineIdx >= minIdx; } private static string LevelBadge(string level) => level switch { "ERR" or "FTL" => "chip-bad", "WRN" => "chip-warn", "INF" => "chip-ok", _ => "chip-idle", }; private static string RowClass(string level) => level switch { "ERR" or "FTL" => "table-danger", "WRN" => "table-warning", _ => string.Empty, }; public async ValueTask DisposeAsync() { if (_streamCts is not null) { await _streamCts.CancelAsync(); _streamCts.Dispose(); } if (_hub is not null) { await _hub.DisposeAsync(); _hub = null; } } }