diff --git a/frontend-modern/src/components/Alerts/__tests__/ThresholdsTable.test.tsx b/frontend-modern/src/components/Alerts/__tests__/ThresholdsTable.test.tsx index 5c3797570..c22e9d889 100644 --- a/frontend-modern/src/components/Alerts/__tests__/ThresholdsTable.test.tsx +++ b/frontend-modern/src/components/Alerts/__tests__/ThresholdsTable.test.tsx @@ -345,7 +345,11 @@ describe('ThresholdsTable Resource Rendering', () => { expect(screen.getByTestId('resource-name-h1')).toHaveTextContent('Host 1'); }); - it('renders governed systems with the policy-aware display label', async () => { + it('renders policy-redacted systems with their raw display name (operator-local UI does not redact)', async () => { + // Resource-policy redaction is a transmission-boundary policy (docs/PRIVACY.md): + // it gates what leaves the instance toward non-local model providers, not the + // operator's own browser. The Threshold table must show the same name the + // operator sees on /infrastructure. setPathname('/alerts/thresholds/systems'); const host = { id: 'h2', @@ -367,8 +371,8 @@ describe('ThresholdsTable Resource Rendering', () => { expect(screen.getByTestId('resource-table-Systems')).toBeInTheDocument(); }); - expect(screen.getByTestId('resource-name-h2')).toHaveTextContent('redacted by policy'); - expect(screen.getByTestId('resource-name-h2')).not.toHaveTextContent('secret-host'); + expect(screen.getByTestId('resource-name-h2')).toHaveTextContent('Secret Host'); + expect(screen.getByTestId('resource-name-h2')).not.toHaveTextContent('redacted by policy'); }); it('renders TrueNAS appliances on the canonical systems tab with their disk surface', async () => { @@ -431,7 +435,7 @@ describe('ThresholdsTable Resource Rendering', () => { expect(screen.getByTestId('resource-name-guest1')).toHaveTextContent('vm1'); }); - it('renders governed guests with the policy-aware display label', async () => { + it('renders policy-redacted guests with their raw display name in operator-local UI', async () => { setPathname('/alerts/thresholds/infrastructure'); const guest = { id: 'guest2', @@ -453,11 +457,11 @@ describe('ThresholdsTable Resource Rendering', () => { expect(screen.getByTestId('section-VMs & Containers')).toBeInTheDocument(); }); - expect(screen.getByTestId('resource-name-guest2')).toHaveTextContent('redacted by policy'); - expect(screen.getByTestId('resource-name-guest2')).not.toHaveTextContent('secret-vm-2'); + expect(screen.getByTestId('resource-name-guest2')).toHaveTextContent('Secret VM 2'); + expect(screen.getByTestId('resource-name-guest2')).not.toHaveTextContent('redacted by policy'); }); - it('renders governed guest groups with the policy-aware node header label', async () => { + it('renders policy-redacted guest-group node headers with the raw display name', async () => { setPathname('/alerts/thresholds/infrastructure'); const node = { id: 'node-governed', @@ -489,11 +493,15 @@ describe('ThresholdsTable Resource Rendering', () => { expect(screen.getByTestId('group-header-0')).toBeInTheDocument(); }); - expect(screen.getByTestId('group-header-0')).toHaveTextContent('redacted by policy'); - expect(screen.getByTestId('group-header-0')).not.toHaveTextContent('secret-node'); + // Group headers run the same friendly-node normalizer as non-policied nodes + // (see "PVE Node 1" -> "PVE" earlier), so "Secret Node" friendly-shortens to + // "Secret". The point of this test is that the redacted aiSafeSummary does + // NOT replace the node header in operator-local UI. + expect(screen.getByTestId('group-header-0')).toHaveTextContent('Secret'); + expect(screen.getByTestId('group-header-0')).not.toHaveTextContent('redacted by policy'); }); - it('renders governed storage with the policy-aware display label', async () => { + it('renders policy-redacted storage with its raw display name in operator-local UI', async () => { setPathname('/alerts/thresholds/infrastructure'); const storage = { id: 'storage1', @@ -514,11 +522,11 @@ describe('ThresholdsTable Resource Rendering', () => { expect(screen.getByTestId('resource-name-storage1')).toBeInTheDocument(); }); - expect(screen.getByTestId('resource-name-storage1')).toHaveTextContent('redacted by policy'); - expect(screen.getByTestId('resource-name-storage1')).not.toHaveTextContent('secret-datastore'); + expect(screen.getByTestId('resource-name-storage1')).toHaveTextContent('Secret Datastore'); + expect(screen.getByTestId('resource-name-storage1')).not.toHaveTextContent('redacted by policy'); }); - it('renders governed docker containers with the policy-aware display label', async () => { + it('renders policy-redacted docker containers with their raw display name in operator-local UI', async () => { setPathname('/alerts/thresholds/containers'); const dockerHost = { id: 'docker-host-1', @@ -564,10 +572,10 @@ describe('ThresholdsTable Resource Rendering', () => { expect( screen.getByTestId('resource-name-docker:docker-host-1/container-governed'), - ).toHaveTextContent('redacted by policy'); + ).toHaveTextContent('Secret Nginx'); expect( screen.getByTestId('resource-name-docker:docker-host-1/container-governed'), - ).not.toHaveTextContent('secret-nginx'); + ).not.toHaveTextContent('redacted by policy'); }); it('renders TrueNAS app containers under canonical container runtimes without Docker-only controls', async () => { @@ -616,7 +624,7 @@ describe('ThresholdsTable Resource Rendering', () => { expect(screen.queryByText('Swarm service alerts')).not.toBeInTheDocument(); }); - it('renders governed agent disk node labels with the policy-aware display label', async () => { + it('renders policy-redacted agent disk node labels with the raw display name in operator-local UI', async () => { setPathname('/alerts/thresholds/systems'); const host = { id: 'agent-governed', @@ -645,11 +653,11 @@ describe('ThresholdsTable Resource Rendering', () => { }); expect(screen.getByTestId('resource-node-agent:agent-governed/disk:var-lib')).toHaveTextContent( - 'redacted by policy', + 'Secret Host', ); expect( screen.getByTestId('resource-node-agent:agent-governed/disk:var-lib'), - ).not.toHaveTextContent('secret-host'); + ).not.toHaveTextContent('redacted by policy'); }); }); diff --git a/frontend-modern/src/features/alerts/helpers.ts b/frontend-modern/src/features/alerts/helpers.ts index 658cf40fa..26cb2981c 100644 --- a/frontend-modern/src/features/alerts/helpers.ts +++ b/frontend-modern/src/features/alerts/helpers.ts @@ -2,7 +2,7 @@ import type { EmailConfig } from '@/api/notifications'; import type { Resource, ResourceType } from '@/types/resource'; import type { RawOverrideConfig, HysteresisThreshold } from '@/types/alerts'; import { getResourceTypeLabel } from '@/utils/resourceTypePresentation'; -import { getPreferredResourceDisplayName } from '@/utils/resourceIdentity'; +import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity'; import { MAX_ALERTS_MIN, MAX_ALERTS_MAX, @@ -336,7 +336,12 @@ export const guessNumericId = (value: string): number => { export const getAlertResourceDisplayLabel = (resource: Resource, fallback?: string): string => (() => { - const preferred = getPreferredResourceDisplayName(resource); + // Alert threshold tables, override pickers, and incident panels are operator-facing + // local UI. Per docs/PRIVACY.md, resource-policy redaction applies before non-local + // model requests leave the instance — not to the operator's own browser. Use the + // raw infrastructure display name so a Tower-policied row reads "Tower" here, the + // same as on /infrastructure and /storage. + const preferred = getPreferredInfrastructureDisplayName(resource); if (preferred && preferred !== resource.id) { return preferred; } diff --git a/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsDockerData.ts b/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsDockerData.ts index efb0485a0..72df986db 100644 --- a/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsDockerData.ts +++ b/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsDockerData.ts @@ -32,7 +32,7 @@ export function useThresholdsDockerData(inputs: ThresholdsDataInputs) { const hosts: TableResource[] = (props.containerRuntimes ?? []).map((host) => { const idCandidates = dockerHostOverrideIdCandidates(host); const originalName = getAlertResourceDisplayLabel(host); - const friendlyName = getFriendlyAlertNodeName(originalName, host.policy); + const friendlyName = getFriendlyAlertNodeName(originalName); const override = findOverrideByCandidates(overridesMap, idCandidates); const resourceId = override?.id || idCandidates[0] || host.id; const disableConnectivity = override?.disableConnectivity || false; @@ -113,7 +113,7 @@ export function useThresholdsDockerData(inputs: ThresholdsDataInputs) { const dockerHostIds = dockerHostOverrideIdCandidates(host); const dockerHostIdForActions = dockerHostIds[0] || host.id; const hostLabel = getAlertResourceDisplayLabel(host); - const friendlyHostName = getFriendlyAlertNodeName(hostLabel, host.policy); + const friendlyHostName = getFriendlyAlertNodeName(hostLabel); const hostLabelLower = hostLabel.toLowerCase(); const friendlyHostNameLower = friendlyHostName.toLowerCase(); const hostHostname = getPreferredResourceHostname(host); @@ -230,7 +230,7 @@ export function useThresholdsDockerData(inputs: ThresholdsDataInputs) { const meta: Record = {}; (props.containerRuntimes ?? []).forEach((host) => { const originalName = getAlertResourceDisplayLabel(host); - const friendlyName = getFriendlyAlertNodeName(originalName, host.policy); + const friendlyName = getFriendlyAlertNodeName(originalName); const headerMeta: GroupHeaderMeta = { displayName: friendlyName, rawName: originalName, diff --git a/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsHostData.ts b/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsHostData.ts index 243b911be..86429fa25 100644 --- a/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsHostData.ts +++ b/frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsHostData.ts @@ -43,7 +43,7 @@ export function useThresholdsHostData(inputs: ThresholdsDataInputs) { const hasNote = Boolean(note && note.trim().length > 0); const originalDisplayName = getAlertResourceDisplayLabel(node); - const friendlyName = getFriendlyAlertNodeName(originalDisplayName, node.policy, clusterName); + const friendlyName = getFriendlyAlertNodeName(originalDisplayName, clusterName); const rawName = node.name; const sanitizedName = friendlyName || originalDisplayName || rawName.split('.')[0] || rawName; const guestUrlValue = typeof data?.guestURL === 'string' ? data.guestURL.trim() : ''; diff --git a/frontend-modern/src/features/alerts/thresholds/thresholdsResourceModel.ts b/frontend-modern/src/features/alerts/thresholds/thresholdsResourceModel.ts index 2bcc491b9..762d1f4ca 100644 --- a/frontend-modern/src/features/alerts/thresholds/thresholdsResourceModel.ts +++ b/frontend-modern/src/features/alerts/thresholds/thresholdsResourceModel.ts @@ -1,7 +1,6 @@ import { unwrap } from 'solid-js/store'; import { getPreferredResourceHostname } from '@/utils/resourceIdentity'; -import { requiresGovernedResourceDisplay } from '@/types/resource'; import type { Resource } from '@/types/resource'; import { getAgentDiscoveryResourceId, @@ -149,9 +148,8 @@ export const getFriendlyNodeName = (value: string, clusterName?: string): string export const getFriendlyAlertNodeName = ( value: string, - policy?: Resource['policy'], clusterName?: string, -): string => (requiresGovernedResourceDisplay(policy) ? value : getFriendlyNodeName(value, clusterName)); +): string => getFriendlyNodeName(value, clusterName); export function buildNodeHeaderMeta(node: Resource) { const data = platformData(node); @@ -160,7 +158,7 @@ export function buildNodeHeaderMeta(node: Resource) { (data?.isClusterMember as boolean | undefined) ?? Boolean(node.clusterId); const originalDisplayName = getAlertResourceDisplayLabel(node); - const friendlyName = getFriendlyAlertNodeName(originalDisplayName, node.policy, clusterName); + const friendlyName = getFriendlyAlertNodeName(originalDisplayName, clusterName); const guestUrlValue = typeof data?.guestURL === 'string' ? data.guestURL.trim() : ''; const hostValue = typeof data?.host === 'string' ? data.host.trim() : ''; diff --git a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts index 387db9c2f..bf7035857 100644 --- a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts +++ b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts @@ -173,7 +173,12 @@ describe('normalizeMetricDelayMap', () => { }); describe('alert resource display labels', () => { - it('uses the governed aiSafeSummary when policy requires redaction', () => { + it('returns the raw display name in operator-facing alert UI even when policy requires redaction', () => { + // Alert threshold tables, override pickers, and incident panels are local UI for + // the operator. Resource-policy redaction is a transmission-boundary policy + // (docs/PRIVACY.md): it gates non-local model requests, not the operator's own + // browser. The same row that reads "Secret Host" on /infrastructure must read + // "Secret Host" here, not "redacted by policy". const resource = { id: 'resource-1', name: 'secret-host', @@ -189,7 +194,7 @@ describe('alert resource display labels', () => { aiSafeSummary: 'redacted by policy', } as unknown as Resource; - expect(getAlertResourceDisplayLabel(resource)).toBe('redacted by policy'); + expect(getAlertResourceDisplayLabel(resource)).toBe('Secret Host'); }); it('falls back to the provided alert-specific fallback when needed', () => {