Add first-host install success handoff

This commit is contained in:
rcourtman
2026-03-27 09:57:04 +00:00
parent 4bec0bde51
commit 6b1fedc0c1
5 changed files with 107 additions and 9 deletions
@@ -153,6 +153,13 @@ management, and fleet control surfaces.
must default to the install workspace, and the workspace shell must make
the first-host sequence explicit before operators drift into reporting and
control surfaces.
8. Keep post-install lifecycle completion explicit inside
`frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`
and `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`.
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
generic lookup result.
## Current State
@@ -202,6 +202,13 @@ Own canonical runtime payload shapes between backend and frontend.
15. Keep recovery payload platform vocabulary canonical across that same `/api/recovery/*` surface: point payloads must expose `platform`, rollup payloads must expose `platforms`, and any compatibility `provider` / `providers` aliases must remain secondary fallback fields rather than replacing the shared response model
16. Keep recovery linked-resource vocabulary canonical across that same `/api/recovery/*` surface: points and rollups must expose `itemResourceId` as the canonical linked-resource field, accepted legacy `subjectResourceId` aliases must remain compatibility-only input or secondary payload fields, and the shared proof surface must pin that normalization in the same slice as any handler change
17. Keep recovery external item-reference vocabulary canonical across that same `/api/recovery/*` surface: point and rollup payloads must expose `itemRef` as the canonical external item-reference field, accepted legacy `subjectRef` aliases must remain compatibility-only secondary payload fields, and the shared proof surface must pin that normalization in the same slice as any handler change
18. Keep first-host lookup completion explicit on the shared install-state API
boundary: when
`frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`
receives a successful connected-agent lookup result, the canonical install
flow must expose direct navigation into `/dashboard` and
`/settings/infrastructure/operations` rather than leaving the operator on a
transport-only status readout.
## Current State
@@ -513,11 +513,33 @@ export const InfrastructureInstallerSection: Component = () => {
const agent = () => result().agent!;
const lookupStatusPresentation = () =>
getUnifiedAgentLookupStatusPresentation(agent().connected);
const isConnected = () => agent().connected;
return (
<div class="space-y-1 rounded-md border border-blue-200 bg-surface px-3 py-2 text-xs text-blue-900 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-100">
<div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
<div class="text-sm font-semibold">{agent().displayName || agent().hostname}</div>
<div
class={`space-y-3 rounded-md border px-3 py-3 text-xs ${
isConnected()
? 'border-emerald-200 bg-emerald-50 text-emerald-950 dark:border-emerald-700 dark:bg-emerald-900 dark:text-emerald-50'
: 'border-blue-200 bg-surface text-blue-900 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-100'
}`}
>
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div class="space-y-1">
<div class="text-sm font-semibold">
{isConnected() ? 'First host connected' : agent().displayName || agent().hostname}
</div>
<p
class={`text-xs ${
isConnected()
? 'text-emerald-800 dark:text-emerald-200'
: 'text-blue-800 dark:text-blue-200'
}`}
>
{isConnected()
? `${agent().displayName || agent().hostname} is reporting live telemetry to Pulse. Open the dashboard to verify your first overview, or continue in Reporting & control to inspect this host and add more infrastructure.`
: `${agent().displayName || agent().hostname} has been found, but Pulse is not receiving a live check-in yet. Keep the installer running on that machine and check again.`}
</p>
</div>
<div class="flex items-center gap-2">
<span
class={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-semibold ${lookupStatusPresentation().badgeClass}`}
@@ -529,13 +551,39 @@ export const InfrastructureInstallerSection: Component = () => {
</span>
</div>
</div>
<div>
Last seen {formatRelativeTime(agent().lastSeen)} (
{formatAbsoluteTime(agent().lastSeen)})
<div class="space-y-1">
<div>
Last seen {formatRelativeTime(agent().lastSeen)} (
{formatAbsoluteTime(agent().lastSeen)})
</div>
<Show when={agent().agentVersion}>
<div
class={`text-xs ${
isConnected()
? 'text-emerald-800 dark:text-emerald-200'
: 'text-blue-700 dark:text-blue-200'
}`}
>
Agent version {agent().agentVersion}
</div>
</Show>
</div>
<Show when={agent().agentVersion}>
<div class="text-xs text-blue-700 dark:text-blue-200">
Agent version {agent().agentVersion}
<Show when={isConnected()}>
<div class="flex flex-col gap-2 sm:flex-row">
<button
type="button"
onClick={state.openDashboard}
class="inline-flex items-center justify-center rounded-md bg-emerald-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-700"
>
Open dashboard
</button>
<button
type="button"
onClick={state.openInfrastructureInventory}
class="inline-flex items-center justify-center rounded-md border border-emerald-300 bg-white px-3 py-2 text-sm font-medium text-emerald-900 transition-colors hover:bg-emerald-100 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-100 dark:hover:bg-emerald-800"
>
Open reporting &amp; control
</button>
</div>
</Show>
</div>
@@ -25,6 +25,18 @@ import type {
State,
} from '@/types/api';
const { navigateMock } = vi.hoisted(() => ({
navigateMock: vi.fn(),
}));
vi.mock('@solidjs/router', async () => {
const actual = await vi.importActual<typeof import('@solidjs/router')>('@solidjs/router');
return {
...actual,
useNavigate: () => navigateMock,
};
});
let mockWsStore: {
state: Pick<State, 'connectedInfrastructure'>;
connected: () => boolean;
@@ -561,6 +573,7 @@ const getConnectionUrlInput = (): HTMLInputElement => {
beforeEach(() => {
securityStatusResponse = { requiresAuth: true, apiTokenConfigured: false };
navigateMock.mockReset();
lookupMock.mockReset();
createTokenMock.mockReset();
deleteAgentMock.mockReset();
@@ -698,6 +711,18 @@ describe('InfrastructureOperationsController agent lookup', () => {
await waitFor(() => expect(lookupMock).toHaveBeenCalled(), { interval: 0 });
await waitFor(() => expect(screen.getByText('Connected')).toBeInTheDocument(), { interval: 0 });
expect(screen.getByText('First host connected')).toBeInTheDocument();
expect(
screen.getByText(
'Host One is reporting live telemetry to Pulse. Open the dashboard to verify your first overview, or continue in Reporting & control to inspect this host and add more infrastructure.',
),
).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Open dashboard' }));
expect(navigateMock).toHaveBeenCalledWith('/dashboard');
fireEvent.click(screen.getByRole('button', { name: 'Open reporting & control' }));
expect(navigateMock).toHaveBeenCalledWith('/settings/infrastructure/operations');
});
it('shows error message when agent is not found', async () => {
@@ -16,6 +16,7 @@ import { copyToClipboard } from '@/utils/clipboard';
import { getPulseBaseUrl } from '@/utils/url';
import { logger } from '@/utils/logger';
import { STORAGE_KEYS } from '@/utils/localStorage';
import { buildInfrastructureWorkspacePath } from './infrastructureWorkspaceModel';
import {
buildUnixAgentInstallCommand,
buildWindowsAgentInstallCommand,
@@ -330,6 +331,14 @@ Generate a scoped install token below before copying Unified Agent install comma
navigate('/settings/infrastructure/proxmox');
};
const openDashboard = () => {
navigate('/dashboard');
};
const openInfrastructureInventory = () => {
navigate(buildInfrastructureWorkspacePath('inventory'));
};
const isEmbedded = () => options.embedded ?? false;
return {
@@ -361,7 +370,9 @@ Generate a scoped install token below before copying Unified Agent install comma
lookupLoading,
lookupResult,
lookupValue,
openDashboard,
openDirectProxmoxSetup,
openInfrastructureInventory,
requiresToken,
selectedAgentUrl,
setCustomAgentUrl,