@page "/sessions/{SessionId}" @inherits DashboardPageBase @implements IAsyncDisposable @using Microsoft.AspNetCore.SignalR.Client @using ZB.MOM.WW.MxGateway.Contracts.Proto @using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs Dashboard Session @if (Snapshot is null) {
Loading session.
} else if (CurrentSession is null) {

Session Not Found

The session is not present in the current snapshot.

} else {

Session Details

@CurrentSession.SessionId

Session

Backend@CurrentSession.BackendName
Client identity@DashboardDisplay.Text(CurrentSession.ClientIdentity)
Client session@DashboardDisplay.Text(CurrentSession.ClientSessionName)
Client correlation@DashboardDisplay.Text(CurrentSession.ClientCorrelationId)
Opened@DashboardDisplay.DateTime(CurrentSession.OpenedAt)
Last activity@DashboardDisplay.DateTime(CurrentSession.LastClientActivityAt)
Lease expires@DashboardDisplay.DateTime(CurrentSession.LeaseExpiresAt)
Events received@DashboardDisplay.Count(CurrentSession.EventsReceived)
Last fault@DashboardDisplay.Text(CurrentSession.LastFault)

Worker

Process id@(CurrentSession.WorkerProcessId?.ToString(System.Globalization.CultureInfo.InvariantCulture) ?? "-")
State
Last heartbeat@DashboardDisplay.DateTime(CurrentSession.LastWorkerHeartbeatAt)

Recent events

@(_eventsConnected ? "live" : "offline")
@if (_recentEvents.Count == 0) {
Waiting for events. The dashboard mirrors the session's gRPC event stream — events appear here only while a gRPC client is also consuming this session's events.
} else {
@foreach (MxEvent evt in _recentEvents) { }
Worker seq Family Server Item Status
@evt.WorkerSequence @evt.Family @evt.ServerHandle @evt.ItemHandle @DashboardDisplay.Text(EventStatusLabel(evt))
}
} @code { private const int MaxRecentEvents = 50; [Parameter] public string SessionId { get; set; } = string.Empty; private DashboardSessionSummary? CurrentSession => Snapshot?.Sessions.FirstOrDefault(session => string.Equals(session.SessionId, SessionId, StringComparison.Ordinal)); private HubConnection? _eventsHub; private bool _eventsConnected; private string? _subscribedSessionId; private readonly LinkedList _recentEvents = new(); protected override async Task OnParametersSetAsync() { if (!string.Equals(_subscribedSessionId, SessionId, StringComparison.Ordinal)) { await DetachEventsHubAsync().ConfigureAwait(false); await AttachEventsHubAsync().ConfigureAwait(false); } } private async Task AttachEventsHubAsync() { if (string.IsNullOrWhiteSpace(SessionId)) { return; } _eventsHub = HubFactory.Create("/hubs/events"); _eventsHub.On(EventsHub.EventMessage, async mxEvent => { _recentEvents.AddFirst(mxEvent); while (_recentEvents.Count > MaxRecentEvents) { _recentEvents.RemoveLast(); } await InvokeAsync(StateHasChanged).ConfigureAwait(false); }); _eventsHub.Closed += _ => { _eventsConnected = false; return InvokeAsync(StateHasChanged); }; _eventsHub.Reconnected += _ => { _eventsConnected = true; return InvokeAsync(StateHasChanged); }; try { await _eventsHub.StartAsync().ConfigureAwait(false); await _eventsHub.SendAsync("SubscribeSession", SessionId).ConfigureAwait(false); _eventsConnected = true; _subscribedSessionId = SessionId; } catch { _eventsConnected = false; } } private async Task DetachEventsHubAsync() { HubConnection? hub = _eventsHub; _eventsHub = null; _eventsConnected = false; _subscribedSessionId = null; _recentEvents.Clear(); if (hub is not null) { try { await hub.DisposeAsync().ConfigureAwait(false); } catch { // Disposal-time errors are best-effort. } } } private static string EventStatusLabel(MxEvent evt) { return evt.Family == MxEventFamily.OnAlarmTransition ? evt.OnAlarmTransition?.AlarmFullReference ?? string.Empty : string.Empty; } public new async ValueTask DisposeAsync() { await DetachEventsHubAsync().ConfigureAwait(false); await base.DisposeAsync().ConfigureAwait(false); } }