Align first-run infrastructure source paths

Route first-run guidance through the unified infrastructure source picker and align setup, tour, dashboard, and empty-state copy with the source-strategy model.
This commit is contained in:
rcourtman 2026-04-23 23:07:19 +01:00
parent 2a85408a7f
commit 4fb67cd547
30 changed files with 376 additions and 353 deletions

View file

@ -477,21 +477,21 @@ an add-only capacity posture.
6. Keep Proxmox registration continuity self-healing: stale local registration markers must be verified against Pulse before the host agent skips setup, and a missing matching node on the Pulse side must drive canonical re-registration instead of asking operators to delete marker files manually.
7. Keep first-session lifecycle handoff explicit: the live setup completion
surface in `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`
must route the primary CTA into `/settings/infrastructure/install`, frame
that route as the first-host install step, and present `Platform
connections` as the named API-backed alternative for Proxmox, TrueNAS, and
future provider integrations rather than leaving post-setup next actions
implicit. That API-backed alternative must be a real first-run handoff
control, not prose-only guidance.
must route the primary CTA into `/settings/infrastructure?add=pick`, frame
that route as source strategy selection, and present platform API inventory
plus Pulse Agent telemetry as peer choices for Proxmox, TrueNAS, VMware,
standalone hosts, and future provider integrations rather than leaving
post-setup next actions implicit. A direct Pulse Agent handoff may remain as
a secondary control for operators who already know the first source is
agent-managed, but the primary first-run path is the unified source picker.
Once the completion surface observes connected systems, that same handoff
model must derive its follow-up actions from the canonical connected-system
path classification rather than a raw connected-agent count. API-backed
first-session states must keep `Platform connections` visible without
hiding `Infrastructure Install` when the next system should run the unified
agent, and install-managed first-session states must not suppress the
explicit API-backed alternative when the runtime has already connected
platform-owned systems. The API-backed versus install-workspace split must
come from the governed onboarding paths in
first-session states must keep `Add infrastructure` visible for both
API-backed and agent-managed next systems instead of reviving separate
`Platform connections` and `Infrastructure Install` branches. The
API-backed versus agent-managed classification must come from the governed
onboarding paths in
`docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json` through
the shared frontend manifest helper, not from a Setup Wizard-local platform
allowlist. When preview-only browser proof needs a deterministic connected
@ -520,9 +520,10 @@ connections` as the named API-backed alternative for Proxmox, TrueNAS, and
still belong to the reporting inventory and inline lifecycle detail, but
they must not appear as peer connection rows on that top ledger. Adding a
new system must stay a single entry point on that ledger:
one `Add connection` entry point that keeps `Install on a host` explicit
for the agent path while opening the saved-connection create flow for
API-backed platforms on the same page. `/settings/infrastructure/install`,
one `Add infrastructure` entry point that opens the source picker, keeps
`Install on a host` explicit only after the operator chooses Pulse Agent,
and opens the saved-connection create flow for API-backed platforms on the
same page. `/settings/infrastructure/install`,
`/settings/infrastructure/platforms`, and
`/settings/infrastructure/operations` remain valid deep links, but they
must resolve to section focus on that same single-page workspace rather
@ -544,15 +545,15 @@ connections` as the named API-backed alternative for Proxmox, TrueNAS, and
11. Keep the dev first-session proof deterministic on the real wizard path:
`tests/integration/tests/helpers.ts` and
`tests/integration/tests/11-first-session.spec.ts` must refresh first-run
state through `/api/security/dev/reset-first-run`, then prove both the
canonical `Open Infrastructure Install` handoff and the explicit
`Open Platform connections` handoff against the live setup wizard instead
of relying on stale bootstrap tokens, dashboard fallbacks, or preview-only
coverage. That API-backed handoff may keep the operator-facing `Platform
connections` label, but it must land on the shared infrastructure
onboarding contract at `/settings/infrastructure?add=pick` and normalize
back to `/settings/infrastructure` instead of reviving a separate
platform-management shell.
state through `/api/security/dev/reset-first-run`, then prove the
canonical `Add infrastructure` handoff and the explicit `Install Pulse
Agent` secondary handoff against the live setup wizard instead of relying
on stale bootstrap tokens, dashboard fallbacks, or preview-only coverage.
The primary handoff must land on the shared infrastructure onboarding
contract at `/settings/infrastructure?add=pick` and normalize back to
`/settings/infrastructure` instead of reviving a separate
platform-management shell. The secondary agent handoff must land on
`/settings/infrastructure?add=agent`.
When the first host reports successfully, the install workflow must treat
that as a completion handoff with direct navigation into `/dashboard` and
`/settings/infrastructure/operations` instead of leaving operators on a
@ -572,16 +573,19 @@ connections` label, but it must land on the shared infrastructure
ordered around the actual first-run operator sequence: credentials that must
be saved now should be visible before the operator leaves the screen, and
the completion surface should present one canonical primary next-step path
into Infrastructure Install instead of repeating competing install or
dashboard CTAs across multiple sections. Once the first monitored host is
into Add infrastructure instead of repeating competing install or dashboard
CTAs across multiple sections. Once the first monitored system is
already connected, that same surface must pivot its primary CTA and headline
to `/` so the operator is sent to the dashboard rather than being told to
install the first host again. While the first host is still pending, that
same completion narrative must describe Infrastructure Install as the place
where the first-host scoped install token is prepared from setup handoff,
and when it names the shared settings workspace for follow-up lifecycle
control it must use the canonical `Infrastructure` label instead of
reviving the retired `Infrastructure Operations` wording.
connect the first source again. While the first source is still pending,
that same completion narrative must describe Add infrastructure as the
place where the operator chooses platform API inventory, Pulse Agent
telemetry, or both. If the operator selects the direct agent path from that
completion surface, the agent install body may prepare the first-host
scoped install token from setup handoff, and when it names the shared
settings workspace for follow-up lifecycle control it must use the
canonical `Infrastructure` label instead of reviving the retired
`Infrastructure Operations` wording.
not as a second manual token-generation task the operator still needs to
figure out.
13. Keep API-backed platform onboarding explicit across
@ -590,11 +594,12 @@ connections` label, but it must land on the shared infrastructure
`frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`,
`frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, and
`frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`.
TrueNAS must be presented as a Platform connections workflow first, not as
a dedicated Unified Agent install profile. The install workspace may remain
available for optional later agent augmentation on TrueNAS, but first-run
copy, alternative CTAs, and install-profile lists must not imply that an
agent install is the required bootstrap for TrueNAS support in Pulse.
TrueNAS must be presented as an API-backed source flow through Add
infrastructure first, not as a dedicated Unified Agent install profile. The
agent install path may remain available for optional later agent
augmentation on TrueNAS, but first-run copy, alternative CTAs, and
install-profile lists must not imply that an agent install is the required
bootstrap for TrueNAS support in Pulse.
14. Keep first-session and lifecycle-adjacent frontend resource handling on the
canonical unified-resource boundary. Top-level TrueNAS appliances may reach
setup-completion or infrastructure lifecycle surfaces only as canonical
@ -610,9 +615,10 @@ connections` label, but it must land on the shared infrastructure
16. Keep onboarding ownership aligned with
`docs/release-control/v6/internal/PLATFORM_SUPPORT_MODEL.md`: agent-backed
first-class platforms belong to the install/reporting lifecycle path,
API-backed first-class platforms belong to Platform connections, and any
later unified-agent augmentation on an API-backed platform must remain an
optional secondary path instead of silently becoming the required bootstrap.
API-backed first-class platforms belong to the Add infrastructure API
source flow, and any later unified-agent augmentation on an API-backed
platform must remain an optional secondary path instead of silently
becoming the required bootstrap.
## Current State
@ -753,12 +759,12 @@ just to decide whether to show assistant-adjacent UI.
That same platform-connections ownership now also includes mock-runtime
continuity for API-backed platforms. When `/api/system/mock-mode` flips a
running server between real and mock data, the canonical TrueNAS and VMware
settings routes must keep surfacing through the same Platform connections
workspace and handoff URLs instead of depending on process-start-only wiring
or a mock-only alternate shell.
settings routes must keep surfacing through the same Add infrastructure source
picker and handoff URLs instead of depending on process-start-only wiring or a
mock-only alternate shell.
That same lifecycle-owned mock path now also requires one shared fixture owner
for API-backed platform onboarding. TrueNAS and VMware connection-list payloads
shown in Platform connections must be assembled from the canonical
shown in Add infrastructure must be assembled from the canonical
`internal/mock/` platform fixture layer, so settings handoff metadata cannot
drift from the runtime mock inventory and shared storage/recovery context.
That same lifecycle-adjacent mock path must stay graph-first at the shared
@ -1043,10 +1049,11 @@ Infrastructure workspace. `ConnectionsTable.tsx`,
install/direct/reporting operator flow, with `ConnectionsTable.tsx` plus
`connectionsTableModel.ts` as the canonical top-level infrastructure ledger
and the governed add/edit modals as the API-backed add/edit surface.
Operator-facing setup copy may still use `Platform connections`, but that
label now means the shared Infrastructure onboarding path
(`/settings/infrastructure?add=pick`) rather than a standalone
`PlatformConnectionsWorkspace.tsx` shell.
Operator-facing setup copy should use `Add infrastructure` and source-strategy
language for the shared Infrastructure onboarding path
(`/settings/infrastructure?add=pick`) rather than reviving the standalone
`PlatformConnectionsWorkspace.tsx` shell or the old `Platform connections`
label.
That infrastructure destination now has one canonical mental model:
configured infrastructure sources stay visible on the landing page as the
primary objects the operator manages. The landing table is instance-first, not
@ -1124,7 +1131,7 @@ surfaces grow a second VMware availability fetch or a VMware-only handoff
path.
That same infrastructure workspace boundary now also owns the first-run
handoff copy for new operators. `InfrastructureWorkspace.tsx` must keep
`Install on a host` and `Platform connections` explicit in the shared
platform API inventory and Pulse Agent telemetry explicit in the shared
workspace instead of leaving first-session guidance implicit in generic
settings-shell prose or retreating to one provider's name or one onboarding
mode as the primary story.
@ -2107,17 +2114,17 @@ advertising automatic token rotation after each copy once the active transport
is explicitly tokenless.
The same first-session contract now also owns the landing handoff after secure
setup: RC-proof and helpers must treat direct navigation into
`/settings/infrastructure/install` as the canonical completion path, rather
than assuming the legacy dashboard-only landing still defines successful
wizard completion.
`/settings/infrastructure?add=pick` as the canonical completion path, rather
than assuming an agent-only install landing or the legacy dashboard-only
landing still defines successful wizard completion.
That same `SetupCompletionPanel` boundary must also stay on the direct
`setup-completion-install-surface` proof path, rather than relying only on shared
helper coverage or downstream install tests to catch lifecycle drift in the
setup completion surface.
`setup-completion-source-picker-surface` proof path, rather than relying only
on shared helper coverage or downstream install tests to catch lifecycle drift
in the setup completion surface.
That same first-session browser proof must also exercise the explicit
`Platform connections` completion action through the real setup wizard flow
for API-backed starts like TrueNAS, rather than relying only on the preview
route or prose-level assertions to represent the API-backed alternative.
`Install Pulse Agent` secondary action through the real setup wizard flow,
rather than relying only on the preview route or prose-level assertions to
represent the agent-managed alternative.
The same ownership also covers manual install fallback in the infrastructure
settings surface: active and ignored Connected infrastructure rows must now
come from the backend-owned `connectedInfrastructure` projection instead of a

View file

@ -244,7 +244,7 @@ work extends shared components instead of creating new local variants.
The shared navigation guide owns route-aware first focus: when it opens
from a top-level product route such as `/recovery`, the first highlighted
step should match that route instead of always restarting at Dashboard.
5. Keep shared infrastructure shell state on the reusable settings boundary: `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` and `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx` must continue to derive provider counts, availability, and shared subtab copy from one infrastructure-settings source — via the unified aggregator through `frontend-modern/src/components/Settings/useConnectionsLedger.ts` — instead of creating provider-local summary fetches or VMware-only shell vocabulary. Phase 9 retired the old `PlatformConnectionsWorkspace` per-type shell, but setup guidance may still use `Platform connections` as the operator-facing label for the shared API-backed onboarding path.
5. Keep shared infrastructure shell state on the reusable settings boundary: `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` and `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx` must continue to derive provider counts, availability, and shared subtab copy from one infrastructure-settings source — via the unified aggregator through `frontend-modern/src/components/Settings/useConnectionsLedger.ts` — instead of creating provider-local summary fetches or VMware-only shell vocabulary. Phase 9 retired the old `PlatformConnectionsWorkspace` per-type shell; setup guidance should now use `Add infrastructure` plus source-strategy language for API-backed onboarding.
That same shared shell boundary now owns the first-run posture for
`/settings/infrastructure`: the landing route should read as one
source-manager workspace with configured infrastructure instances first
@ -584,16 +584,17 @@ work extends shared components instead of creating new local variants.
path to the bare `/settings/infrastructure`, which renders the unified
Connections table, not to a separate install subview or to reporting/
control. The first-session story is owned by that table's own empty state
and the `Add connection` entry point on it, not by a second landing route,
and the `Add infrastructure` entry point on it, not by a second landing route,
so first-time operators and returning operators see one consistent
infrastructure surface by default.
14. Keep dashboard onboarding copy on the shared presentation owner in
`frontend-modern/src/utils/dashboardEmptyStatePresentation.ts`. Both the
infrastructure empty state and the dashboard route's no-resources state
must name the canonical install workspace explicitly, keep `Platform
connections` visible as the API-backed alternative for Proxmox and
TrueNAS, and expose the same first-host next step instead of falling back
to passive “nothing here yet” wording.
must route first-time operators into the canonical
`/settings/infrastructure?add=pick` source picker, describe platform API
inventory and Pulse Agent telemetry as equal source strategies, and avoid
falling back to either passive “nothing here yet” wording or the retired
install-first / `Platform connections` split.
15. Keep cross-surface investigation handoffs on shared route ownership.
Feature shells such as Alerts and Patrol may decide which governed
destination chips to render, but canonical href, label, dedupe, and
@ -642,9 +643,10 @@ connections` visible as the API-backed alternative for Proxmox and
that helper instead of maintaining page-local copies of the same hover/focus
rules.
`frontend-modern/src/App.tsx` must land `/` on the dashboard shell and let
the governed dashboard empty state route first-time operators into
Infrastructure Install, instead of preserving a separate root-only jump to
`/infrastructure` that drifts from the rest of the onboarding contract.
the governed dashboard empty state route first-time operators into the
`Add infrastructure` source picker, instead of preserving a separate
root-only jump to `/infrastructure` or an agent-only install jump that
drifts from the rest of the onboarding contract.
The same entry-shell contract must also canonicalize authenticated
`/login`: once auth succeeds, the shared shell must resolve that route back
onto the governed dashboard landing path instead of rendering a page-local
@ -1446,6 +1448,9 @@ the new navigation in one pass without needing historical layout context.
That copy should stay direct and present-tense. Each guided step should say
what the destination does, not depend on v5 comparisons, migration framing, or
older information architecture to make sense.
The Infrastructure tour step must describe the source model directly: platform
API inventory, Pulse Agent telemetry, and discovered candidates are managed as
infrastructure sources in one place.
That guided welcome surface should stay compact. The canonical shape is a
coachmark-sized card centered on the current destination with one short
step-specific sentence, a small clickable step strip, and minimal footer
@ -1596,7 +1601,16 @@ implementation fallback language.
The estate summary may surface resource and alert issue counts only as
below-the-summary detail references; it must not claim the whole dashboard has
no issues when storage or recovery widgets still own independent health
signals.
signals. Those detail references must use governed dashboard section anchors so
the first viewport can move focus to Problem Resources or Alerts without
inventing separate dashboard drill-down routes.
The dashboard may add an optional Pulse Brief below that estate orientation
only when Assistant and Patrol are actually enabled and configured. That brief
must stay additive to the factual dashboard source of truth, derive its first
render from the already-owned estate, overview, action, storage, and recovery
facts, and hand off to Pulse Assistant through a structured context prompt
instead of replacing the route's canonical numbers, tables, or lane-owned
widgets with model prose.
The recovery feature shell now also depends on the shared
`frontend-modern/src/components/shared/Subtabs.tsx` primitive for its primary
protected-items versus recovery-events workspace switch. The recovery lane may
@ -2415,18 +2429,18 @@ reference cases, and
`frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts`
locks that direct-root contract so single-surface pages do not quietly regain
redundant outer spacing chrome.
The same shared settings-shell boundary now also owns the API-backed
alternative path inside Infrastructure.
The same shared settings-shell boundary now also owns the API-backed source
path inside Infrastructure.
`frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`,
`frontend-modern/src/components/Settings/settingsHeaderMeta.ts`,
`frontend-modern/src/components/Settings/settingsNavigationModel.ts`,
`frontend-modern/src/utils/dashboardEmptyStatePresentation.ts`,
`frontend-modern/src/utils/infrastructureEmptyStatePresentation.ts`, and
adjacent setup guidance may still use `Platform connections` as the
operator-facing first-run label for API-backed onboarding, but that label must
resolve to the shared `Infrastructure` destination and its inline
`ConnectionEditor` add flow rather than reviving a standalone platform shell
or provider-local route.
adjacent setup guidance must use `Add infrastructure` as the operator-facing
first-run label for API-backed onboarding, resolve that label to the shared
`Infrastructure` destination and its inline `ConnectionEditor` add flow, and
avoid reviving a standalone platform shell, `Platform connections` label, or
provider-local route.
That same settings-shell contract also owns the shared infrastructure summary
state. `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`,
`frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts`,

View file

@ -256,11 +256,11 @@ assembly branch.
5. Keep the infrastructure landing empty state on canonical first-run routing:
when inventory is empty, `frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx`
and `frontend-modern/src/utils/infrastructureEmptyStatePresentation.ts`
must send operators directly to `/settings/infrastructure/install`, name
first-host install as the default next step, and keep `Platform connections`
as the explicit API-backed alternative for Proxmox, TrueNAS, and future
provider-backed platforms instead of regressing to generic settings-root
CTAs or provider-specific one-off routes.
must send operators directly to `/settings/infrastructure?add=pick`, name
source strategy selection as the default next step, and present platform
API inventory plus Pulse Agent telemetry as peer source options instead of
regressing to generic settings-root CTAs, an agent-only install jump, the
retired `Platform connections` split, or provider-specific one-off routes.
6. Keep infrastructure route-backed source filters on canonical unified-resource
truth. `frontend-modern/src/features/infrastructure/` must preserve a
route-owned source such as `truenas` in the filter option set even when the

View file

@ -89,7 +89,7 @@ export function DashboardStateCards(props: DashboardStateCardsProps) {
!props.kioskMode() ? (
<button
type="button"
onClick={() => props.navigate(buildInfrastructureOnboardingPath('agent'))}
onClick={() => props.navigate(buildInfrastructureOnboardingPath('pick'))}
class="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
{props.dashboardInfrastructureEmptyState().actionLabel}

View file

@ -985,7 +985,7 @@ describe('Dashboard performance contract', () => {
expect(workloadPanelSource).not.toContain('TableHead');
expect(dashboardStateCardsSource).toContain('dashboardInfrastructureEmptyState().title');
expect(dashboardStateCardsSource).toContain('dashboardDisconnectedState().actionLabel');
expect(dashboardStateCardsSource).toContain("buildInfrastructureOnboardingPath('agent')");
expect(dashboardStateCardsSource).toContain("buildInfrastructureOnboardingPath('pick')");
expect(dashboardStatsStripSource).toContain('totalStats().running');
expect(dashboardStatsStripSource).toContain('totalStats().stopped');
});

View file

@ -20,10 +20,10 @@ describe('DashboardStateCards', () => {
description: 'No guests match your current filters',
})}
dashboardInfrastructureEmptyState={() => ({
title: 'No infrastructure hosts connected',
title: 'No infrastructure sources connected',
description:
'To start using Pulse, first add your infrastructure in Settings → Infrastructure → Install on a host. If you want an API-backed platform such as Proxmox or TrueNAS instead, use Settings → Infrastructure → Platform connections.',
actionLabel: 'Open infrastructure setup',
'Start in Settings → Infrastructure by choosing a source strategy. Connect a platform API for inventory and health, install Pulse Agent for host telemetry, or use both when you want full coverage.',
actionLabel: 'Add infrastructure source',
})}
dashboardLoadingState={() => ({
title: 'Loading dashboard data...',
@ -45,8 +45,8 @@ describe('DashboardStateCards', () => {
/>
));
fireEvent.click(screen.getByRole('button', { name: 'Open infrastructure setup' }));
fireEvent.click(screen.getByRole('button', { name: 'Add infrastructure source' }));
expect(navigate).toHaveBeenCalledWith('/settings/infrastructure?add=agent');
expect(navigate).toHaveBeenCalledWith('/settings/infrastructure?add=pick');
});
});

View file

@ -39,7 +39,7 @@ export const InfrastructureInstallerSection: Component = () => {
<div class="space-y-2">
<p class="font-semibold">Security configured. Save these first-run credentials now.</p>
<p class="text-xs text-emerald-800 dark:text-emerald-200">
This is the canonical handoff from first-run setup into Infrastructure Install.
This is the Pulse Agent handoff from first-run setup inside Add infrastructure.
<Show
when={state.setupHandoffAutoTokenPending()}
fallback={
@ -475,8 +475,8 @@ export const InfrastructureInstallerSection: Component = () => {
{state.getSelectedInstallProfile().description}
</p>
<p class="mt-1.5 text-xs text-muted">
API-backed platforms such as TrueNAS connect through Add connection
choose the platform type.
API-backed platforms such as TrueNAS connect through Add infrastructure
choose source type.
</p>
<Show when={state.getInstallProfileFlags().length > 0}>
<p class="mt-1.5 text-xs text-muted">

View file

@ -68,7 +68,7 @@ describe('ConnectionsTable', () => {
expect(screen.getByText('Start monitoring infrastructure')).toBeInTheDocument();
expect(
screen.getByText(/Available system types: VMware vCenter, TrueNAS SCALE/i),
screen.getByText(/Supported source types include VMware vCenter, TrueNAS SCALE/i),
).toBeInTheDocument();
expect(screen.queryByRole('table')).toBeNull();
});

View file

@ -387,9 +387,9 @@ describe('InfrastructureWorkspace', () => {
expect(screen.getByText('Start monitoring infrastructure')).toBeInTheDocument(),
);
expect(
screen.getByText('Add infrastructure systems to start monitoring your environment.'),
screen.getByText('Choose an infrastructure source to start monitoring your environment.'),
).toBeInTheDocument();
expect(screen.getByText(/Available system types: VMware vCenter/i)).toBeInTheDocument();
expect(screen.getByText(/Supported source types include VMware vCenter/i)).toBeInTheDocument();
expect(screen.getByText(/standalone hosts through Pulse Agent/i)).toBeInTheDocument();
});

View file

@ -1,12 +1,4 @@
import {
Component,
createSignal,
createEffect,
createMemo,
onCleanup,
Show,
For,
} from 'solid-js';
import { Component, createSignal, createEffect, createMemo, onCleanup, Show, For } from 'solid-js';
import { copyToClipboard } from '@/utils/clipboard';
import { logger } from '@/utils/logger';
import { apiFetchJSON } from '@/utils/apiClient';
@ -30,33 +22,33 @@ interface CompleteStepProps {
const UNIFIED_RESOURCE_GUIDANCE = {
title: 'What happens next',
description:
'Pulse is now secured. Next, choose the first infrastructure path: use Infrastructure Install for a host that should run the unified agent, or use Platform connections for API-backed platforms like Proxmox, TrueNAS, and VMware.',
'Pulse is now secured. Next, choose how the first system should enter the unified infrastructure model: platform API inventory, Pulse Agent telemetry, or both.',
steps: [
{
title: 'Open Infrastructure Install',
title: 'Open Add infrastructure',
description:
'Use the canonical install workspace where Pulse prepares the first-host install token from setup and keeps Platform connections beside it when the first target is API-backed.',
'Review the supported source types in one place before choosing a platform API, Pulse Agent, or both.',
},
{
title: 'Copy the command for your target system',
title: 'Choose the source strategy',
description:
'Choose Linux, macOS, Windows, or another supported target only when the first system should run the unified agent directly.',
'Connect a platform API for inventory and health, install Pulse Agent for node-local telemetry, or combine both where full coverage matters.',
},
{
title: 'Run it on the first host you want to monitor',
title: 'Save the source and confirm coverage',
description:
'When that agent-managed host connects, Pulse creates your first monitored system and you can add more infrastructure from there.',
'When the source connects, Pulse creates the first monitored system and the dashboard becomes the live estate overview.',
},
],
inventoryFacts: [
'Start with one host, then add more systems later from the same install workspace.',
'Infrastructure Install owns the token, connection URL, TLS/CA settings, and platform-specific commands.',
'API-backed platforms like Proxmox, TrueNAS, and VMware use Platform connections instead of a dedicated install profile in Infrastructure Install.',
'Start with one source, then add more systems later from Settings → Infrastructure.',
'Platform APIs own inventory and health. Pulse Agent owns host telemetry, local services, Docker, and Kubernetes discovery.',
'VMware, TrueNAS, Proxmox, PBS, and PMG use API-backed source flows; standalone hosts use Pulse Agent.',
],
} as const;
const INFRASTRUCTURE_INSTALL_PATH = buildInfrastructureOnboardingPath('agent');
const PLATFORM_CONNECTIONS_PATH = buildInfrastructureOnboardingPath('pick');
const ADD_INFRASTRUCTURE_PATH = buildInfrastructureOnboardingPath('pick');
const AGENT_INSTALL_PATH = buildInfrastructureOnboardingPath('agent');
const SETUP_WIZARD_TELEMETRY_SURFACE = 'setup_wizard_complete';
export const SetupCompletionPanel: Component<CompleteStepProps> = (props) => {
@ -104,7 +96,7 @@ export const SetupCompletionPanel: Component<CompleteStepProps> = (props) => {
if (
!firstAgentConnectionTracked &&
nextConnectedSystems.some((system) => system.connectionPath === 'install')
nextConnectedSystems.some((system) => system.connectionPath === 'agent')
) {
trackAgentFirstConnected(SETUP_WIZARD_TELEMETRY_SURFACE, 'first_agent');
firstAgentConnectionTracked = true;
@ -142,7 +134,7 @@ export const SetupCompletionPanel: Component<CompleteStepProps> = (props) => {
const downloadCredentials = () => {
const baseUrl = getPulseBaseUrl();
const infrastructureUrl = `${baseUrl.replace(/\/$/, '')}${INFRASTRUCTURE_INSTALL_PATH}`;
const infrastructureUrl = `${baseUrl.replace(/\/$/, '')}${ADD_INFRASTRUCTURE_PATH}`;
const content = `Pulse Credentials
==================
Generated: ${new Date().toISOString()}
@ -161,8 +153,8 @@ Infrastructure:
---------------
${infrastructureUrl}
Use Add connection to connect your first host or API-backed platform
(Proxmox, TrueNAS, VMware, and others).
Use Add infrastructure to choose a platform API, Pulse Agent, or both
for the first system Pulse should monitor.
Keep these credentials secure!
`;
@ -178,21 +170,19 @@ Keep these credentials secure!
URL.revokeObjectURL(url);
};
const handleOpenInstallWorkspace = () => {
props.onComplete(INFRASTRUCTURE_INSTALL_PATH);
const handleOpenAddInfrastructure = () => {
props.onComplete(ADD_INFRASTRUCTURE_PATH);
};
const handleOpenPlatformConnections = () => {
props.onComplete(PLATFORM_CONNECTIONS_PATH);
const handleOpenAgentInstall = () => {
props.onComplete(AGENT_INSTALL_PATH);
};
const handleGoToDashboard = () => {
props.onComplete('/');
};
const completionViewModel = createMemo(() =>
buildSetupCompletionViewModel(connectedSystems()),
);
const completionViewModel = createMemo(() => buildSetupCompletionViewModel(connectedSystems()));
return (
<div class="max-w-2xl mx-auto bg-surface border border-border overflow-hidden animate-fade-in relative rounded-md p-6 sm:p-8 text-center text-base-content">
@ -406,7 +396,7 @@ Keep these credentials secure!
Infrastructure
</div>
<code class="text-base-content font-mono text-xs break-all">
{INFRASTRUCTURE_INSTALL_PATH}
{ADD_INFRASTRUCTURE_PATH}
</code>
</div>
@ -507,8 +497,8 @@ Keep these credentials secure!
</h3>
<p class="mt-2 text-xs text-muted max-w-xl">
{completionViewModel().hasConnectedSystems
? 'Pulse already has a live monitored system. Open the dashboard to confirm the first overview, then return to Infrastructure when you want to continue with the next system path.'
: 'The canonical install flow now lives in Infrastructure. Open that workspace to continue with the first-host install token Pulse prepares from setup, adjust the agent connection URL only if needed, configure TLS or custom CA options, and copy the correct command for the first system you want Pulse to monitor.'}
? 'Pulse already has a live monitored system. Open the dashboard to confirm the first overview, then return to Add infrastructure when you want to connect the next API or Agent source.'
: 'Add infrastructure now owns the first source decision. Open the picker to choose a platform API for inventory, Pulse Agent for host telemetry, or both when the first system needs full coverage.'}
</p>
</div>
<div class="rounded-sm bg-blue-50 px-2 py-1 text-[10px] font-medium text-blue-700 dark:bg-blue-900 dark:text-blue-300">
@ -516,48 +506,43 @@ Keep these credentials secure!
</div>
</div>
<div class="mt-4 rounded-md border border-border bg-surface-alt p-4">
<div class="text-[11px] font-medium uppercase tracking-wider text-muted">
Next step
</div>
<div class="text-[11px] font-medium uppercase tracking-wider text-muted">Next step</div>
<div class="mt-2 text-sm text-base-content">
{completionViewModel().nextStepSummary}
</div>
<div class="mt-2 text-xs text-muted">
{completionViewModel().nextStepDetail}
</div>
<div class="mt-2 text-xs text-muted">{completionViewModel().nextStepDetail}</div>
</div>
<div class="mt-4 flex flex-col gap-3 sm:flex-row">
<button
onClick={() =>
completionViewModel().primaryAction === 'dashboard'
? handleGoToDashboard()
: handleOpenInstallWorkspace()
: handleOpenAddInfrastructure()
}
class="inline-flex items-center justify-center gap-2 rounded-md bg-blue-600 px-4 py-3 text-sm font-semibold text-white transition-colors hover:bg-blue-700"
>
{completionViewModel().primaryAction === 'dashboard'
? 'Go to Dashboard'
: 'Open Infrastructure Install'}
: 'Add infrastructure'}
</button>
<Show when={completionViewModel().showPlatformConnectionsAction}>
<Show when={completionViewModel().showAddInfrastructureAction}>
<button
onClick={handleOpenPlatformConnections}
onClick={handleOpenAddInfrastructure}
class="inline-flex items-center justify-center gap-2 rounded-md border border-border px-4 py-3 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
>
Open Platform connections
Add infrastructure
</button>
</Show>
<Show when={completionViewModel().showInstallAction}>
<Show when={completionViewModel().showAgentInstallAction}>
<button
onClick={handleOpenInstallWorkspace}
onClick={handleOpenAgentInstall}
class="inline-flex items-center justify-center gap-2 rounded-md border border-border px-4 py-3 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
>
Open Infrastructure Install
Install Pulse Agent
</button>
</Show>
</div>
</div>
</div>
</div>
);

View file

@ -3,29 +3,27 @@ import setupCompletionPanelSource from '../SetupCompletionPanel.tsx?raw';
import setupCompletionModelSource from '../setupCompletionModel.ts?raw';
describe('SetupCompletionPanel guardrails', () => {
it('keeps setup completion aligned with the canonical infrastructure install workspace', () => {
it('keeps setup completion aligned with the canonical add-infrastructure picker', () => {
expect(setupCompletionPanelSource).toContain('buildInfrastructureOnboardingPath');
expect(setupCompletionPanelSource).toContain(
"const INFRASTRUCTURE_INSTALL_PATH = buildInfrastructureOnboardingPath('agent');",
"const ADD_INFRASTRUCTURE_PATH = buildInfrastructureOnboardingPath('pick');",
);
expect(setupCompletionPanelSource).toContain(
"const PLATFORM_CONNECTIONS_PATH = buildInfrastructureOnboardingPath('pick');",
"const AGENT_INSTALL_PATH = buildInfrastructureOnboardingPath('agent');",
);
expect(setupCompletionPanelSource).toContain('Open Infrastructure Install');
expect(setupCompletionPanelSource).toContain('Open Platform connections');
expect(setupCompletionPanelSource).toContain('Add infrastructure');
expect(setupCompletionPanelSource).toContain('Install Pulse Agent');
expect(setupCompletionPanelSource).toContain('Credentials you must save now');
expect(setupCompletionPanelSource).toContain('Shown during setup');
expect(setupCompletionPanelSource).toContain('props.onComplete(INFRASTRUCTURE_INSTALL_PATH);');
expect(setupCompletionPanelSource).toContain('props.onComplete(PLATFORM_CONNECTIONS_PATH);');
expect(setupCompletionPanelSource).toContain('props.onComplete(ADD_INFRASTRUCTURE_PATH);');
expect(setupCompletionPanelSource).toContain('props.onComplete(AGENT_INSTALL_PATH);');
expect(setupCompletionPanelSource).toContain(
'Use Add connection to connect your first host or API-backed platform',
'Use Add infrastructure to choose a platform API, Pulse Agent, or both',
);
expect(setupCompletionPanelSource).toContain(
'continue with the first-host install token Pulse prepares from setup',
);
expect(setupCompletionPanelSource).not.toContain(
'Use Add infrastructure to connect your first host or API-backed platform',
'Open the picker to choose a platform API for inventory, Pulse Agent for host telemetry',
);
expect(setupCompletionPanelSource).not.toContain('Use Add connection to connect');
expect(setupCompletionPanelSource).not.toContain("from '@/stores/licenseCommercial';");
expect(setupCompletionPanelSource).not.toContain('runStartProTrialAction');
expect(setupCompletionPanelSource).not.toContain('loadCommercialPosture');
@ -39,19 +37,19 @@ describe('SetupCompletionPanel guardrails', () => {
it('describes setup completion through the unified resource model instead of legacy install-command copy', () => {
expect(setupCompletionPanelSource).toContain("title: 'What happens next'");
expect(setupCompletionPanelSource).toContain('Pulse is now secured.');
expect(setupCompletionPanelSource).toContain("title: 'Open Infrastructure Install'");
expect(setupCompletionPanelSource).toContain(
'Pulse prepares the first-host install token from setup',
);
expect(setupCompletionPanelSource).toContain("title: 'Run it on the first host you want to monitor'");
expect(setupCompletionPanelSource).toContain("title: 'Open Add infrastructure'");
expect(setupCompletionPanelSource).toContain('Review the supported source types in one place');
expect(setupCompletionPanelSource).toContain("title: 'Save the source and confirm coverage'");
expect(setupCompletionPanelSource).toContain('What to expect');
expect(setupCompletionPanelSource).toContain('First system first');
expect(setupCompletionPanelSource).toContain('Start with one host, then add more systems later from the same install workspace.');
expect(setupCompletionPanelSource).toContain(
'API-backed platforms like Proxmox, TrueNAS, and VMware use Platform connections instead of a dedicated install profile in Infrastructure Install.',
'Start with one source, then add more systems later from Settings',
);
expect(setupCompletionPanelSource).toContain(
'Platform APIs own inventory and health. Pulse Agent owns host telemetry',
);
expect(setupCompletionModelSource).toContain(
'If the first system is API-backed, use Platform connections instead of starting with host install.',
'Start with a platform API when a platform manages the estate.',
);
expect(setupCompletionPanelSource).not.toContain('Smart Auto-Detection');
expect(setupCompletionPanelSource).not.toContain('Agent Metrics');
@ -61,14 +59,16 @@ describe('SetupCompletionPanel guardrails', () => {
it('keeps connected infrastructure classification on the canonical setup model', () => {
expect(setupCompletionPanelSource).toContain('buildSetupCompletionConnectedSystems');
expect(setupCompletionPanelSource).toContain('buildSetupCompletionViewModel');
expect(setupCompletionPanelSource).toContain("props.connectedResourcesOverride !== undefined");
expect(setupCompletionPanelSource).toContain("setConnectedSystems(buildSetupCompletionConnectedSystems(props.connectedResourcesOverride));");
expect(setupCompletionPanelSource).toContain('props.connectedResourcesOverride !== undefined');
expect(setupCompletionPanelSource).toContain(
'setConnectedSystems(buildSetupCompletionConnectedSystems(props.connectedResourcesOverride));',
);
expect(setupCompletionModelSource).toContain('isAgentFacetInfrastructureResource');
expect(setupCompletionModelSource).toContain('getPreferredInfrastructureDisplayName');
expect(setupCompletionModelSource).toContain('getPreferredResourceHostname');
expect(setupCompletionModelSource).toContain('getSourcePlatformManifestEntry');
expect(setupCompletionModelSource).toContain("sourcePlatformSupportsOnboardingPath");
expect(setupCompletionModelSource).toContain("displayTokens[displayTokens.length - 1]");
expect(setupCompletionModelSource).toContain('sourcePlatformSupportsOnboardingPath');
expect(setupCompletionModelSource).toContain('displayTokens[displayTokens.length - 1]');
expect(setupCompletionModelSource).not.toContain('PLATFORM_CONNECTION_PLATFORM_TYPES');
expect(setupCompletionModelSource).not.toContain("resource.type === 'truenas'");
expect(setupCompletionModelSource).not.toContain('getPreferredResourceDisplayName(resource)');
@ -89,30 +89,36 @@ describe('SetupCompletionPanel guardrails', () => {
});
it('keeps setup completion on one primary next-step surface instead of repeated CTA sections', () => {
expect(setupCompletionPanelSource).toContain("const [showCredentials, setShowCredentials] = createSignal(true);");
expect(setupCompletionPanelSource).toContain('Save the admin login and API token before leaving this screen');
expect(setupCompletionPanelSource).toContain(
'const [showCredentials, setShowCredentials] = createSignal(true);',
);
expect(setupCompletionPanelSource).toContain(
'Save the admin login and API token before leaving this screen',
);
expect(setupCompletionPanelSource).toContain('Recommended next step');
expect(setupCompletionPanelSource).toContain('Go to Dashboard');
expect(setupCompletionModelSource).toContain("heroTitle: 'First monitored system connected'");
expect(setupCompletionModelSource).toContain("heroTitle: 'Connect your first monitored system'");
expect(setupCompletionModelSource).toContain(
"heroTitle: 'Choose your first infrastructure source'",
);
expect(setupCompletionPanelSource).toContain(
"completionViewModel().primaryAction === 'dashboard'",
);
expect(setupCompletionPanelSource).toContain(
'completionViewModel().showPlatformConnectionsAction',
'completionViewModel().showAddInfrastructureAction',
);
expect(setupCompletionPanelSource).toContain('completionViewModel().showInstallAction');
expect(setupCompletionPanelSource).toContain('handleOpenPlatformConnections');
expect(setupCompletionPanelSource).toContain('completionViewModel().showAgentInstallAction');
expect(setupCompletionPanelSource).toContain('handleOpenAddInfrastructure');
expect(setupCompletionPanelSource).not.toContain('hasConnectedAgents');
expect(setupCompletionPanelSource).not.toContain('connectedAgents().length');
expect(setupCompletionPanelSource).not.toContain(
'You can return here later from Connections & Inventory if you skip install for now.',
);
expect(setupCompletionPanelSource).toContain(
'The canonical install flow now lives in Infrastructure.',
'Add infrastructure now owns the first source decision.',
);
expect(setupCompletionPanelSource).toContain(
'then return to Infrastructure when you want to continue with the next system path.',
'then return to Add infrastructure when you want to connect the next API or Agent source.',
);
});
});

View file

@ -77,13 +77,14 @@ describe('SetupCompletionPanel', () => {
vi.unstubAllGlobals();
});
it('frames setup completion around the canonical infrastructure install workspace', async () => {
it('frames setup completion around the canonical add-infrastructure picker', async () => {
render(() => <SetupCompletionPanel state={baseState} onComplete={vi.fn()} />);
expect(screen.getByText('Connect your first monitored system')).toBeInTheDocument();
expect(screen.getByText('Choose your first infrastructure source')).toBeInTheDocument();
expect(screen.getByText('What happens next')).toBeInTheDocument();
expect(screen.getAllByText('Open Infrastructure Install').length).toBeGreaterThan(0);
expect(screen.getByRole('button', { name: 'Open Platform connections' })).toBeInTheDocument();
expect(screen.getAllByText('Open Add infrastructure').length).toBeGreaterThan(0);
expect(screen.getByRole('button', { name: 'Add infrastructure' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Install Pulse Agent' })).toBeInTheDocument();
expect(screen.getByText('Credentials you must save now')).toBeInTheDocument();
expect(screen.getByText('Shown during setup')).toBeInTheDocument();
expect(screen.getByText('admin')).toBeInTheDocument();
@ -92,17 +93,17 @@ describe('SetupCompletionPanel', () => {
expect(screen.getByText('First system first')).toBeInTheDocument();
expect(
screen.getByText(
'Infrastructure Install owns the token, connection URL, TLS/CA settings, and platform-specific commands.',
'Platform APIs own inventory and health. Pulse Agent owns host telemetry, local services, Docker, and Kubernetes discovery.',
),
).toBeInTheDocument();
expect(
screen.getByText(
'Use the canonical install workspace where Pulse prepares the first-host install token from setup and keeps Platform connections beside it when the first target is API-backed.',
'Review the supported source types in one place before choosing a platform API, Pulse Agent, or both.',
),
).toBeInTheDocument();
expect(
screen.getByText(
'API-backed platforms like Proxmox, TrueNAS, and VMware use Platform connections instead of a dedicated install profile in Infrastructure Install.',
'VMware, TrueNAS, Proxmox, PBS, and PMG use API-backed source flows; standalone hosts use Pulse Agent.',
),
).toBeInTheDocument();
@ -111,27 +112,27 @@ describe('SetupCompletionPanel', () => {
expect(screen.queryByText('Windows (PowerShell as Administrator)')).not.toBeInTheDocument();
});
it('hands install into the canonical infrastructure workspace', async () => {
it('hands the primary action into the canonical source picker', async () => {
const onComplete = vi.fn();
render(() => <SetupCompletionPanel state={baseState} onComplete={onComplete} />);
fireEvent.click(screen.getAllByRole('button', { name: 'Open Infrastructure Install' })[0]);
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure?add=agent');
});
it('hands API-backed starts into the canonical platform connections workspace', async () => {
const onComplete = vi.fn();
render(() => <SetupCompletionPanel state={baseState} onComplete={onComplete} />);
fireEvent.click(screen.getByRole('button', { name: 'Open Platform connections' }));
fireEvent.click(screen.getByRole('button', { name: 'Add infrastructure' }));
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure?add=pick');
});
it('downloads credentials that point operators to the install workspace instead of inline commands', async () => {
it('keeps a direct Pulse Agent handoff for operators who already know the first source', async () => {
const onComplete = vi.fn();
render(() => <SetupCompletionPanel state={baseState} onComplete={onComplete} />);
fireEvent.click(screen.getByRole('button', { name: 'Install Pulse Agent' }));
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure?add=agent');
});
it('downloads credentials that point operators to the unified source picker instead of inline commands', async () => {
const anchorClickMock = vi.fn();
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName) => {
const element = document.createElementNS('http://www.w3.org/1999/xhtml', tagName);
@ -156,8 +157,8 @@ describe('SetupCompletionPanel', () => {
expect(content).toContain('Web Login:');
expect(content).toContain('Admin API Token:');
expect(content).toContain('Infrastructure:');
expect(content).toContain('https://pulse.example.com/settings/infrastructure?add=agent');
expect(content).toContain('Use Add connection to connect');
expect(content).toContain('https://pulse.example.com/settings/infrastructure?add=pick');
expect(content).toContain('Use Add infrastructure to choose');
expect(content).not.toContain('Example Install Command');
expect(content).not.toContain('Example Windows Install Command');
@ -201,7 +202,7 @@ describe('SetupCompletionPanel', () => {
expect(screen.getByText('First monitored system connected')).toBeInTheDocument();
expect(
screen.getByText(
'Your admin account is ready and Pulse is already receiving telemetry. Open the dashboard to verify the first overview, then return to Infrastructure Install when you want to add more host-installed systems.',
'Your admin account is ready and Pulse is already receiving telemetry. Open the dashboard to verify the first overview, then return to Add infrastructure when you want another Pulse Agent or platform API source.',
),
).toBeInTheDocument();
expect(
@ -209,28 +210,30 @@ describe('SetupCompletionPanel', () => {
).toBeInTheDocument();
expect(
screen.getByText(
'Infrastructure Install stays available any time you want to add more host-installed systems.',
'Add infrastructure stays available for more Pulse Agent systems or platform API inventory when a platform manages the estate.',
),
).toBeInTheDocument();
expect(screen.getAllByRole('button', { name: 'Go to Dashboard' }).length).toBeGreaterThan(0);
expect(
screen.getAllByRole('button', { name: 'Open Infrastructure Install' }).length,
).toBeGreaterThan(0);
expect(screen.queryByRole('button', { name: 'Open Platform connections' })).not.toBeInTheDocument();
expect(screen.getAllByRole('button', { name: 'Add infrastructure' }).length).toBeGreaterThan(0);
expect(screen.queryByRole('button', { name: 'Install Pulse Agent' })).not.toBeInTheDocument();
expect(trackAgentFirstConnectedMock).toHaveBeenCalledWith(
'setup_wizard_complete',
'first_agent',
);
const nextStepHeading = screen.getByRole('heading', { name: 'Open your first dashboard view' });
const nextStepCard = nextStepHeading.closest('div.bg-surface.rounded-md.border.border-border.p-6.text-left.mb-6');
const nextStepCard = nextStepHeading.closest(
'div.bg-surface.rounded-md.border.border-border.p-6.text-left.mb-6',
);
expect(nextStepCard).not.toBeNull();
fireEvent.click(within(nextStepCard as HTMLElement).getByRole('button', { name: 'Go to Dashboard' }));
fireEvent.click(
within(nextStepCard as HTMLElement).getByRole('button', { name: 'Go to Dashboard' }),
);
expect(onComplete).toHaveBeenCalledWith('/');
});
it('keeps platform connections available for API-backed starts after the first system connects', async () => {
it('keeps add infrastructure available for API-backed starts after the first system connects', async () => {
const onComplete = vi.fn();
apiFetchJSONMock.mockResolvedValue({
resources: [
@ -262,26 +265,28 @@ describe('SetupCompletionPanel', () => {
expect(screen.getByText('First monitored system connected')).toBeInTheDocument();
expect(
screen.getByText(
'Your admin account is ready and Pulse is already receiving telemetry. Open the dashboard to verify the first overview, then return to Platform connections when you want to add more API-backed systems.',
'Your admin account is ready and Pulse is already receiving telemetry. Open the dashboard to verify the first overview, then return to Add infrastructure when you want another platform API or Pulse Agent source.',
),
).toBeInTheDocument();
expect(
screen.getByText(
'Platform connections stays available any time you want to add more API-backed systems, and Infrastructure Install is ready when the next system should run the unified agent.',
'Add infrastructure stays available for more API-backed systems or Pulse Agent telemetry when a system needs node-local coverage.',
),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Go to Dashboard' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Open Platform connections' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Open Infrastructure Install' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add infrastructure' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Install Pulse Agent' })).not.toBeInTheDocument();
expect(trackAgentFirstConnectedMock).not.toHaveBeenCalled();
const nextStepHeading = screen.getByRole('heading', { name: 'Open your first dashboard view' });
const nextStepCard = nextStepHeading.closest('div.bg-surface.rounded-md.border.border-border.p-6.text-left.mb-6');
const nextStepCard = nextStepHeading.closest(
'div.bg-surface.rounded-md.border.border-border.p-6.text-left.mb-6',
);
expect(nextStepCard).not.toBeNull();
fireEvent.click(
within(nextStepCard as HTMLElement).getByRole('button', {
name: 'Open Platform connections',
name: 'Add infrastructure',
}),
);
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure?add=pick');
@ -318,16 +323,16 @@ describe('SetupCompletionPanel', () => {
expect(screen.getByText('VMware vSphere')).toBeInTheDocument();
expect(
screen.getByText(
'Your admin account is ready and Pulse is already receiving telemetry. Open the dashboard to verify the first overview, then return to Platform connections when you want to add more API-backed systems.',
'Your admin account is ready and Pulse is already receiving telemetry. Open the dashboard to verify the first overview, then return to Add infrastructure when you want another platform API or Pulse Agent source.',
),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Open Platform connections' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add infrastructure' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Open Platform connections' }));
fireEvent.click(screen.getByRole('button', { name: 'Add infrastructure' }));
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure?add=pick');
});
it('keeps both continuation paths visible when install-managed and API-backed systems are already present', async () => {
it('keeps add infrastructure visible when agent and API-backed systems are already present', async () => {
apiFetchJSONMock.mockResolvedValue({
resources: [
{
@ -367,16 +372,16 @@ describe('SetupCompletionPanel', () => {
expect(
screen.getByText(
'Your admin account is ready and Pulse is already receiving telemetry. Open the dashboard to verify the first overview, then return to Platform connections or Infrastructure Install when you want to add more systems.',
'Your admin account is ready and Pulse is already receiving telemetry. Open the dashboard to verify the first overview, then return to Add infrastructure when you want another platform API or Agent source.',
),
).toBeInTheDocument();
expect(
screen.getByText(
'Platform connections and Infrastructure Install both stay available any time you want to expand from this first system.',
'Add infrastructure stays available any time you want to expand from this first system with another API source, Agent source, or both.',
),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Open Platform connections' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Open Infrastructure Install' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add infrastructure' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Install Pulse Agent' })).not.toBeInTheDocument();
expect(trackAgentFirstConnectedMock).toHaveBeenCalledWith(
'setup_wizard_complete',
'first_agent',
@ -438,9 +443,7 @@ describe('SetupCompletionPanel', () => {
});
expect(screen.queryByText('Monitor from Anywhere')).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: /start free trial/i }),
).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /start free trial/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /set up relay/i })).not.toBeInTheDocument();
});
});

View file

@ -56,9 +56,9 @@ describe('SetupCompletionPreview', () => {
expect(apiFetchJSONMock).not.toHaveBeenCalled();
fireEvent.click(screen.getAllByRole('button', { name: 'Open Infrastructure Install' })[0]);
fireEvent.click(screen.getByRole('button', { name: 'Add infrastructure' }));
expect(navigateMock).toHaveBeenCalledWith('/settings/infrastructure?add=agent');
expect(navigateMock).toHaveBeenCalledWith('/settings/infrastructure?add=pick');
});
it('renders the VMware-connected preview scenario without polling runtime state', () => {
@ -69,6 +69,6 @@ describe('SetupCompletionPreview', () => {
expect(apiFetchJSONMock).not.toHaveBeenCalled();
expect(screen.getByText('First monitored system connected')).toBeInTheDocument();
expect(screen.getByText('VMware vSphere')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Open Platform connections' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add infrastructure' })).toBeInTheDocument();
});
});

View file

@ -65,7 +65,12 @@ describe('WelcomeStep', () => {
expect(screen.getByText('Unlock this Pulse server')).toBeInTheDocument();
expect(screen.getByText('Create the admin account')).toBeInTheDocument();
expect(screen.getByText('Install the first host')).toBeInTheDocument();
expect(screen.getByText('Choose the first source')).toBeInTheDocument();
expect(
screen.getByText(
'Connect a platform API, install Pulse Agent, or use both for full coverage.',
),
).toBeInTheDocument();
expect(screen.getByText('What this token does')).toBeInTheDocument();
expect(
screen.getByText(

View file

@ -14,8 +14,8 @@ import {
getPreferredResourceHostname,
} from '@/utils/resourceIdentity';
export type SetupCompletionConnectionPath = 'install' | 'platforms';
export type SetupCompletionPrimaryAction = 'dashboard' | 'install';
export type SetupCompletionConnectionPath = 'agent' | 'api';
export type SetupCompletionPrimaryAction = 'dashboard' | 'sources';
export interface ConnectedSetupSystem {
id: string;
@ -29,16 +29,16 @@ export interface SetupCompletionViewModel {
connectedSummaryLabel: string;
credentialsContinuationText: string;
hasConnectedSystems: boolean;
hasInstallConnectedSystems: boolean;
hasPlatformConnectedSystems: boolean;
hasAgentConnectedSystems: boolean;
hasApiConnectedSystems: boolean;
heroDescription: string;
heroTitle: string;
nextStepDetail: string;
nextStepSummary: string;
nextStepTitle: string;
primaryAction: SetupCompletionPrimaryAction;
showInstallAction: boolean;
showPlatformConnectionsAction: boolean;
showAddInfrastructureAction: boolean;
showAgentInstallAction: boolean;
}
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
@ -65,14 +65,14 @@ const getSetupCompletionPlatformLabel = (resource: Resource): string | null => {
return displayTokens[displayTokens.length - 1] || manifestPlatform.uiLabel;
};
const isPlatformConnectedSetupResource = (resource: Resource): boolean =>
const isApiConnectedSetupResource = (resource: Resource): boolean =>
sourcePlatformSupportsOnboardingPath(
getSetupCompletionPlatformKey(resource),
'platform-connections',
);
const isInstallConnectedSetupResource = (resource: Resource): boolean =>
isAgentFacetInfrastructureResource(resource) && !isPlatformConnectedSetupResource(resource);
const isAgentConnectedSetupResource = (resource: Resource): boolean =>
isAgentFacetInfrastructureResource(resource) && !isApiConnectedSetupResource(resource);
const getConnectedSetupSystemTypeLabel = (resource: Resource): string => {
return getSetupCompletionPlatformLabel(resource) || 'Agent';
@ -88,20 +88,17 @@ const getConnectedSetupSystemHost = (resource: Resource): string => {
const vmware = asRecord(platformData?.vmware);
return (
asString(proxmox?.instance) ||
asString(truenas?.hostname) ||
asString(vmware?.hostname) ||
''
asString(proxmox?.instance) || asString(truenas?.hostname) || asString(vmware?.hostname) || ''
);
};
const toConnectedSetupSystem = (resource: Resource): ConnectedSetupSystem | null => {
if (!isSetupCompletionInfrastructureResource(resource)) return null;
const connectionPath = isPlatformConnectedSetupResource(resource)
? 'platforms'
: isInstallConnectedSetupResource(resource)
? 'install'
const connectionPath = isApiConnectedSetupResource(resource)
? 'api'
: isAgentConnectedSetupResource(resource)
? 'agent'
: null;
if (!connectionPath) return null;
@ -130,8 +127,8 @@ export function buildSetupCompletionConnectedSystems(
continue;
}
if (existing.connectionPath === 'install' && nextSystem.connectionPath === 'platforms') {
existing.connectionPath = 'platforms';
if (existing.connectionPath === 'agent' && nextSystem.connectionPath === 'api') {
existing.connectionPath = 'api';
existing.typeLabel = nextSystem.typeLabel;
}
if (!existing.host && nextSystem.host) {
@ -148,57 +145,49 @@ export function buildSetupCompletionConnectedSystems(
}
const buildConnectedHeroDescription = (
hasInstallConnectedSystems: boolean,
hasPlatformConnectedSystems: boolean,
hasAgentConnectedSystems: boolean,
hasApiConnectedSystems: boolean,
): string => {
const prefix =
'Your admin account is ready and Pulse is already receiving telemetry. Open the dashboard to verify the first overview';
if (hasInstallConnectedSystems && hasPlatformConnectedSystems) {
return `${prefix}, then return to Platform connections or Infrastructure Install when you want to add more systems.`;
if (hasAgentConnectedSystems && hasApiConnectedSystems) {
return `${prefix}, then return to Add infrastructure when you want another platform API or Agent source.`;
}
if (hasPlatformConnectedSystems) {
return `${prefix}, then return to Platform connections when you want to add more API-backed systems.`;
if (hasApiConnectedSystems) {
return `${prefix}, then return to Add infrastructure when you want another platform API or Pulse Agent source.`;
}
return `${prefix}, then return to Infrastructure Install when you want to add more host-installed systems.`;
return `${prefix}, then return to Add infrastructure when you want another Pulse Agent or platform API source.`;
};
const buildCredentialsContinuationText = (
hasInstallConnectedSystems: boolean,
hasPlatformConnectedSystems: boolean,
_hasAgentConnectedSystems: boolean,
_hasApiConnectedSystems: boolean,
): string => {
if (hasInstallConnectedSystems && hasPlatformConnectedSystems) {
return 'the dashboard, Platform connections, or Infrastructure Install.';
}
if (hasPlatformConnectedSystems) {
return 'the dashboard, Platform connections, or Infrastructure Install.';
}
return 'the dashboard or Infrastructure Install.';
return 'the dashboard or Add infrastructure.';
};
const buildConnectedNextStepDetail = (
hasInstallConnectedSystems: boolean,
hasPlatformConnectedSystems: boolean,
hasAgentConnectedSystems: boolean,
hasApiConnectedSystems: boolean,
): string => {
if (hasInstallConnectedSystems && hasPlatformConnectedSystems) {
return 'Platform connections and Infrastructure Install both stay available any time you want to expand from this first system.';
if (hasAgentConnectedSystems && hasApiConnectedSystems) {
return 'Add infrastructure stays available any time you want to expand from this first system with another API source, Agent source, or both.';
}
if (hasPlatformConnectedSystems) {
return 'Platform connections stays available any time you want to add more API-backed systems, and Infrastructure Install is ready when the next system should run the unified agent.';
if (hasApiConnectedSystems) {
return 'Add infrastructure stays available for more API-backed systems or Pulse Agent telemetry when a system needs node-local coverage.';
}
return 'Infrastructure Install stays available any time you want to add more host-installed systems.';
return 'Add infrastructure stays available for more Pulse Agent systems or platform API inventory when a platform manages the estate.';
};
export function buildSetupCompletionViewModel(
connectedSystems: readonly ConnectedSetupSystem[],
): SetupCompletionViewModel {
const hasConnectedSystems = connectedSystems.length > 0;
const hasInstallConnectedSystems = connectedSystems.some(
(system) => system.connectionPath === 'install',
);
const hasPlatformConnectedSystems = connectedSystems.some(
(system) => system.connectionPath === 'platforms',
const hasAgentConnectedSystems = connectedSystems.some(
(system) => system.connectionPath === 'agent',
);
const hasApiConnectedSystems = connectedSystems.some((system) => system.connectionPath === 'api');
const connectedSystemNoun = connectedSystems.length === 1 ? 'system' : 'systems';
const nextStepSummary =
@ -209,45 +198,42 @@ export function buildSetupCompletionViewModel(
if (!hasConnectedSystems) {
return {
connectedSummaryLabel: 'Connected (0 systems)',
credentialsContinuationText: 'Infrastructure Install or Platform connections.',
credentialsContinuationText: 'Add infrastructure.',
hasConnectedSystems,
hasInstallConnectedSystems,
hasPlatformConnectedSystems,
hasAgentConnectedSystems,
hasApiConnectedSystems,
heroDescription:
'Your admin account is ready. Next, choose the first infrastructure path: open Infrastructure Install for a host that should run the unified agent, or open Platform connections for API-backed platforms such as Proxmox, TrueNAS, and VMware.',
heroTitle: 'Connect your first monitored system',
'Your admin account is ready. Next, choose how the first system should enter the unified infrastructure model: platform API inventory, Pulse Agent telemetry, or both.',
heroTitle: 'Choose your first infrastructure source',
nextStepDetail:
'If the first system is API-backed, use Platform connections instead of starting with host install.',
nextStepSummary: 'Open Infrastructure Install to bring your first monitored system into Pulse.',
nextStepTitle: 'Choose your first infrastructure path',
primaryAction: 'install',
showInstallAction: false,
showPlatformConnectionsAction: true,
'Start with a platform API when a platform manages the estate. Install Pulse Agent when the system itself should report node-local telemetry.',
nextStepSummary: 'Open Add infrastructure to choose a platform API, Pulse Agent, or both.',
nextStepTitle: 'Choose the first source strategy',
primaryAction: 'sources',
showAddInfrastructureAction: false,
showAgentInstallAction: true,
};
}
return {
connectedSummaryLabel: `Connected (${connectedSystems.length} ${connectedSystemNoun})`,
credentialsContinuationText: buildCredentialsContinuationText(
hasInstallConnectedSystems,
hasPlatformConnectedSystems,
hasAgentConnectedSystems,
hasApiConnectedSystems,
),
hasConnectedSystems,
hasInstallConnectedSystems,
hasPlatformConnectedSystems,
hasAgentConnectedSystems,
hasApiConnectedSystems,
heroDescription: buildConnectedHeroDescription(
hasInstallConnectedSystems,
hasPlatformConnectedSystems,
hasAgentConnectedSystems,
hasApiConnectedSystems,
),
heroTitle: 'First monitored system connected',
nextStepDetail: buildConnectedNextStepDetail(
hasInstallConnectedSystems,
hasPlatformConnectedSystems,
),
nextStepDetail: buildConnectedNextStepDetail(hasAgentConnectedSystems, hasApiConnectedSystems),
nextStepSummary,
nextStepTitle: 'Open your first dashboard view',
primaryAction: 'dashboard',
showInstallAction: hasConnectedSystems,
showPlatformConnectionsAction: hasPlatformConnectedSystems,
showAddInfrastructureAction: hasConnectedSystems,
showAgentInstallAction: false,
};
}

View file

@ -176,7 +176,7 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
</p>
<p class="mt-4 text-sm text-muted max-w-xl mx-auto animate-fade-in delay-300">
You are about to do three things: unlock setup on this Pulse server, create your admin
account, and install the first system you want Pulse to monitor.
account, and choose the first infrastructure source Pulse should monitor.
</p>
</div>
@ -203,9 +203,9 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
<div class="text-[11px] font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300">
Step 3
</div>
<div class="mt-1 text-sm font-semibold text-base-content">Install the first host</div>
<div class="mt-1 text-sm font-semibold text-base-content">Choose the first source</div>
<p class="mt-1 text-xs text-muted">
Open Infrastructure Install and connect the first system you want Pulse to monitor.
Connect a platform API, install Pulse Agent, or use both for full coverage.
</p>
</div>
</div>
@ -218,9 +218,9 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
Unlock Setup
</h3>
<p class="text-sm text-muted mb-6">
Run the following command on the Pulse server to retrieve the one-time bootstrap
token that unlocks this wizard. Do not paste the raw `.bootstrap_token` file
contents directly.
Run the following command on the Pulse server to retrieve the one-time bootstrap token
that unlocks this wizard. Do not paste the raw `.bootstrap_token` file contents
directly.
</p>
<div class="mb-4 rounded-md border border-blue-200 bg-blue-50 px-4 py-3 dark:border-blue-800 dark:bg-blue-950/40">

View file

@ -35,7 +35,9 @@ describe('WhatsNewModal', () => {
expect(whatsNewModalSource).not.toContain('createSignal');
expect(whatsNewModalSource).not.toContain('WHATS_NEW_NAV_V2_SHOWN');
expect(whatsNewModalSource).not.toContain('Migration guide');
expect(whatsNewModalSource).not.toContain('https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md');
expect(whatsNewModalSource).not.toContain(
'https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md',
);
expect(whatsNewModalSource).not.toContain('bg-gradient');
expect(whatsNewModalSource).not.toContain('backdrop-blur-sm');
@ -60,8 +62,12 @@ describe('WhatsNewModal', () => {
expect(whatsNewModalModelSource).toContain('WHATS_NEW_KICKER_LABEL');
expect(whatsNewModalModelSource).toContain('WHATS_NEW_PROGRESS_PREFIX');
expect(whatsNewModalModelSource).toContain("WHATS_NEW_PRIMARY_ACTION_LABEL = 'Done'");
expect(whatsNewModalModelSource).not.toContain('https://github.com/rcourtman/Pulse/blob/main/docs/README.md');
expect(whatsNewModalModelSource).not.toContain('https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md');
expect(whatsNewModalModelSource).not.toContain(
'https://github.com/rcourtman/Pulse/blob/main/docs/README.md',
);
expect(whatsNewModalModelSource).not.toContain(
'https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md',
);
expect(whatsNewModalModelSource).toContain('WHATS_NEW_DOCS_LABEL');
expect(whatsNewModalModelSource).toContain("title: 'Dashboard'");
});
@ -73,7 +79,9 @@ describe('WhatsNewModal', () => {
expect(dialog).toBeInTheDocument();
expect(within(dialog).getByText('Step 1 of 5')).toBeInTheDocument();
expect(within(dialog).getByText('Nav guide')).toBeInTheDocument();
expect(within(dialog).getByText(/Start here for health, alerts, capacity/i)).toBeInTheDocument();
expect(
within(dialog).getByText(/Start here for the live estate overview/i),
).toBeInTheDocument();
expect(within(dialog).queryByText('Where Things Moved')).not.toBeInTheDocument();
expect(within(dialog).getByRole('link', { name: 'Navigation guide' })).toBeInTheDocument();
expect(within(dialog).getByRole('link', { name: 'Telemetry details' })).toBeInTheDocument();
@ -129,26 +137,30 @@ describe('WhatsNewModal', () => {
it('advances through the guided tour and finishes on the last step', async () => {
render(() => <WhatsNewModal />);
expect(await screen.findByText(/Start here for health, alerts, capacity/i)).toBeInTheDocument();
expect(await screen.findByText(/Start here for the live estate overview/i)).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
expect(await screen.findByText(/Use this for nodes, hosts, clusters/i)).toBeInTheDocument();
expect(
await screen.findByText(/Use this to add and manage infrastructure sources/i),
).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
expect(await screen.findByText(/Use this for VMs, containers, and pods/i)).toBeInTheDocument();
expect(await screen.findByText(/Use this for VMs, containers, pods/i)).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
expect(await screen.findByText(/Use this for datastores, pools, disks, and capacity/i)).toBeInTheDocument();
expect(await screen.findByText(/Use this for pools, datastores, disks/i)).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
expect(await screen.findByText(/Use this for backups, snapshots, and replication/i)).toBeInTheDocument();
expect(
await screen.findByText(/Use this for backup coverage, snapshots, replication/i),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Done' })).toBeInTheDocument();
});
it('lets the user jump to a tour stop directly from the stop map', async () => {
render(() => <WhatsNewModal />);
expect(await screen.findByText(/Start here for health, alerts, capacity/i)).toBeInTheDocument();
expect(await screen.findByText(/Start here for the live estate overview/i)).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /Workloads/i }));
expect(await screen.findByText(/Use this for VMs, containers, and pods/i)).toBeInTheDocument();
expect(await screen.findByText(/Use this for VMs, containers, pods/i)).toBeInTheDocument();
expect(screen.getByText('Step 3 of 5')).toBeInTheDocument();
});
@ -166,6 +178,8 @@ describe('WhatsNewModal', () => {
const dialog = await screen.findByRole('dialog', { name: 'Pulse navigation guide' });
expect(within(dialog).getByText('Step 5 of 5')).toBeInTheDocument();
expect(within(dialog).getByText(/Use this for backups, snapshots, and replication/i)).toBeInTheDocument();
expect(
within(dialog).getByText(/Use this for backup coverage, snapshots, replication/i),
).toBeInTheDocument();
});
});

View file

@ -14,35 +14,37 @@ export const WHATS_NEW_PRIVACY_URL = PRIVACY_DOC_URL;
export const WHATS_NEW_FEATURE_CARDS: WhatsNewFeatureCard[] = [
{
accent: 'border-indigo-200 bg-indigo-50 dark:border-indigo-800 dark:bg-indigo-900',
description: 'Start here for health, alerts, capacity, and recent activity.',
description:
'Start here for the live estate overview: health, alerts, capacity, and recent activity.',
icon: 'dashboard',
target: 'dashboard',
title: 'Dashboard',
},
{
accent: 'border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900',
description: 'Use this for nodes, hosts, clusters, and other platform roots.',
description:
'Use this to add and manage infrastructure sources: platform API inventory, Pulse Agent telemetry, and discovered candidates.',
icon: 'infrastructure',
target: 'infrastructure',
title: 'Infrastructure',
},
{
accent: 'border-rose-200 bg-rose-50 dark:border-rose-800 dark:bg-rose-900',
description: 'Use this for VMs, containers, and pods.',
description: 'Use this for VMs, containers, pods, and other running workloads.',
icon: 'workloads',
target: 'workloads',
title: 'Workloads',
},
{
accent: 'border-emerald-200 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-900',
description: 'Use this for datastores, pools, disks, and capacity.',
description: 'Use this for pools, datastores, disks, datasets, and capacity.',
icon: 'storage',
target: 'storage',
title: 'Storage',
},
{
accent: 'border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-900',
description: 'Use this for backups, snapshots, and replication.',
description: 'Use this for backup coverage, snapshots, replication, and restore readiness.',
icon: 'recovery',
target: 'recovery',
title: 'Recovery',

View file

@ -145,7 +145,7 @@ export function InfrastructurePageSurface() {
actions={
<button
type="button"
onClick={() => navigate(buildInfrastructureOnboardingPath('agent'))}
onClick={() => navigate(buildInfrastructureOnboardingPath('pick'))}
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"
>
<SettingsIcon class="h-3.5 w-3.5" />

View file

@ -283,7 +283,7 @@ export default function Dashboard() {
<p class="mt-2 text-sm text-muted">{dashboardNoResourcesState().description}</p>
<button
type="button"
onClick={() => navigate(buildInfrastructureOnboardingPath('agent'))}
onClick={() => navigate(buildInfrastructureOnboardingPath('pick'))}
class="mt-4 inline-flex items-center rounded-md border border-transparent bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700"
>
{dashboardNoResourcesState().actionLabel}

View file

@ -35,7 +35,7 @@ export default function RuntimeHome() {
if (summaryFailed()) {
return DASHBOARD_PATH;
}
return summaryHasResources() ? DASHBOARD_PATH : buildInfrastructureOnboardingPath('agent');
return summaryHasResources() ? DASHBOARD_PATH : buildInfrastructureOnboardingPath('pick');
});
onMount(() => {

View file

@ -204,13 +204,13 @@ describe('Dashboard page module contract', () => {
).toBeInTheDocument();
expect(
screen.getByText(
'The dashboard appears after Pulse receives its first monitored system. Add a Pulse Agent or platform API source from Infrastructure setup, then this page becomes the live estate overview.',
'The dashboard appears after Pulse receives its first monitored system. Add an infrastructure source with API inventory, Agent telemetry, or both, then this page becomes the live estate overview.',
),
).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Add infrastructure source' }));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure?add=agent');
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure?add=pick');
});
it('renders the governed storage and recovery dashboard panels', () => {

View file

@ -40,27 +40,27 @@ describe('Infrastructure empty state', () => {
navigateSpy.mockReset();
});
it('shows empty state with direct install guidance when no resources exist', async () => {
it('shows empty state with source-strategy guidance when no resources exist', async () => {
const { getByText } = render(() => <Infrastructure />);
await waitFor(() => {
expect(getByText('No infrastructure resources yet')).toBeInTheDocument();
expect(getByText('No infrastructure sources yet')).toBeInTheDocument();
});
const button = getByText('Open Infrastructure Install');
const button = getByText('Add infrastructure source');
expect(button).toBeInTheDocument();
expect(button.closest('button')).toBeInTheDocument();
});
it('navigates to the install workspace when the empty-state button is clicked', async () => {
it('navigates to the source picker when the empty-state button is clicked', async () => {
const { getByText } = render(() => <Infrastructure />);
await waitFor(() => {
expect(getByText('Open Infrastructure Install')).toBeInTheDocument();
expect(getByText('Add infrastructure source')).toBeInTheDocument();
});
fireEvent.click(getByText('Open Infrastructure Install'));
fireEvent.click(getByText('Add infrastructure source'));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure?add=agent');
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure?add=pick');
});
});

View file

@ -54,7 +54,7 @@ describe('RuntimeHome', () => {
expect(getDashboardSummaryMock).not.toHaveBeenCalled();
});
it('routes hosted empty workspaces into infrastructure install', async () => {
it('routes hosted empty workspaces into the infrastructure source picker', async () => {
getDashboardSummaryMock.mockResolvedValue({
health: { totalResources: 0 },
});
@ -63,7 +63,7 @@ describe('RuntimeHome', () => {
await waitFor(() => {
expect(getDashboardSummaryMock).toHaveBeenCalledTimes(1);
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure?add=agent', {
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure?add=pick', {
replace: true,
});
});

View file

@ -12,10 +12,10 @@ import {
describe('dashboardEmptyStatePresentation', () => {
it('returns the infrastructure onboarding empty state', () => {
expect(getDashboardInfrastructureEmptyState()).toEqual({
title: 'No infrastructure hosts connected',
title: 'No infrastructure sources connected',
description:
'To start using Pulse, first add your infrastructure in Settings → Infrastructure → Install on a host. If you want an API-backed platform such as Proxmox or TrueNAS instead, use Settings → Infrastructure → Platform connections.',
actionLabel: 'Open infrastructure setup',
'Start in Settings → Infrastructure by choosing a source strategy. Connect a platform API for inventory and health, install Pulse Agent for host telemetry, or use both when you want full coverage.',
actionLabel: 'Add infrastructure source',
});
});
@ -79,7 +79,7 @@ describe('dashboardEmptyStatePresentation', () => {
expect(getDashboardNoResourcesState()).toEqual({
title: 'Connect your first infrastructure source',
description:
'The dashboard appears after Pulse receives its first monitored system. Add a Pulse Agent or platform API source from Infrastructure setup, then this page becomes the live estate overview.',
'The dashboard appears after Pulse receives its first monitored system. Add an infrastructure source with API inventory, Agent telemetry, or both, then this page becomes the live estate overview.',
actionLabel: 'Add infrastructure source',
});
});

View file

@ -8,10 +8,10 @@ import {
describe('infrastructureEmptyStatePresentation', () => {
it('returns the infrastructure onboarding empty state', () => {
expect(getInfrastructureEmptyState()).toEqual({
title: 'No infrastructure resources yet',
title: 'No infrastructure sources yet',
description:
'Start by opening Settings → Infrastructure → Install on a host and adding the first system you want Pulse to monitor. If you prefer an API-backed platform such as Proxmox or TrueNAS instead, use Platform connections.',
actionLabel: 'Open Infrastructure Install',
'Start in Settings → Infrastructure by choosing a source strategy. Connect a platform API for inventory and health, install Pulse Agent for host telemetry, or use both when you want full coverage.',
actionLabel: 'Add infrastructure source',
});
});

View file

@ -161,11 +161,12 @@ describe('infrastructureOnboardingPresentation', () => {
});
expect(getInfrastructureEmptyStateSummary()).toBe(
'Add infrastructure systems to start monitoring your environment.',
'Choose an infrastructure source to start monitoring your environment.',
);
expect(getInfrastructureEmptyStateDetail()).toContain(
'Supported source types include VMware vCenter',
);
expect(getInfrastructureEmptyStateDetail()).toContain('Available system types: VMware vCenter');
expect(getInfrastructureEmptyStateDetail()).toContain('standalone hosts through Pulse Agent');
expect(getInfrastructureEmptyStateDetail()).toContain('Docker and Kubernetes are discovered');
expect(getInfrastructureEmptyStateDetail()).toContain('VMware vCenter is also available now.');
});
});

View file

@ -1,9 +1,9 @@
export function getDashboardInfrastructureEmptyState() {
return {
title: 'No infrastructure hosts connected',
title: 'No infrastructure sources connected',
description:
'To start using Pulse, first add your infrastructure in Settings → Infrastructure → Install on a host. If you want an API-backed platform such as Proxmox or TrueNAS instead, use Settings → Infrastructure → Platform connections.',
actionLabel: 'Open infrastructure setup',
'Start in Settings → Infrastructure by choosing a source strategy. Connect a platform API for inventory and health, install Pulse Agent for host telemetry, or use both when you want full coverage.',
actionLabel: 'Add infrastructure source',
} as const;
}
@ -59,7 +59,7 @@ export function getDashboardNoResourcesState() {
return {
title: 'Connect your first infrastructure source',
description:
'The dashboard appears after Pulse receives its first monitored system. Add a Pulse Agent or platform API source from Infrastructure setup, then this page becomes the live estate overview.',
'The dashboard appears after Pulse receives its first monitored system. Add an infrastructure source with API inventory, Agent telemetry, or both, then this page becomes the live estate overview.',
actionLabel: 'Add infrastructure source',
} as const;
}

View file

@ -1,9 +1,9 @@
export function getInfrastructureEmptyState() {
return {
title: 'No infrastructure resources yet',
title: 'No infrastructure sources yet',
description:
'Start by opening Settings → Infrastructure → Install on a host and adding the first system you want Pulse to monitor. If you prefer an API-backed platform such as Proxmox or TrueNAS instead, use Platform connections.',
actionLabel: 'Open Infrastructure Install',
'Start in Settings → Infrastructure by choosing a source strategy. Connect a platform API for inventory and health, install Pulse Agent for host telemetry, or use both when you want full coverage.',
actionLabel: 'Add infrastructure source',
} as const;
}

View file

@ -319,7 +319,7 @@ export const getInfrastructureSupportSummaryBadges = (): {
});
export const getInfrastructureEmptyStateSummary = (): string =>
'Add infrastructure systems to start monitoring your environment.';
'Choose an infrastructure source to start monitoring your environment.';
export const getInfrastructureEmptyStateDetail = (): string =>
'Available system types: VMware vCenter, TrueNAS SCALE, Proxmox VE, Proxmox Backup Server, Proxmox Mail Gateway, and standalone hosts through Pulse Agent. Docker and Kubernetes are discovered from supported agent hosts. VMware vCenter is also available now.';
'Supported source types include VMware vCenter, TrueNAS SCALE, Proxmox VE, Proxmox Backup Server, Proxmox Mail Gateway, and standalone hosts through Pulse Agent. Docker and Kubernetes are discovered from supported agent hosts.';