Keep Machines limited to Pulse Agent resources

This commit is contained in:
rcourtman 2026-05-26 07:45:16 +01:00
parent 3561803d76
commit 1e16cf34f5
10 changed files with 73 additions and 83 deletions

View file

@ -240,11 +240,11 @@ pause, test, edit, and remove.
An availability target classified as `machine` is still agentless unless a
Pulse Agent actually registers for that host. That classification exists so
servers, desktops, laptops, and comparable computers such as a Mac mini can
appear in Standalone Machines when monitored by reachability probes; it must
not create lifecycle authority, install state, agent profile membership, or
host-command capability. A `targetKind=machine` add-dialog handoff is only a
preselected availability-resource creation path, not an agent install or
lifecycle onboarding state.
use machine-specific reachability copy in Availability checks; it must not
create Standalone Machines membership, lifecycle authority, install state,
agent profile membership, or host-command capability. A `targetKind=machine`
add-dialog handoff is only a preselected availability-resource creation path,
not an agent install or lifecycle onboarding state.
The lifecycle-owned onboarding presentation helper must consume the governed
platform support manifest for readiness stage, primary mode, canonical
projections, and support-floor posture.

View file

@ -436,12 +436,13 @@ the canonical monitored-system blocked payload.
`frontend-modern/src/api/availabilityTargets.ts`, and the unified-resource
availability payload together. The bounded `targetKind` vocabulary is
`machine`, `service`, and `device`; missing legacy values default to
`service`, and browser consumers may place only explicitly classified
`machine` targets into Standalone Machines. Browser add-dialog routes may
carry a `targetKind` query value to preselect that bounded kind, but the
persisted contract remains the `AvailabilityTarget.targetKind` payload.
API clients must not infer that classification from protocol, hostname,
port, or row label.
`service`. Browser add-dialog routes may carry a `targetKind` query value
to preselect that bounded kind, but the persisted contract remains the
`AvailabilityTarget.targetKind` payload. Browser consumers must not place
availability targets into Standalone Machines from that classification;
Machines membership requires Pulse Agent resource evidence. API clients
must not infer that classification from protocol, hostname, port, or row
label.
3. Add dedicated contract tests for new stable payloads
Unified resource type-filter and organization-share resource type additions
must route through `internal/api/resources.go`, `internal/api/org_handlers.go`,

View file

@ -2836,7 +2836,9 @@ availability settings at `/settings/monitoring/availability`, with
Machine entry points must add `targetKind=machine`, while the focused
availability checks entry points for services and devices must add
`targetKind=service`, so deep links open the same owned dialog with the correct
bounded target kind already selected.
bounded target kind already selected. That target kind only scopes the
availability form copy and payload; it must not make agentless reachability
targets eligible for the Machines table.
Former nested aliases such as `/settings/infrastructure/install`,
`/settings/infrastructure/platforms/proxmox/pbs`,
`/settings/infrastructure/api/pve`, and `/settings/workloads/docker` must fail
@ -3590,16 +3592,16 @@ metric slot, while keeping recent check timing and fuller failure context in
the tooltip or drawer so operators can understand what was measured without
duplicated row chrome.
Operational navigation for those agentless endpoints belongs to the
frontend-primitives-owned Machines surface rather than a new primary nav item.
The page may show availability checks beside standalone Pulse Agent machines and
provide a focused availability tab, but Settings remains the add/edit owner and
frontend-primitives-owned Machines surface as a focused Availability checks tab
rather than a new primary nav item. The page may show availability checks beside
standalone Pulse Agent machines, but Settings remains the add/edit owner and
the app shell must not add a separate top-level Availability destination.
The Machines page must not pretend its machine list is a generic overview:
the default tab is `Machines`, and the full availability-check row list belongs
the default tab is `Machines`, the Machines table is only for Pulse Agent-backed
resources with host telemetry, and the full availability-check row list belongs
to the `Availability checks` tab. Servers, laptops, desktops, and comparable
computers that are monitored by agentless reachability checks are still
machines when the availability target is classified as `machine`; service and
device checks stay in the focused availability tab and may only trigger a
compact handoff from Machines. The Machines empty and handoff actions must use
the machine-targeted availability add route, while Availability checks actions
must use the service/device-targeted route.
computers monitored only by agentless reachability checks may use
`targetKind=machine` in the availability form, but they stay in Availability
checks until a Pulse Agent registers and supplies CPU, memory, disk, and network
telemetry. Machines empty and handoff actions must lead to Pulse Agent install
or the Availability checks tab, not to an agentless machine row in Machines.

View file

@ -801,9 +801,10 @@ recovery scope, or a storage/recovery-owned secret source.
32. Keep agentless availability endpoints neutral on the shared unified-resource and API contracts. When `internal/api/availability_handlers.go`, `internal/api/connections_handlers.go`, `internal/api/platform_mock_connections.go`, or `frontend-modern/src/hooks/useUnifiedResources.ts` surface `network-endpoint` availability resources, storage and recovery may consume their liveness as infrastructure context only; they must not reinterpret ping/TCP/HTTP endpoints as storage providers, backup targets, recovery repositories, or protected-workload evidence.
That neutrality includes availability targets whose `targetKind` is
`machine`. A Linux server, desktop, laptop, or Mac mini monitored by an
agentless reachability check may appear in Standalone Machines, but storage
and recovery must still treat the row as liveness context unless a separate
storage/recovery-owned relationship ties it to backup or restore evidence.
agentless reachability check still belongs to Availability checks rather
than Standalone Machines; storage and recovery must treat the row as
liveness context unless a separate storage/recovery-owned relationship ties
it to backup or restore evidence.
33. Keep infrastructure summary chart bucketing and short response caching presentation-only on the adjacent shared API boundary. When `internal/api/router.go` normalizes mixed-cadence infrastructure history into equal-time summary buckets or serves a cached summary payload for repeated operator-facing summary-card requests, storage and recovery may consume the resulting visual context only; they must not reinterpret those normalized chart samples, cached timestamps, or cache hits as recovery freshness windows, backup cadence, or restore evidence.
34. Keep workload chart downsampling and short response caching presentation-only on that same adjacent shared API boundary. When `internal/api/router.go` caps mixed-cadence workload history into equal-time buckets or serves a cached workload-summary payload for repeated operator-facing workload-card requests, storage and recovery may consume the resulting visual context only; they must not reinterpret those shaped chart samples, cached timestamps, or cache hits as recovery freshness windows, backup cadence, or restore evidence.
The same adjacent chart boundary now covers compact storage capacity
@ -1363,10 +1364,10 @@ storage or recovery inventory. Storage/recovery consumers may receive
they must not fold those endpoints into protected-item counts, storage health
rollups, recovery evidence, or storage/recovery licensing or readiness
messages unless a separately governed storage/recovery relationship is added.
The agentless `machine` target kind does not change that boundary: it is a
Standalone presentation classification for computer-shaped reachability targets,
not storage ownership, repository membership, backup coverage, or restore
authority.
The agentless `machine` target kind does not change that boundary: it is
availability presentation vocabulary for computer-shaped reachability targets,
not Standalone Machines membership, storage ownership, repository membership,
backup coverage, or restore authority.
That same owned summary path now also runs through
`useStorageSummaryCharts.ts`: the storage page owns one page-scoped summary
range and one shared storage-summary history fetch, and both the sticky

View file

@ -742,8 +742,9 @@ AI-only summary payloads, or page-local heuristics.
add handoffs must include `targetKind=machine`; service and device check
handoffs must include `targetKind=service`. Saved availability targets carry
a `targetKind` so agentless servers, laptops, desktops, and comparable
computers can participate in the Machines inventory without reclassifying
service or device checks as machines.
computers can use machine-specific reachability copy in Availability checks
without reclassifying service or device checks as machines. Machines
inventory membership still requires Pulse Agent resource evidence.
6. Keep infrastructure source visibility on canonical unified-resource truth.
Settings infrastructure source filters and summaries must preserve known
sources such as `truenas` and `availability` through the shared source
@ -2804,8 +2805,9 @@ redirect. Canonical infrastructure management lives under
lives under `/settings/monitoring/availability`; day-to-day resource
inspection lives on the platform/runtime pages named by the
frontend-primitives-owned IA contract, with unified resources projecting
standalone agent machines and agentless endpoint checks for the Machines
consumer. Future infrastructure work must extend those owners rather than
standalone agent machines for the Machines consumer and agentless endpoint
checks for the Availability checks consumer. Future infrastructure work must
extend those owners rather than
recreating
`frontend-modern/src/features/infrastructure/`, a `/infrastructure` route, or
a separate top-level availability route.

View file

@ -204,7 +204,7 @@ export const AgentsMachinesTable: Component<{
<PlatformTableEmptyState
icon={props.emptyIcon}
title="No machines match current filters"
description="Adjust the search or status filter to see more Pulse Agent or agentless machines."
description="Adjust the search or status filter to see more Pulse Agent machines."
/>
}
>

View file

@ -1,10 +1,8 @@
import { Show, createEffect, createMemo, createSignal } from 'solid-js';
import { A, useLocation, useNavigate } from '@solidjs/router';
import ActivityIcon from 'lucide-solid/icons/activity';
import PlusIcon from 'lucide-solid/icons/plus';
import ServerIcon from 'lucide-solid/icons/server';
import SettingsIcon from 'lucide-solid/icons/settings';
import { buildAvailabilityTargetAddPath } from '@/components/Settings/availabilitySettingsModel';
import { buildInfrastructureOnboardingPath } from '@/components/Settings/infrastructureWorkspaceModel';
import {
PlatformErrorState,
@ -89,7 +87,7 @@ export function StandalonePageSurface() {
fallback={
<PlatformTableLoadingState
title="Loading machines"
description="Pulse is loading Pulse Agent machines, agentless computers, and availability checks."
description="Pulse is loading Pulse Agent machines and availability checks."
/>
}
>
@ -118,8 +116,8 @@ export function StandalonePageSurface() {
fallback={
<PlatformTableEmptyState
icon={machineIcon()}
title="No monitored machines"
description="Install Pulse Agent on servers, laptops, and desktops for full telemetry, or add an agentless machine check when reachability is enough."
title="No Pulse Agent machines"
description="Install Pulse Agent on servers, laptops, and desktops for CPU, memory, disk, and network telemetry. Agentless reachability checks live in Availability checks."
actions={
<div class="flex flex-wrap items-center justify-center gap-2">
<button
@ -130,12 +128,9 @@ export function StandalonePageSurface() {
<SettingsIcon class="h-3.5 w-3.5" />
Add agent
</button>
<A
href={buildAvailabilityTargetAddPath('machine')}
class="inline-flex items-center gap-2 rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-base-content shadow-sm hover:bg-slate-50"
>
<PlusIcon class="h-3.5 w-3.5" />
Add machine check
<A href={buildStandalonePath('availability')} class={overviewActionClass}>
<ActivityIcon class="h-3.5 w-3.5" />
View checks
</A>
</div>
}
@ -149,10 +144,18 @@ export function StandalonePageSurface() {
<Show when={model().availabilityChecks.length > 0}>
<PlatformTableEmptyState
icon={machineIcon()}
title="No machines"
description="Service and device checks are monitored from the Availability checks tab. Mark an availability target as a machine when it represents a server, laptop, or desktop."
title="No Pulse Agent machines"
description="Availability checks, including ICMP checks for computers, stay in the Availability checks tab. Install Pulse Agent when you need full machine telemetry."
actions={
<div class="flex flex-wrap items-center justify-center gap-2">
<button
type="button"
onClick={() => navigate(buildInfrastructureOnboardingPath('pick'))}
class={overviewActionClass}
>
<SettingsIcon class="h-3.5 w-3.5" />
Add agent
</button>
<A
href={buildStandalonePath('availability')}
class={overviewActionClass}
@ -160,13 +163,6 @@ export function StandalonePageSurface() {
<ActivityIcon class="h-3.5 w-3.5" />
View checks
</A>
<A
href={buildAvailabilityTargetAddPath('machine')}
class={overviewActionClass}
>
<PlusIcon class="h-3.5 w-3.5" />
Add machine check
</A>
</div>
}
/>
@ -177,7 +173,7 @@ export function StandalonePageSurface() {
resources={model().machines}
emptyIcon={machineIcon()}
emptyTitle="No machines"
emptyDescription="Install the Pulse Agent on Linux, macOS, Windows, or Unraid systems for full telemetry, or add an agentless machine check when reachability is enough."
emptyDescription="Install Pulse Agent on Linux, macOS, Windows, or Unraid systems for full CPU, memory, disk, and network telemetry."
/>
</Show>
</div>

View file

@ -112,7 +112,7 @@ afterEach(() => {
});
describe('StandalonePageSurface', () => {
it('keeps overview focused on standalone machines, including agentless machines only', () => {
it('keeps overview focused on Pulse Agent machines only', () => {
render(() => <StandalonePageSurface />);
expect(mocks.useUnifiedResources).toHaveBeenCalledWith(
@ -124,7 +124,7 @@ describe('StandalonePageSurface', () => {
'data-tabs',
'machines,availability',
);
expect(screen.getByTestId('agents-machines-table')).toHaveAttribute('data-resource-count', '2');
expect(screen.getByTestId('agents-machines-table')).toHaveAttribute('data-resource-count', '1');
expect(screen.queryByTestId('availability-checks-table')).not.toBeInTheDocument();
});
@ -176,18 +176,16 @@ describe('StandalonePageSurface', () => {
expect(screen.queryByTestId('agents-machines-table')).not.toBeInTheDocument();
expect(screen.queryByTestId('availability-checks-table')).not.toBeInTheDocument();
expect(screen.getByText('No machines')).toBeInTheDocument();
expect(screen.getByText('No Pulse Agent machines')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add agent' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'View checks' })).toHaveAttribute(
'href',
'/standalone/availability',
);
expect(screen.getByRole('link', { name: 'Add machine check' })).toHaveAttribute(
'href',
'/settings/monitoring/availability?add=target&targetKind=machine',
);
expect(screen.queryByRole('link', { name: 'Add machine check' })).not.toBeInTheDocument();
});
it('renders the machines table when only an agentless machine is present', () => {
it('keeps agentless machine checks in the availability handoff', () => {
mocks.useUnifiedResources.mockReturnValue({
resources: () => [
resource({
@ -205,7 +203,11 @@ describe('StandalonePageSurface', () => {
render(() => <StandalonePageSurface />);
expect(screen.getByTestId('agents-machines-table')).toHaveAttribute('data-resource-count', '1');
expect(screen.queryByText('No machines')).not.toBeInTheDocument();
expect(screen.queryByTestId('agents-machines-table')).not.toBeInTheDocument();
expect(screen.getByText('No Pulse Agent machines')).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'View checks' })).toHaveAttribute(
'href',
'/standalone/availability',
);
});
});

View file

@ -66,7 +66,7 @@ describe('standalonePageModel', () => {
expect(model.availabilityChecks).toEqual([]);
});
it('keeps agentless availability checks beside standalone machines', () => {
it('keeps agentless availability checks out of standalone machines', () => {
const model = buildStandalonePageModel([
resource({ id: 'mac-mini', platformType: 'agent', type: 'agent', sources: ['agent'] }),
resource({
@ -85,8 +85,8 @@ describe('standalonePageModel', () => {
}),
]);
expect(model.machines.map((item) => item.id)).toEqual(['mac-mini', 'router-ping']);
expect(model.machines.map((item) => item.id)).toEqual(['mac-mini']);
expect(model.availabilityChecks.map((item) => item.id)).toEqual(['router-ping', 'endpoint-1']);
expect(model.resources.map((item) => item.id)).toEqual(['mac-mini', 'router-ping']);
expect(model.resources.map((item) => item.id)).toEqual(['mac-mini']);
});
});

View file

@ -15,22 +15,8 @@ export const isAgentlessAvailabilityResource = (resource: Resource): boolean =>
resource.platformType === 'availability' ||
resource.sources?.includes('availability') === true;
const availabilityTargetKindFor = (resource: Resource): string =>
String(
resource.availability?.targetKind ??
(resource.platformData?.availability as { targetKind?: string } | undefined)?.targetKind ??
'',
)
.trim()
.toLowerCase();
export const isAgentlessMachineResource = (resource: Resource): boolean =>
isAgentlessAvailabilityResource(resource) && availabilityTargetKindFor(resource) === 'machine';
export function buildStandalonePageModel(resources: readonly Resource[]): StandalonePageModel {
const machines = resources.filter(
(resource) => isStandaloneMachineResource(resource) || isAgentlessMachineResource(resource),
);
const machines = resources.filter(isStandaloneMachineResource);
const availabilityChecks = resources.filter(isAgentlessAvailabilityResource);
return {
machines,