mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-23 16:03:30 +00:00
Stop redacting resource names in operator-local alert UI
Alert threshold tables, override pickers, and incident panels were running every resource name through the policy-governed display helper. A Tower-policied row read "agent (warning)" in /settings -> Alerts -> Thresholds even though the same resource read "Tower" on /infrastructure and /storage Pools, which already use the raw infrastructure display name. Per docs/PRIVACY.md, resource-policy redaction applies "Before non-local model requests leave the instance" -- it gates AI-bound, cross-org, and relay paths, not the operator's own browser. Redacting in local /settings UI was inconsistent with the policy intent and with the rest of the operator surface. Switch the alert-resource label helper to getPreferredInfrastructureDisplayName so threshold rows match Infrastructure and Storage. Drop the now-dead governance gate inside getFriendlyAlertNodeName (and the unused policy parameter all three threshold hooks were threading through). Invert the six ThresholdsTable test fixtures plus the alerts-helpers test to assert the new local-UI invariant. Cloud-bound surfaces (AI Chat, organization sharing, the resource-detail drawer's governance meta panel) still call getPreferredResourceDisplayName and continue to redact -- their tests are unchanged.
This commit is contained in:
parent
b2dfae0715
commit
abdde303ac
6 changed files with 46 additions and 30 deletions
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, GroupHeaderMeta> = {};
|
||||
(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,
|
||||
|
|
|
|||
|
|
@ -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() : '';
|
||||
|
|
|
|||
|
|
@ -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() : '';
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue