Link alert cards to their platform from incident metadata (#2564)

The alert card chose its platform page by resource type, then by
searching the message for "vmware". Provider incidents sit on canonical
`vm`, `storage`, `network` and `agent` types, so that only worked while
a summary happened to name the platform. #2559 added an
`incidentProvider === 'vmware'` check for vSphere alone. On main, other
platforms still fall through:

- **TrueNAS pool incidents** link to the Proxmox overview, and Alerts ›
Overview › More › Monitoring says "Proxmox remains the inventory owner".
- **Agent alerts** link to `/machines`. No such route exists, so the
link opens "Page not found: No route matched /machines".
- **TrueNAS metric alerts** (`truenas-pool`, `truenas-dataset`, …) fall
through to Proxmox the same way.

**What changes**
- The card reads the platform from an explicit `platformType`, then the
incident's `incidentProvider`, then the resource's `resourceSources`,
through the shared `resolvePlatformTypeFromSources`. A PBS incident that
Pulse raises itself (provider `pulse`) resolves by its `pbs` source.
- That platform maps to its page through
`PRIMARY_PLATFORM_NAV_SCOPE_IDS` and the `routing/resourceLinks.ts` path
builders, and the same canonical id goes to the Monitoring policy menu,
so the link and the named owner agree. Raw route strings in the card are
gone.
- The resource-type and message fallbacks remain only for alerts whose
metadata names no platform, now with `truenas-` types, and with agent
alerts sent to `/standalone/machines`.
- vSphere links no longer depend on the alarm wording at all. #2559
already made them right on main by the provider check; this replaces
that special case with the general resolver.

**Verification**
- `AlertOverviewAlertCard.system-scope.test.tsx`: a vSphere VM alarm
whose message lacks "VMware", a TrueNAS pool incident, a PBS incident
with provider `pulse`, and agent and TrueNAS metric alerts without
incident metadata. Each asserts the link and the platform passed to the
Monitoring menu. All five fail against main's card; against the card
from before #2559, the vSphere VM alarm linked to `/proxmox/overview`.
- Private mock stack at main `af4e6a493c`, 1440 and 390: a vCenter alarm
on the mock VM client-portal-01 (message "Virtual machine memory usage")
and an agent-host CPU alert, appended to the live websocket
`activeAlerts` with Playwright `routeWebSocket`, plus the mock's own
TrueNAS pool incident. Each link was followed to the vSphere overview,
TrueNAS overview and Machines, and each menu named VMware vSphere,
TrueNAS and Pulse agent. With main's card swapped back in under Vite,
the TrueNAS card went to Proxmox with Proxmox as owner and the agent
card hit Page not found.
- Receipt: `frontend-modern/browser-verification/977a32825e65be8f.json`.
This commit is contained in:
rcourtman
2026-10-06 12:15:15 +01:00
committed by GitHub
5 changed files with 250 additions and 23 deletions
@@ -2432,11 +2432,21 @@ line; held delivery states that can surprise an operator use the attention
tone, while cooldown, quiet-hours, monitor-only, and successful/pending states
remain neutral.
The card's resource link resolves the owning platform page from alert
metadata, never from message wording alone: a provider incident whose
`incidentProvider` is `vmware` links to the vSphere overview even though its
message is only the alarm vCenter raised, and VMware incidents land on
metadata, never from message wording alone. Provider incidents land on
canonical `vm`, `storage`, `network`, and `agent` resource types rather than a
`vmware-` prefixed type.
platform-prefixed type, so the card reads the platform from an explicit
`platformType`, then the incident's `incidentProvider`, then the resource's
`resourceSources`, all through `resolvePlatformTypeFromSources` in
`frontend-modern/src/utils/sourcePlatforms.ts`: a vCenter alarm whose message
is only the alarm's name links to the vSphere overview, a TrueNAS pool
incident to TrueNAS, and a PBS incident that Pulse itself raised (provider
`pulse`) to Proxmox by its `pbs` source. The resolved platform maps to its
page through `PRIMARY_PLATFORM_NAV_SCOPE_IDS` and the `routing/resourceLinks.ts`
path builders, and the same canonical platform id goes to the Monitoring
policy menu, so the link and the named inventory owner cannot disagree. Only
an alert whose metadata names no platform falls back to its resource type
(`agent`, `docker-*`, `k8s-*`, `truenas-*`, `vmware-*`) and finally the message;
agent alerts open `/standalone/machines`, because no `/machines` route exists.
The retired dashboard recent-alert panel must not be reintroduced as a
parallel alert surface. Alert summary/tone copy belongs to the alert overview
presentation owner, and any future compact alert surface must compose the
@@ -4334,6 +4334,14 @@ Storage / Recovery hrefs) were deleted from
panel and Patrol findings panel that consumed them now keep investigation
in-place through their existing handoff buttons and inline actions. Future
cross-surface drilldown chips must not reanimate the legacy helpers.
An alert card's single resource link opens the owning platform's page. It
resolves the platform through the shared `utils/sourcePlatforms.ts` resolver,
maps it to a page through `PRIMARY_PLATFORM_NAV_SCOPE_IDS` in
`features/platformNavigation/platformNavigationModel.ts`, and builds the href
with the `routing/resourceLinks.ts` path builders. It must not freeze raw
route strings or keep its own platform alias table, because those drifted
before: agent alerts linked to a `/machines` route that never existed, and
TrueNAS incidents linked to Proxmox.
Command palette and keyboard shortcuts moved to platform-first on 2026-05-16,
and top-level aggregate workspace routes were retired on 2026-05-25
@@ -0,0 +1,42 @@
{
"version": 1,
"base_sha": "af4e6a493cbca1e6c7d4397964efb146e950fca2",
"verified_at": "2026-10-06T10:36:32Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/features/alerts/AlertOverviewAlertCard.tsx"
],
"content_sha256": {
"frontend-modern/src/features/alerts/AlertOverviewAlertCard.tsx": "73b1cd6cb17494a95c266697755f68537d3d71cac6bb6cb3f7ce9dda5b892f45"
},
"routes": [
"/alerts/overview",
"/vmware/overview",
"/truenas/overview",
"/standalone/machines"
],
"viewports": [
{
"width": 1440,
"height": 900
},
{
"width": 390,
"height": 844
}
],
"states": [
"Alerts > Overview on a private mock stack (backend :7711, sidecar Vite :5231) at main af4e6a493c with the change. A vCenter alarm on the real mock vSphere VM client-portal-01 (vm-aceb0f0c3469f43a; metadata resourceType vm, incidentProvider vmware, incidentCode vmware_alarm_state, resourceSources [vmware]; message 'Virtual machine memory usage', no 'VMware' in it) was appended to the live websocket activeAlerts snapshots, because the mock scenario clears VM alarms. An agent-host CPU alert on the real agent-only mock host Apollo-114 (metadata resourceType agent, no incident metadata) was appended the same way",
"The mock's own TrueNAS pool incident on 'archive' (zfs-pool-state, incidentProvider truenas, resourceSources [truenas]), not injected",
"With the change, at 1440 and 390: the vSphere VM alarm card links to /vmware/overview and its More > Monitoring menu reads 'VMware vSphere remains the inventory owner' and 'VMware vSphere keeps the inventory record'; the TrueNAS pool card links to /truenas/overview and the menu names TrueNAS; the agent-host card links to /standalone/machines and the menu names Pulse agent",
"Each link followed to a rendered platform page: the vSphere overview, the TrueNAS overview, and Machines listing Apollo-114; none showed Page not found",
"Same session with main's AlertOverviewAlertCard.tsx swapped back in under Vite: the vSphere VM alarm was already right on main (#2559 + #2560), the TrueNAS pool card linked to /proxmox/overview with 'Proxmox remains the inventory owner', and the agent-host card linked to /machines, which rendered 'Page not found: No route matched /machines', at 1440 and 390",
"Monitoring menu at 390 opens as a body portal within the viewport"
],
"interactions": [
"Proxied the real websocket with Playwright routeWebSocket and appended the two alerts to each activeAlerts array; also appended them to the /api/alerts/active REST response",
"Clicked More, then Monitoring, on each of the three cards and read the menu header and the Retire from monitoring description",
"Clicked each card's resource link, recorded the landed route, checked for the Page not found state, screenshotted the page, then went back",
"Swapped AlertOverviewAlertCard.tsx to main's version under the running Vite for a same-session before, then restored the change"
]
}
@@ -34,6 +34,29 @@ import { ResourceMonitoringPolicyAction } from './ResourceMonitoringPolicyAction
import { AlertSnoozeAction } from './AlertSnoozeAction';
import { isAlertSnoozed } from './useAlertSnoozeState';
import { isPulseSystemAlert } from '@/utils/alertScope';
import {
normalizeSourcePlatformQueryValue,
resolvePlatformTypeFromSources,
} from '@/utils/sourcePlatforms';
import {
DOCKER_PATH,
KUBERNETES_PATH,
PROXMOX_PATH,
STANDALONE_PATH,
TRUENAS_PATH,
VMWARE_PATH,
buildDockerPath,
buildKubernetesPath,
buildProxmoxPath,
buildStandalonePath,
buildTrueNASPath,
buildVmwarePath,
} from '@/routing/resourceLinks';
import {
PRIMARY_PLATFORM_NAV_IDS,
PRIMARY_PLATFORM_NAV_SCOPE_IDS,
type PrimaryPlatformNavId,
} from '@/features/platformNavigation/platformNavigationModel';
interface AlertOverviewAlertCardProps {
alert: Alert;
@@ -48,6 +71,25 @@ interface AlertOverviewAlertCardProps {
// in one banner instead of on each card.
const GLOBAL_DELIVERY_HOLD_REASONS = new Set(['notifications_inactive', 'notifications_disabled']);
// The page that lists each primary platform's resources, keyed by nav id so
// a new platform page cannot ship without an alert link.
const PLATFORM_PAGE_PATHS: Record<PrimaryPlatformNavId, string> = {
proxmox: buildProxmoxPath(),
docker: buildDockerPath(),
kubernetes: buildKubernetesPath(),
truenas: buildTrueNASPath(),
vmware: buildVmwarePath(),
standalone: buildStandalonePath(),
};
const platformPagePath = (platform: string | undefined): string | undefined => {
if (!platform) return undefined;
const navId = PRIMARY_PLATFORM_NAV_IDS.find((id) =>
PRIMARY_PLATFORM_NAV_SCOPE_IDS[id].includes(platform),
);
return navId ? PLATFORM_PAGE_PATHS[navId] : undefined;
};
export function AlertOverviewAlertCard(props: AlertOverviewAlertCardProps) {
const alertKey = () => getCanonicalAlertId(props.alert);
const hasResource = () =>
@@ -80,40 +122,61 @@ export function AlertOverviewAlertCard(props: AlertOverviewAlertCardProps) {
(isAlertSnoozed(props.alert) && Boolean(props.alert.operationalRecord?.suppression?.expiresAt));
const timelineOpen = () => props.timelineState.expandedIncidents().has(alertKey());
// Incident alerts land on canonical agent/vm/storage/network resources, so
// the resource type alone cannot name the platform. Their metadata carries
// the incident's provider and the resource's sources, which do, whatever
// the message says.
const metadataPlatform = (): string | undefined => {
const metadata = props.alert.metadata;
const explicit =
typeof metadata?.platformType === 'string'
? normalizeSourcePlatformQueryValue(metadata.platformType)
: '';
if (explicit) return explicit;
const provider =
typeof metadata?.incidentProvider === 'string'
? resolvePlatformTypeFromSources([metadata.incidentProvider])
: undefined;
if (provider) return provider;
const sources = Array.isArray(metadata?.resourceSources)
? metadata.resourceSources.filter((source): source is string => typeof source === 'string')
: [];
return resolvePlatformTypeFromSources(sources);
};
const resourceLink = (): string => {
const platformPage = platformPagePath(metadataPlatform());
if (platformPage) return platformPage;
const rid = props.alert.resourceId ?? '';
const resourceType =
typeof props.alert.metadata?.resourceType === 'string'
? (props.alert.metadata.resourceType as string)
: '';
// vCenter incidents land on canonical agent/vm/storage/network resources
// and say only what vCenter flagged, so the incident's provider names the
// page, ahead of the generic resource-type routes: an ESXi host is an
// `agent` resource that the Machines page does not list.
if (props.alert.metadata?.incidentProvider === 'vmware') return '/vmware/overview';
if (rid.startsWith('agent:') || resourceType === 'agent') return '/machines';
if (rid.startsWith('agent:') || resourceType === 'agent') return buildStandalonePath();
if (
rid.includes('docker') ||
resourceType === 'docker-container' ||
resourceType === 'docker-host'
)
return '/docker/overview';
return buildDockerPath();
if (resourceType === 'kubernetes' || resourceType.startsWith('k8s-'))
return '/kubernetes/overview';
return buildKubernetesPath();
if (resourceType.startsWith('truenas-')) return buildTrueNASPath();
if (resourceType.startsWith('vmware-') || props.alert.message?.toLowerCase().includes('vmware'))
return '/vmware/overview';
return '/proxmox/overview';
return buildVmwarePath();
return buildProxmoxPath();
};
const platformTypeForPolicy = (): string | undefined => {
const metadataPlatform = props.alert.metadata?.platformType;
if (typeof metadataPlatform === 'string' && metadataPlatform.trim()) return metadataPlatform;
const platform = metadataPlatform();
if (platform) return platform;
const link = resourceLink();
if (link.startsWith('/proxmox')) return 'proxmox';
if (link.startsWith('/docker')) return 'docker';
if (link.startsWith('/kubernetes')) return 'kubernetes';
if (link.startsWith('/vmware')) return 'vmware';
if (link.startsWith('/machines')) return 'agent';
if (link.startsWith(PROXMOX_PATH)) return 'proxmox';
if (link.startsWith(DOCKER_PATH)) return 'docker';
if (link.startsWith(KUBERNETES_PATH)) return 'kubernetes';
if (link.startsWith(TRUENAS_PATH)) return 'truenas';
if (link.startsWith(VMWARE_PATH)) return 'vmware';
if (link.startsWith(STANDALONE_PATH)) return 'agent';
return undefined;
};
@@ -124,7 +187,7 @@ export function AlertOverviewAlertCard(props: AlertOverviewAlertCardProps) {
if (resourceId.startsWith('agent:')) return 'agent';
if (resourceId.includes('docker')) return 'app-container';
if (resourceId.includes('k8s') || resourceId.includes('kubernetes')) return 'pod';
if (resourceLink() === '/proxmox/overview') return 'vm';
if (resourceLink() === buildProxmoxPath()) return 'vm';
return undefined;
};
@@ -15,7 +15,9 @@ vi.mock('@/components/Alerts/InvestigateAlertButton', () => ({
InvestigateAlertButton: () => null,
}));
vi.mock('../ResourceMonitoringPolicyAction', () => ({
ResourceMonitoringPolicyAction: () => <button>Resource monitoring policy</button>,
ResourceMonitoringPolicyAction: (props: any) => (
<button data-platform-type={props.platformType}>Resource monitoring policy</button>
),
}));
vi.mock('../AlertSnoozeAction', () => ({ AlertSnoozeAction: () => null }));
import { AlertOverviewAlertCard } from '../AlertOverviewAlertCard';
@@ -136,6 +138,108 @@ describe('Pulse system-alert overview scope', () => {
);
});
it.each([
{
name: 'a vCenter alarm on a VM whose message does not say VMware',
resourceId: 'vm:vc-1:vm-2041',
resourceName: 'sql-prod-01',
message: 'Virtual machine memory usage on sql-prod-01 (red)',
metadata: {
resourceType: 'vm',
incidentProvider: 'vmware',
incidentCode: 'vmware_alarm_state',
resourceSources: ['vmware'],
},
href: '/vmware/overview',
platformType: 'vmware-vsphere',
},
{
name: 'a TrueNAS pool incident',
resourceId: 'storage:truenas-1:tank',
resourceName: 'tank',
message: 'Pool tank is degraded',
metadata: {
resourceType: 'storage',
incidentProvider: 'truenas',
incidentCode: 'pool_degraded',
resourceSources: ['truenas'],
},
href: '/truenas/overview',
platformType: 'truenas',
},
{
name: 'a PBS datastore incident whose provider is Pulse itself',
resourceId: 'storage:pbs-1:main',
resourceName: 'main',
message: 'Datastore main is nearly full',
metadata: {
resourceType: 'storage',
incidentProvider: 'pulse',
incidentCode: 'capacity_runway_low',
resourceSources: ['pbs'],
},
href: '/proxmox/overview',
platformType: 'proxmox-pbs',
},
])(
'links $name to its platform page and policy owner',
({ resourceId, resourceName, message, metadata, href, platformType }) => {
const alert = makeSystemAlert('resource-incident', {
id: resourceId,
resourceId,
resourceName,
message,
metadata,
});
render(() => (
<AlertOverviewAlertCard alert={alert} state={state} timelineState={timelineState} />
));
expect(screen.getByRole('link', { name: resourceName })).toHaveAttribute('href', href);
fireEvent.click(screen.getByRole('button', { name: 'More' }));
expect(screen.getByText('Resource monitoring policy')).toHaveAttribute(
'data-platform-type',
platformType,
);
},
);
it.each([
{
name: 'an agent host metric alert to Machines',
resourceId: 'agent:host-12',
resourceType: 'agent',
href: '/standalone/machines',
platformType: 'agent',
},
{
name: 'a TrueNAS pool metric alert to TrueNAS',
resourceId: 'truenas-1:pool:tank',
resourceType: 'truenas-pool',
href: '/truenas/overview',
platformType: 'truenas',
},
])('links $name when metadata names no platform', (example) => {
const alert = makeSystemAlert('usage', {
id: example.resourceId,
resourceId: example.resourceId,
resourceName: 'monitored-resource',
message: 'Usage at 92%',
metadata: { resourceType: example.resourceType },
});
render(() => (
<AlertOverviewAlertCard alert={alert} state={state} timelineState={timelineState} />
));
expect(screen.getByRole('link', { name: 'monitored-resource' })).toHaveAttribute(
'href',
example.href,
);
fireEvent.click(screen.getByRole('button', { name: 'More' }));
expect(screen.getByText('Resource monitoring policy')).toHaveAttribute(
'data-platform-type',
example.platformType,
);
});
it('closes the open timeline when the disclosure is collapsed', () => {
const alert = makeSystemAlert('cpu', { id: 'open-timeline', resourceId: 'vm-pulse' });
const openTimeline = {