@page "/deployment/debug-view" @using ScadaLink.Security @using ScadaLink.Commons.Entities.Instances @using ScadaLink.Commons.Entities.Sites @using ScadaLink.Commons.Interfaces.Repositories @using ScadaLink.Commons.Messages.DebugView @using ScadaLink.Commons.Messages.Streaming @using ScadaLink.Commons.Types @using ScadaLink.Commons.Types.Enums @using ScadaLink.Communication @attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)] @inject ITemplateEngineRepository TemplateEngineRepository @inject ISiteRepository SiteRepository @inject DebugStreamService DebugStreamService @inject IJSRuntime JS @implements IDisposable

Debug View

@if (_loading) { } else {
@if (!_connected) { } else { Live }
@if (_connected && _snapshot != null) {
@* Attribute Values *@
Attribute Values @_attributeValues.Count values
@foreach (var av in _attributeValues.Values.OrderBy(a => a.AttributeName)) { }
Attribute Value Quality Timestamp
@av.AttributeName @ValueFormatter.FormatDisplayValue(av.Value) @av.Quality @av.Timestamp.LocalDateTime.ToString("HH:mm:ss.fff")
@* Alarm States *@
Alarm States @_alarmStates.Count alarms
@foreach (var alarm in _alarmStates.Values.OrderBy(a => a.AlarmName)) { }
Alarm State Priority Timestamp
@alarm.AlarmName @alarm.State @alarm.Priority @alarm.Timestamp.LocalDateTime.ToString("HH:mm:ss.fff")
Snapshot received: @_snapshot.SnapshotTimestamp.LocalDateTime.ToString("HH:mm:ss") | @_attributeValues.Count attributes, @_alarmStates.Count alarms
} else if (_connected) { } }
@code { private List _sites = new(); private List _siteInstances = new(); private int _selectedSiteId; private int _selectedInstanceId; private bool _loading = true; private bool _connected; private bool _connecting; private DebugViewSnapshot? _snapshot; private Dictionary _attributeValues = new(); private Dictionary _alarmStates = new(); private DebugStreamSession? _session; private ToastNotification _toast = default!; protected override async Task OnInitializedAsync() { try { _sites = (await SiteRepository.GetAllSitesAsync()).ToList(); } catch (Exception ex) { _toast.ShowError($"Failed to load sites: {ex.Message}"); } _loading = false; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; var storedSiteId = await JS.InvokeAsync("localStorage.getItem", "debugView.siteId"); var storedInstanceId = await JS.InvokeAsync("localStorage.getItem", "debugView.instanceId"); if (!string.IsNullOrEmpty(storedSiteId) && int.TryParse(storedSiteId, out var siteId) && !string.IsNullOrEmpty(storedInstanceId) && int.TryParse(storedInstanceId, out var instanceId)) { _selectedSiteId = siteId; await LoadInstancesForSite(); _selectedInstanceId = instanceId; StateHasChanged(); await Connect(); } } private async Task LoadInstancesForSite() { _siteInstances.Clear(); _selectedInstanceId = 0; if (_selectedSiteId == 0) return; try { _siteInstances = (await TemplateEngineRepository.GetInstancesBySiteIdAsync(_selectedSiteId)) .Where(i => i.State == InstanceState.Enabled) .ToList(); } catch (Exception ex) { _toast.ShowError($"Failed to load instances: {ex.Message}"); } } private void OnInstanceSelectionChanged() { // No-op; selection is tracked via _selectedInstanceId binding } private async Task Connect() { if (_selectedInstanceId == 0 || _selectedSiteId == 0) return; _connecting = true; try { var session = await DebugStreamService.StartStreamAsync( _selectedInstanceId, onEvent: evt => { switch (evt) { case AttributeValueChanged av: _attributeValues[av.AttributeName] = av; _ = InvokeAsync(StateHasChanged); break; case AlarmStateChanged al: _alarmStates[al.AlarmName] = al; _ = InvokeAsync(StateHasChanged); break; } }, onTerminated: () => { _connected = false; _session = null; _ = InvokeAsync(() => { _toast.ShowError("Debug stream terminated (site disconnected)."); StateHasChanged(); }); }); _session = session; // Populate initial state from snapshot _attributeValues.Clear(); foreach (var av in session.InitialSnapshot.AttributeValues) _attributeValues[av.AttributeName] = av; _alarmStates.Clear(); foreach (var al in session.InitialSnapshot.AlarmStates) _alarmStates[al.AlarmName] = al; _snapshot = session.InitialSnapshot; _connected = true; // Persist selection to localStorage for auto-reconnect on refresh await JS.InvokeVoidAsync("localStorage.setItem", "debugView.siteId", _selectedSiteId.ToString()); await JS.InvokeVoidAsync("localStorage.setItem", "debugView.instanceId", _selectedInstanceId.ToString()); var instance = _siteInstances.FirstOrDefault(i => i.Id == _selectedInstanceId); _toast.ShowSuccess($"Streaming {instance?.UniqueName ?? "instance"}"); } catch (Exception ex) { _toast.ShowError($"Connect failed: {ex.Message}"); } _connecting = false; } private async Task Disconnect() { if (_session != null) { DebugStreamService.StopStream(_session.SessionId); _session = null; } // Clear persisted selection — user explicitly disconnected await JS.InvokeVoidAsync("localStorage.removeItem", "debugView.siteId"); await JS.InvokeVoidAsync("localStorage.removeItem", "debugView.instanceId"); _connected = false; _snapshot = null; _attributeValues.Clear(); _alarmStates.Clear(); } private static string GetAlarmStateBadge(AlarmState state) => state switch { AlarmState.Active => "bg-danger", AlarmState.Normal => "bg-success", _ => "bg-secondary" }; private static string GetAlarmRowClass(AlarmState state) => state switch { AlarmState.Active => "table-danger", _ => "" }; public void Dispose() { if (_session != null) { DebugStreamService.StopStream(_session.SessionId); } } }