using ZB.MOM.WW.GalaxyRepository; using ZB.MOM.WW.GalaxyRepository.Grpc; namespace ZB.MOM.WW.MxGateway.Server.Dashboard; /// /// Projects a shared-library into the /// dashboard's host-side . /// /// /// The shared ZB.MOM.WW.GalaxyRepository cache entry does not compute a /// dashboard summary; this projector relocates the summary logic that previously /// lived inside the gateway's inline Galaxy hierarchy cache. It groups the entry's /// objects into template usage and category counts, maps the cache status to the /// dashboard status, and copies the timestamps and counts the entry already carries. /// public static class DashboardGalaxySummaryProjector { private const int MaxTopTemplates = 10; /// /// Builds a from a shared-library Galaxy /// hierarchy cache entry. /// /// The shared-library cache entry to project. /// The dashboard summary derived from . public static DashboardGalaxySummary Project(GalaxyHierarchyCacheEntry entry) { ArgumentNullException.ThrowIfNull(entry); return BuildSummary(entry, ComputeBreakdown(entry)); } /// /// Computes the O(N) template-usage and category breakdown from the entry's objects. /// This is the only part of the summary that changes with the cache /// — the shared library replaces the object /// set only on a heavy refresh that bumps the sequence — so callers may memoize the result /// on the sequence. The cheap, per-tick-volatile fields are re-read fresh via /// and must never be memoized. /// /// The shared-library cache entry to project. /// The template/category breakdown of 's objects. public static GalaxyObjectBreakdown ComputeBreakdown(GalaxyHierarchyCacheEntry entry) { ArgumentNullException.ThrowIfNull(entry); if (entry.Objects.Count == 0) { return GalaxyObjectBreakdown.Empty; } Dictionary objectsByCategory = new(); Dictionary templateUsage = new(StringComparer.OrdinalIgnoreCase); foreach (GalaxyObject obj in entry.Objects) { objectsByCategory.TryGetValue(obj.CategoryId, out int categoryCount); objectsByCategory[obj.CategoryId] = categoryCount + 1; if (obj.TemplateChain.Count > 0) { string immediate = obj.TemplateChain[0]; if (!string.IsNullOrWhiteSpace(immediate)) { templateUsage.TryGetValue(immediate, out int templateCount); templateUsage[immediate] = templateCount + 1; } } } IReadOnlyList topTemplates = templateUsage .OrderByDescending(usage => usage.Value) .ThenBy(usage => usage.Key, StringComparer.OrdinalIgnoreCase) .Take(MaxTopTemplates) .Select(usage => new DashboardGalaxyTemplateUsage(usage.Key, usage.Value)) .ToArray(); IReadOnlyList objectCategories = objectsByCategory .OrderByDescending(category => category.Value) .ThenBy(category => category.Key) .Select(category => new DashboardGalaxyCategoryCount( category.Key, ResolveCategoryName(category.Key), category.Value)) .ToArray(); return new GalaxyObjectBreakdown(topTemplates, objectCategories); } /// /// Assembles a from an entry's cheap, per-tick-volatile /// fields (status, timestamps, last error, counts) and a precomputed /// . The volatile fields are copied fresh on every call so a /// memoized breakdown never freezes the dashboard's health or timestamps between heavy /// refreshes — the library mutates those fields in place at the same sequence on steady-state /// ticks and on refresh failure. /// /// The cache entry whose volatile fields to copy. /// The precomputed template/category breakdown. /// The assembled dashboard summary. public static DashboardGalaxySummary BuildSummary(GalaxyHierarchyCacheEntry entry, GalaxyObjectBreakdown breakdown) { ArgumentNullException.ThrowIfNull(entry); ArgumentNullException.ThrowIfNull(breakdown); return new DashboardGalaxySummary( Status: MapDashboardStatus(entry.Status), LastQueriedAt: entry.LastQueriedAt, LastSuccessAt: entry.LastSuccessAt, LastDeployTime: entry.LastDeployTime, LastError: entry.LastError, ObjectCount: entry.ObjectCount, AreaCount: entry.AreaCount, AttributeCount: entry.AttributeCount, HistorizedAttributeCount: entry.HistorizedAttributeCount, AlarmAttributeCount: entry.AlarmAttributeCount, TopTemplates: breakdown.TopTemplates, ObjectCategories: breakdown.ObjectCategories); } private static DashboardGalaxyStatus MapDashboardStatus(GalaxyCacheStatus status) => status switch { GalaxyCacheStatus.Healthy => DashboardGalaxyStatus.Healthy, GalaxyCacheStatus.Stale => DashboardGalaxyStatus.Stale, GalaxyCacheStatus.Unavailable => DashboardGalaxyStatus.Unavailable, _ => DashboardGalaxyStatus.Unknown, }; private static string ResolveCategoryName(int categoryId) => categoryId switch { 1 => "WinPlatform", 3 => "AppEngine", 4 => "InTouchViewApp", 10 => "UserDefined", 11 => "FieldReference", 13 => "Area", 17 => "DIObject", 24 => "DDESuiteLinkClient", 26 => "OPCClient", _ => $"Category {categoryId}", }; }