diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGalaxySummaryProjector.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGalaxySummaryProjector.cs
new file mode 100644
index 0000000..3815d58
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGalaxySummaryProjector.cs
@@ -0,0 +1,113 @@
+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);
+
+ IReadOnlyList topTemplates;
+ IReadOnlyList objectCategories;
+
+ if (entry.Objects.Count == 0)
+ {
+ topTemplates = Array.Empty();
+ objectCategories = Array.Empty();
+ }
+ else
+ {
+ 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;
+ }
+ }
+ }
+
+ topTemplates = templateUsage
+ .OrderByDescending(usage => usage.Value)
+ .ThenBy(usage => usage.Key, StringComparer.OrdinalIgnoreCase)
+ .Take(MaxTopTemplates)
+ .Select(usage => new DashboardGalaxyTemplateUsage(usage.Key, usage.Value))
+ .ToArray();
+
+ objectCategories = objectsByCategory
+ .OrderByDescending(category => category.Value)
+ .ThenBy(category => category.Key)
+ .Select(category => new DashboardGalaxyCategoryCount(
+ category.Key,
+ ResolveCategoryName(category.Key),
+ category.Value))
+ .ToArray();
+ }
+
+ 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: topTemplates,
+ ObjectCategories: 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}",
+ };
+}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs
new file mode 100644
index 0000000..7ce303c
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs
@@ -0,0 +1,148 @@
+using ZB.MOM.WW.GalaxyRepository;
+using ZB.MOM.WW.GalaxyRepository.Grpc;
+using ZB.MOM.WW.MxGateway.Server.Dashboard;
+
+namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
+
+///
+/// Unit tests for , the host-side
+/// projection that turns a shared-library
+/// into the dashboard's .
+///
+public sealed class DashboardGalaxySummaryProjectorTests
+{
+ ///
+ /// Verifies that a healthy entry maps status, copies timestamps/counts, and
+ /// groups objects into template usage (ordered by instance count) and
+ /// category counts with resolved names.
+ ///
+ [Fact]
+ public void Project_HealthyEntry_MapsStatusCopiesCountsAndGroupsObjects()
+ {
+ DateTimeOffset queriedAt = new(2026, 6, 25, 10, 0, 0, TimeSpan.Zero);
+ DateTimeOffset successAt = new(2026, 6, 25, 10, 0, 1, TimeSpan.Zero);
+ DateTimeOffset deployAt = new(2026, 6, 24, 8, 30, 0, TimeSpan.Zero);
+
+ // Two templates (PumpTemplate x2, ValveTemplate x1) and two categories
+ // (10 = UserDefined x3, 13 = Area x1).
+ GalaxyObject area = new()
+ {
+ GobjectId = 1,
+ BrowseName = "AreaA",
+ IsArea = true,
+ CategoryId = 13,
+ };
+ GalaxyObject pumpA = new()
+ {
+ GobjectId = 2,
+ BrowseName = "Pump01",
+ CategoryId = 10,
+ };
+ pumpA.TemplateChain.Add("$PumpTemplate");
+ GalaxyObject pumpB = new()
+ {
+ GobjectId = 3,
+ BrowseName = "Pump02",
+ CategoryId = 10,
+ };
+ pumpB.TemplateChain.Add("$PumpTemplate");
+ GalaxyObject valve = new()
+ {
+ GobjectId = 4,
+ BrowseName = "Valve01",
+ CategoryId = 10,
+ };
+ valve.TemplateChain.Add("$ValveTemplate");
+
+ GalaxyObject[] objects = [area, pumpA, pumpB, valve];
+
+ GalaxyHierarchyCacheEntry entry = new(
+ Status: GalaxyCacheStatus.Healthy,
+ Sequence: 7,
+ LastQueriedAt: queriedAt,
+ LastSuccessAt: successAt,
+ LastDeployTime: deployAt,
+ LastError: null,
+ Objects: objects,
+ Index: GalaxyHierarchyIndex.Build(objects),
+ ObjectCount: 4,
+ AreaCount: 1,
+ AttributeCount: 12,
+ HistorizedAttributeCount: 5,
+ AlarmAttributeCount: 2);
+
+ DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);
+
+ Assert.Equal(DashboardGalaxyStatus.Healthy, summary.Status);
+ Assert.Equal(queriedAt, summary.LastQueriedAt);
+ Assert.Equal(successAt, summary.LastSuccessAt);
+ Assert.Equal(deployAt, summary.LastDeployTime);
+ Assert.Null(summary.LastError);
+ Assert.Equal(4, summary.ObjectCount);
+ Assert.Equal(1, summary.AreaCount);
+ Assert.Equal(12, summary.AttributeCount);
+ Assert.Equal(5, summary.HistorizedAttributeCount);
+ Assert.Equal(2, summary.AlarmAttributeCount);
+
+ // Templates ordered by instance count descending: PumpTemplate (2) before ValveTemplate (1).
+ Assert.Equal(2, summary.TopTemplates.Count);
+ Assert.Equal("$PumpTemplate", summary.TopTemplates[0].TemplateName);
+ Assert.Equal(2, summary.TopTemplates[0].InstanceCount);
+ Assert.Equal("$ValveTemplate", summary.TopTemplates[1].TemplateName);
+ Assert.Equal(1, summary.TopTemplates[1].InstanceCount);
+
+ // Categories ordered by object count descending: UserDefined (3) before Area (1).
+ Assert.Equal(2, summary.ObjectCategories.Count);
+ Assert.Equal(10, summary.ObjectCategories[0].CategoryId);
+ Assert.Equal("UserDefined", summary.ObjectCategories[0].CategoryName);
+ Assert.Equal(3, summary.ObjectCategories[0].ObjectCount);
+ Assert.Equal(13, summary.ObjectCategories[1].CategoryId);
+ Assert.Equal("Area", summary.ObjectCategories[1].CategoryName);
+ Assert.Equal(1, summary.ObjectCategories[1].ObjectCount);
+ }
+
+ ///
+ /// Verifies that an unknown entry with no objects projects to the
+ /// -equivalent summary.
+ ///
+ [Fact]
+ public void Project_UnknownEmptyEntry_ProducesUnknownEquivalentSummary()
+ {
+ GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty;
+
+ DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);
+
+ Assert.Equal(DashboardGalaxyStatus.Unknown, summary.Status);
+ Assert.Null(summary.LastQueriedAt);
+ Assert.Null(summary.LastSuccessAt);
+ Assert.Null(summary.LastDeployTime);
+ Assert.Null(summary.LastError);
+ Assert.Equal(0, summary.ObjectCount);
+ Assert.Equal(0, summary.AreaCount);
+ Assert.Equal(0, summary.AttributeCount);
+ Assert.Equal(0, summary.HistorizedAttributeCount);
+ Assert.Equal(0, summary.AlarmAttributeCount);
+ Assert.Empty(summary.TopTemplates);
+ Assert.Empty(summary.ObjectCategories);
+ }
+
+ ///
+ /// Verifies that the cache status enum is faithfully mapped to the dashboard
+ /// status enum across every defined value.
+ ///
+ [Theory]
+ [InlineData(GalaxyCacheStatus.Healthy, DashboardGalaxyStatus.Healthy)]
+ [InlineData(GalaxyCacheStatus.Stale, DashboardGalaxyStatus.Stale)]
+ [InlineData(GalaxyCacheStatus.Unavailable, DashboardGalaxyStatus.Unavailable)]
+ [InlineData(GalaxyCacheStatus.Unknown, DashboardGalaxyStatus.Unknown)]
+ public void Project_MapsCacheStatusToDashboardStatus(
+ GalaxyCacheStatus cacheStatus,
+ DashboardGalaxyStatus expected)
+ {
+ GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with { Status = cacheStatus };
+
+ DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);
+
+ Assert.Equal(expected, summary.Status);
+ }
+}