@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.Enums @using ScadaLink.Communication @attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)] @inject ITemplateEngineRepository TemplateEngineRepository @inject ISiteRepository SiteRepository @inject CommunicationService CommunicationService @inject IJSRuntime JS @implements IDisposable

Debug View

@if (_loading) { } else {
@if (!_connected) { } else { Connected }
@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 @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 string _selectedInstanceName = string.Empty; private bool _loading = true; private bool _connected; private bool _connecting; private DebugViewSnapshot? _snapshot; private Dictionary _attributeValues = new(); private Dictionary _alarmStates = new(); private Timer? _refreshTimer; 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 storedInstanceName = await JS.InvokeAsync("localStorage.getItem", "debugView.instanceName"); if (!string.IsNullOrEmpty(storedSiteId) && int.TryParse(storedSiteId, out var siteId) && !string.IsNullOrEmpty(storedInstanceName)) { _selectedSiteId = siteId; await LoadInstancesForSite(); _selectedInstanceName = storedInstanceName; StateHasChanged(); await Connect(); } } private async Task LoadInstancesForSite() { _siteInstances.Clear(); _selectedInstanceName = string.Empty; 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 async Task Connect() { if (string.IsNullOrEmpty(_selectedInstanceName) || _selectedSiteId == 0) return; _connecting = true; try { var site = _sites.FirstOrDefault(s => s.Id == _selectedSiteId); if (site == null) return; var request = new SubscribeDebugViewRequest(_selectedInstanceName, Guid.NewGuid().ToString("N")); _snapshot = await CommunicationService.SubscribeDebugViewAsync(site.SiteIdentifier, request); // Populate initial state from snapshot _attributeValues.Clear(); foreach (var av in _snapshot.AttributeValues) { _attributeValues[av.AttributeName] = av; } _alarmStates.Clear(); foreach (var al in _snapshot.AlarmStates) { _alarmStates[al.AlarmName] = al; } _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.instanceName", _selectedInstanceName); await JS.InvokeVoidAsync("localStorage.setItem", "debugView.siteIdentifier", site.SiteIdentifier); _toast.ShowSuccess($"Connected to {_selectedInstanceName}"); // Periodic refresh (simulating SignalR push by re-subscribing) _refreshTimer = new Timer(async _ => { try { var refreshRequest = new SubscribeDebugViewRequest(_selectedInstanceName, Guid.NewGuid().ToString("N")); var newSnapshot = await CommunicationService.SubscribeDebugViewAsync(site.SiteIdentifier, refreshRequest); foreach (var av in newSnapshot.AttributeValues) _attributeValues[av.AttributeName] = av; foreach (var al in newSnapshot.AlarmStates) _alarmStates[al.AlarmName] = al; _snapshot = newSnapshot; await InvokeAsync(StateHasChanged); } catch { // Connection may have dropped } }, null, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2)); } catch (Exception ex) { _toast.ShowError($"Connect failed: {ex.Message}"); } _connecting = false; } private async Task Disconnect() { _refreshTimer?.Dispose(); _refreshTimer = null; if (_connected && _selectedSiteId > 0 && !string.IsNullOrEmpty(_selectedInstanceName)) { var site = _sites.FirstOrDefault(s => s.Id == _selectedSiteId); if (site != null) { var request = new UnsubscribeDebugViewRequest(_selectedInstanceName, Guid.NewGuid().ToString("N")); CommunicationService.UnsubscribeDebugView(site.SiteIdentifier, request); } } // Clear persisted selection — user explicitly disconnected await JS.InvokeVoidAsync("localStorage.removeItem", "debugView.siteId"); await JS.InvokeVoidAsync("localStorage.removeItem", "debugView.instanceName"); await JS.InvokeVoidAsync("localStorage.removeItem", "debugView.siteIdentifier"); _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() { _refreshTimer?.Dispose(); } }