Merge origin/main with local pending work and update AGENTS.md references

- Resolve 14 conflicts from popping local stash on top of origin's
  eed1e88 + 8d3352f doc-comment additions (11 mechanical, plus
  version.rs, DashboardAuthenticatorTests.cs, DashboardGalaxyProjector.cs)
- Fix 4 test files that used AGENTS.md as the repo-root sentinel
  (now use CLAUDE.md, since AGENTS.md was removed in 4731ab5)
- Redirect 10 doc citations from AGENTS.md to the matching gateway.md
  sections (Value Model, Status Model, Security, STA Worker Thread
  Model, gRPC Layer rule, cancellation rule)

Verified: solution build clean, x86 worker build clean, 266/266
gateway tests passing, 121/121 worker tests passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-30 14:13:33 -04:00
parent 8d3352f2c6
commit ddad573b75
101 changed files with 6053 additions and 621 deletions
@@ -2,6 +2,7 @@ using Google.Protobuf.WellKnownTypes;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using MxGateway.Contracts.Proto.Galaxy;
using MxGateway.Server.Dashboard;
using MxGateway.Server.Grpc;
namespace MxGateway.Server.Galaxy;
@@ -49,7 +50,16 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
{
GalaxyHierarchyCacheEntry snapshot = Volatile.Read(ref _current);
GalaxyCacheStatus projected = ProjectStatus(snapshot);
return projected == snapshot.Status ? snapshot : snapshot with { Status = projected };
return projected == snapshot.Status
? snapshot
: snapshot with
{
Status = projected,
DashboardSummary = snapshot.DashboardSummary with
{
Status = MapDashboardStatus(projected),
},
};
}
}
@@ -101,6 +111,14 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
LastQueriedAt = queriedAt,
LastSuccessAt = queriedAt,
LastError = null,
DashboardSummary = previous.DashboardSummary with
{
Status = DashboardGalaxyStatus.Healthy,
LastQueriedAt = queriedAt,
LastSuccessAt = queriedAt,
LastDeployTime = deployTime,
LastError = null,
},
};
Volatile.Write(ref _current, refreshed);
_firstLoad.TrySetResult();
@@ -113,11 +131,24 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
List<GalaxyHierarchyRow> hierarchy = hierarchyTask.Result;
List<GalaxyAttributeRow> attributes = attributesTask.Result;
DiscoverHierarchyReply reply = BuildReply(hierarchy, attributes);
IReadOnlyList<GalaxyObject> objects = BuildObjects(hierarchy, attributes);
GalaxyHierarchyIndex index = GalaxyHierarchyIndex.Build(objects);
int areaCount = hierarchy.Count(row => row.IsArea);
int historized = attributes.Count(row => row.IsHistorized);
int alarms = attributes.Count(row => row.IsAlarm);
DashboardGalaxySummary dashboardSummary = BuildDashboardSummary(
status: GalaxyCacheStatus.Healthy,
lastQueriedAt: queriedAt,
lastSuccessAt: queriedAt,
lastDeployTime: deployTime,
lastError: null,
hierarchy: hierarchy,
objectCount: hierarchy.Count,
areaCount: areaCount,
attributeCount: attributes.Count,
historizedAttributeCount: historized,
alarmAttributeCount: alarms);
long nextSequence = previous.Sequence + 1;
GalaxyHierarchyCacheEntry next = new(
@@ -127,9 +158,9 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
LastSuccessAt: queriedAt,
LastDeployTime: deployTime,
LastError: null,
Hierarchy: hierarchy,
Attributes: attributes,
Reply: reply,
Objects: objects,
Index: index,
DashboardSummary: dashboardSummary,
ObjectCount: hierarchy.Count,
AreaCount: areaCount,
AttributeCount: attributes.Count,
@@ -158,13 +189,19 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
Status = previous.HasData ? GalaxyCacheStatus.Stale : GalaxyCacheStatus.Unavailable,
LastQueriedAt = queriedAt,
LastError = exception.Message,
DashboardSummary = previous.DashboardSummary with
{
Status = MapDashboardStatus(previous.HasData ? GalaxyCacheStatus.Stale : GalaxyCacheStatus.Unavailable),
LastQueriedAt = queriedAt,
LastError = exception.Message,
},
};
Volatile.Write(ref _current, failed);
_firstLoad.TrySetResult();
}
}
private static DiscoverHierarchyReply BuildReply(
private static IReadOnlyList<GalaxyObject> BuildObjects(
IReadOnlyList<GalaxyHierarchyRow> hierarchy,
IReadOnlyList<GalaxyAttributeRow> attributes)
{
@@ -172,14 +209,110 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
.GroupBy(a => a.GobjectId)
.ToDictionary(g => g.Key, g => g.ToList());
DiscoverHierarchyReply reply = new();
List<GalaxyObject> objects = new(hierarchy.Count);
foreach (GalaxyHierarchyRow row in hierarchy)
{
reply.Objects.Add(GalaxyProtoMapper.MapObject(row, attributesByGobjectId));
objects.Add(GalaxyProtoMapper.MapObject(row, attributesByGobjectId));
}
return reply;
return objects;
}
private static DashboardGalaxySummary BuildDashboardSummary(
GalaxyCacheStatus status,
DateTimeOffset? lastQueriedAt,
DateTimeOffset? lastSuccessAt,
DateTimeOffset? lastDeployTime,
string? lastError,
IReadOnlyList<GalaxyHierarchyRow> hierarchy,
int objectCount,
int areaCount,
int attributeCount,
int historizedAttributeCount,
int alarmAttributeCount)
{
IReadOnlyList<DashboardGalaxyTemplateUsage> topTemplates;
IReadOnlyList<DashboardGalaxyCategoryCount> objectCategories;
if (hierarchy.Count == 0)
{
topTemplates = Array.Empty<DashboardGalaxyTemplateUsage>();
objectCategories = Array.Empty<DashboardGalaxyCategoryCount>();
}
else
{
Dictionary<int, int> objectsByCategory = new();
Dictionary<string, int> templateUsage = new(StringComparer.OrdinalIgnoreCase);
foreach (GalaxyHierarchyRow row in hierarchy)
{
objectsByCategory.TryGetValue(row.CategoryId, out int categoryCount);
objectsByCategory[row.CategoryId] = categoryCount + 1;
if (row.TemplateChain.Count > 0)
{
string immediate = row.TemplateChain[0];
if (!string.IsNullOrWhiteSpace(immediate))
{
templateUsage.TryGetValue(immediate, out int templateCount);
templateUsage[immediate] = templateCount + 1;
}
}
}
topTemplates = templateUsage
.OrderByDescending(entry => entry.Value)
.ThenBy(entry => entry.Key, StringComparer.OrdinalIgnoreCase)
.Take(10)
.Select(entry => new DashboardGalaxyTemplateUsage(entry.Key, entry.Value))
.ToArray();
objectCategories = objectsByCategory
.OrderByDescending(entry => entry.Value)
.ThenBy(entry => entry.Key)
.Select(entry => new DashboardGalaxyCategoryCount(
entry.Key,
ResolveCategoryName(entry.Key),
entry.Value))
.ToArray();
}
return new DashboardGalaxySummary(
Status: MapDashboardStatus(status),
LastQueriedAt: lastQueriedAt,
LastSuccessAt: lastSuccessAt,
LastDeployTime: lastDeployTime,
LastError: lastError,
ObjectCount: objectCount,
AreaCount: areaCount,
AttributeCount: attributeCount,
HistorizedAttributeCount: historizedAttributeCount,
AlarmAttributeCount: 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}",
};
private GalaxyCacheStatus ProjectStatus(GalaxyHierarchyCacheEntry snapshot)
{
if (snapshot.Status is GalaxyCacheStatus.Unknown or GalaxyCacheStatus.Unavailable)
@@ -1,11 +1,12 @@
using MxGateway.Contracts.Proto.Galaxy;
using MxGateway.Server.Dashboard;
namespace MxGateway.Server.Galaxy;
/// <summary>
/// Immutable snapshot of the Galaxy Repository browse data held by
/// <see cref="GalaxyHierarchyCache"/>. Multiple gRPC clients share the same instance —
/// the materialized <see cref="Reply"/> is produced once per refresh and reused.
/// <see cref="GalaxyHierarchyCache"/>. Multiple gRPC clients share the same
/// materialized object list and precomputed dashboard projection.
/// </summary>
public sealed record GalaxyHierarchyCacheEntry(
GalaxyCacheStatus Status,
@@ -14,9 +15,9 @@ public sealed record GalaxyHierarchyCacheEntry(
DateTimeOffset? LastSuccessAt,
DateTimeOffset? LastDeployTime,
string? LastError,
IReadOnlyList<GalaxyHierarchyRow> Hierarchy,
IReadOnlyList<GalaxyAttributeRow> Attributes,
DiscoverHierarchyReply? Reply,
IReadOnlyList<GalaxyObject> Objects,
GalaxyHierarchyIndex Index,
DashboardGalaxySummary DashboardSummary,
int ObjectCount,
int AreaCount,
int AttributeCount,
@@ -31,9 +32,9 @@ public sealed record GalaxyHierarchyCacheEntry(
LastSuccessAt: null,
LastDeployTime: null,
LastError: null,
Hierarchy: Array.Empty<GalaxyHierarchyRow>(),
Attributes: Array.Empty<GalaxyAttributeRow>(),
Reply: null,
Objects: Array.Empty<GalaxyObject>(),
Index: GalaxyHierarchyIndex.Empty,
DashboardSummary: DashboardGalaxySummary.Unknown,
ObjectCount: 0,
AreaCount: 0,
AttributeCount: 0,