using Microsoft.EntityFrameworkCore; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; namespace ZB.MOM.WW.OtOpcUa.Admin.Services; /// /// One row per record, enriched with the owning /// ClusterNode.ClusterId when available (left-join). The Admin /hosts page /// groups by cluster and renders a per-node → per-driver → per-host tree. /// public sealed record HostStatusRow( string NodeId, string? ClusterId, string DriverInstanceId, string HostName, DriverHostState State, DateTime StateChangedUtc, DateTime LastSeenUtc, string? Detail); /// /// Read-side service for the Admin UI's per-host drill-down. Loads /// rows (written by the Server process's /// HostStatusPublisher) and left-joins ClusterNode so each row knows which /// cluster it belongs to — the Admin UI groups by cluster for the fleet-wide view. /// /// /// The publisher heartbeat is 10s (HostStatusPublisher.HeartbeatInterval). The /// Admin page also polls every ~10s and treats rows with LastSeenUtc older than /// StaleThreshold (30s) as stale — covers a missed heartbeat tolerance plus /// a generous buffer for clock skew and publisher GC pauses. /// public sealed class HostStatusService(OtOpcUaConfigDbContext db) { public static readonly TimeSpan StaleThreshold = TimeSpan.FromSeconds(30); public async Task> ListAsync(CancellationToken ct = default) { // LEFT JOIN on NodeId so a row persists even when its owning ClusterNode row hasn't // been created yet (first-boot bootstrap case — keeps the UI from losing sight of // the reporting server). var rows = await (from s in db.DriverHostStatuses.AsNoTracking() join n in db.ClusterNodes.AsNoTracking() on s.NodeId equals n.NodeId into nodeJoin from n in nodeJoin.DefaultIfEmpty() orderby s.NodeId, s.DriverInstanceId, s.HostName select new HostStatusRow( s.NodeId, n != null ? n.ClusterId : null, s.DriverInstanceId, s.HostName, s.State, s.StateChangedUtc, s.LastSeenUtc, s.Detail)).ToListAsync(ct); return rows; } public static bool IsStale(HostStatusRow row) => DateTime.UtcNow - row.LastSeenUtc > StaleThreshold; }