diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md index 43bdc4f0c4..55336cf3c3 100644 --- a/docs/release-control/v6/internal/subsystems/monitoring.md +++ b/docs/release-control/v6/internal/subsystems/monitoring.md @@ -137,6 +137,9 @@ The same applies to proxmox topology coordinates exposed through typed views: node, cluster, and instance accessors must return canonical trimmed values so monitoring consumers do not fork topology grouping or labeling on `" pve-a "` versus `pve-a`. +Connected infrastructure and monitored-system projections now also use the +shared unified-resource display-name fallback, so the monitoring layer does +not rebuild its own canonical name-or-hostname selection for those surfaces. Storage-backup preservation now also derives node-to-storage membership from canonical `ReadState.StoragePools()` instead of from snapshot-owned storage diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index cb88242e7f..3b825ba9c2 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -193,8 +193,9 @@ shared `ResourcePolicyLabel(...)` and `ResourcePolicyRedactedValue(...)` helpers instead of through AI-local presentation shims, so policy copying and policy-bound labels both stay centralized in the resource model. That same contract also owns the canonical resource display-name fallback, so -name-or-ID presentation stays consistent between the unified AI adapter and -the AI resource context instead of being recomputed locally. +name-or-ID presentation stays consistent between the unified AI adapter, the +AI resource context, and the connected-infrastructure / monitored-system +projections instead of being recomputed locally. That same shared store now also persists append-only action lifecycle, action audit, and export audit records, giving the control-plane verbs a durable home diff --git a/internal/monitoring/connected_infrastructure.go b/internal/monitoring/connected_infrastructure.go index 17f6bdf7dd..7bea7cd857 100644 --- a/internal/monitoring/connected_infrastructure.go +++ b/internal/monitoring/connected_infrastructure.go @@ -236,7 +236,7 @@ func addConnectedInfrastructureSurface( group = &connectedInfrastructureGroup{ id: key, name: connectedInfrastructureName(resource), - displayName: connectedInfrastructureDisplayName(resource), + displayName: unifiedresources.ResourceDisplayName(resource), hostname: connectedInfrastructureHostname(resource), status: "active", healthStatus: strings.TrimSpace(string(resource.Status)), @@ -285,7 +285,7 @@ func addConnectedInfrastructureSurface( group.hostname = connectedInfrastructureHostname(resource) } if group.displayName == "" { - group.displayName = connectedInfrastructureDisplayName(resource) + group.displayName = unifiedresources.ResourceDisplayName(resource) } if group.name == "" { group.name = connectedInfrastructureName(resource) @@ -398,8 +398,7 @@ func connectedInfrastructureHostname(resource unifiedresources.Resource) string func connectedInfrastructureName(resource unifiedresources.Resource) string { for _, candidate := range []string{ - connectedInfrastructureCanonicalDisplayName(resource), - strings.TrimSpace(resource.Name), + unifiedresources.ResourceDisplayName(resource), connectedInfrastructureHostname(resource), resource.ID, } { @@ -410,13 +409,6 @@ func connectedInfrastructureName(resource unifiedresources.Resource) string { return resource.ID } -func connectedInfrastructureDisplayName(resource unifiedresources.Resource) string { - if resource.Canonical != nil { - return strings.TrimSpace(resource.Canonical.DisplayName) - } - return "" -} - func connectedInfrastructureVersion(resource unifiedresources.Resource) string { switch { case resource.Agent != nil && strings.TrimSpace(resource.Agent.AgentVersion) != "": @@ -676,10 +668,3 @@ func connectedInfrastructureCanonicalHostname(resource unifiedresources.Resource } return "" } - -func connectedInfrastructureCanonicalDisplayName(resource unifiedresources.Resource) string { - if resource.Canonical != nil { - return strings.TrimSpace(resource.Canonical.DisplayName) - } - return "" -} diff --git a/internal/monitoring/connected_infrastructure_test.go b/internal/monitoring/connected_infrastructure_test.go index 5c8885bb5c..25970979ad 100644 --- a/internal/monitoring/connected_infrastructure_test.go +++ b/internal/monitoring/connected_infrastructure_test.go @@ -75,3 +75,38 @@ func TestBuildConnectedInfrastructure_AppliesIgnoreStateToActiveSurfaces(t *test t.Fatalf("expected ignored docker surface, got %#v", ignored.Surfaces) } } + +func TestBuildConnectedInfrastructure_UsesSharedDisplayNameFallback(t *testing.T) { + now := time.Unix(1_700_000_000, 0) + items := buildConnectedInfrastructure([]unifiedresources.Resource{ + { + ID: "tower-resource", + LastSeen: now, + Status: unifiedresources.StatusOnline, + Canonical: &unifiedresources.CanonicalIdentity{ + DisplayName: "Canonical Tower", + }, + Agent: &unifiedresources.AgentData{ + AgentID: "tower-agent", + AgentVersion: "1.2.3", + Hostname: "tower.local", + Platform: "linux", + }, + Identity: unifiedresources.ResourceIdentity{ + Hostnames: []string{"tower.local"}, + }, + }, + }, models.StateSnapshot{}) + + if len(items) != 1 { + t.Fatalf("expected 1 connected infrastructure item, got %d", len(items)) + } + + item := items[0] + if item.Name != "Canonical Tower" { + t.Fatalf("expected canonical display name to drive name, got %q", item.Name) + } + if item.DisplayName != "Canonical Tower" { + t.Fatalf("expected canonical display name to drive displayName, got %q", item.DisplayName) + } +} diff --git a/internal/unifiedresources/code_standards_test.go b/internal/unifiedresources/code_standards_test.go index 2a47eeeb7b..9b3d156140 100644 --- a/internal/unifiedresources/code_standards_test.go +++ b/internal/unifiedresources/code_standards_test.go @@ -351,6 +351,28 @@ func TestResourcePolicyLabelHelpersUsedByAIConsumers(t *testing.T) { } } +func TestResourceDisplayNameUsedByInfrastructureConsumers(t *testing.T) { + requiredSnippets := map[string][]string{ + filepath.Join("..", "monitoring", "connected_infrastructure.go"): { + "unifiedresources.ResourceDisplayName(resource)", + }, + filepath.Join(".", "monitored_systems.go"): { + "if name := ResourceDisplayName(*resource); name != \"\" {", + }, + } + for name, snippets := range requiredSnippets { + data, err := os.ReadFile(name) + if err != nil { + t.Fatalf("failed to read %s: %v", name, err) + } + for _, snippet := range snippets { + if !strings.Contains(string(data), snippet) { + t.Fatalf("%s must contain %q", name, snippet) + } + } + } +} + func TestResourceTimelineStoreIndexesSupportFilteredReads(t *testing.T) { data, err := os.ReadFile(filepath.Join("store.go")) if err != nil { diff --git a/internal/unifiedresources/monitored_systems.go b/internal/unifiedresources/monitored_systems.go index 79d7cc3484..2258f1fae9 100644 --- a/internal/unifiedresources/monitored_systems.go +++ b/internal/unifiedresources/monitored_systems.go @@ -238,11 +238,8 @@ func monitoredSystemResourceDisplayName(resource *Resource) string { if resource == nil { return "" } - if resource.Canonical != nil && strings.TrimSpace(resource.Canonical.DisplayName) != "" { - return strings.TrimSpace(resource.Canonical.DisplayName) - } - if strings.TrimSpace(resource.Name) != "" { - return strings.TrimSpace(resource.Name) + if name := ResourceDisplayName(*resource); name != "" { + return name } switch { case resource.Proxmox != nil && strings.TrimSpace(resource.Proxmox.NodeName) != "": diff --git a/internal/unifiedresources/policy_metadata_test.go b/internal/unifiedresources/policy_metadata_test.go index 9b3162cd2b..b0c76b09c3 100644 --- a/internal/unifiedresources/policy_metadata_test.go +++ b/internal/unifiedresources/policy_metadata_test.go @@ -237,6 +237,15 @@ func TestResourcePolicyLabelHelpers(t *testing.T) { } func TestResourceDisplayName(t *testing.T) { + if got := ResourceDisplayName(Resource{ + Name: " node-a ", + ID: "id-a", + Canonical: &CanonicalIdentity{ + DisplayName: "canonical-node-a", + }, + }); got != "canonical-node-a" { + t.Fatalf("ResourceDisplayName() with canonical name = %q, want canonical-node-a", got) + } if got := ResourceDisplayName(Resource{Name: " node-a ", ID: "id-a"}); got != "node-a" { t.Fatalf("ResourceDisplayName() with name = %q, want node-a", got) } diff --git a/internal/unifiedresources/policy_presentation.go b/internal/unifiedresources/policy_presentation.go index 28de6a4014..62d86a2977 100644 --- a/internal/unifiedresources/policy_presentation.go +++ b/internal/unifiedresources/policy_presentation.go @@ -228,6 +228,11 @@ func ResourcePolicyRedactedValue(value string, policy *ResourcePolicy, hints ... // ResourceDisplayName returns the canonical resource display name fallback. func ResourceDisplayName(resource Resource) string { + if resource.Canonical != nil { + if name := strings.TrimSpace(resource.Canonical.DisplayName); name != "" { + return name + } + } if name := strings.TrimSpace(resource.Name); name != "" { return name }