feat(setup): point unlicensed Pro installs at license activation

A fresh Pro install has no way to discover where its activation key goes:
the Plans & Billing nav entry is navigation-suppressed until a license or
subscription exists (hideWhenUpgradeHidden), which hides the only panel
holding the activation form exactly while the install is unlicensed. A Pro
customer hit this after the broker download flow and emailed support.

The setup completion panel now probes /api/license/runtime-capabilities
and /api/license/status with the setup token and, only when the compiled
Pro build reports no valid license, renders an Activate Pulse Pro card
linking straight to /settings/pulse-intelligence/billing/plan (direct
routes stay reachable by design). Community builds, licensed installs,
and failed probes all keep the card hidden, so the self-hosted opt-in
commercial posture is untouched. The pro-unlicensed preview scenario
keeps browser proof deterministic; strings are localized in EN/DE/ES;
agent-lifecycle, frontend-primitives, and security-privacy contracts
record the new handoff and catalog boundaries.
This commit is contained in:
rcourtman
2026-08-10 00:02:45 +01:00
parent a7849c9ef0
commit 1b9bc9482e
15 changed files with 399 additions and 25 deletions
@@ -5449,6 +5449,19 @@ primitive, carry localized names for the exact value being copied, and retain
a 40-pixel mobile touch floor. The setup surface must not reintroduce raw,
unlabelled icon buttons or a phone-only copy-control fork.
The setup completion surface also owns the Pro activation handoff. When the
compiled Pro edition reports no valid license (both read with the setup
token from `/api/license/runtime-capabilities` and `/api/license/status`,
gated by `shouldShowSetupProActivationPointer` requiring an explicitly
invalid license, never a failed or missing probe), the completion panel
renders a factual activation card whose action targets the direct
`/settings/pulse-intelligence/billing/plan` route. That direct-route target
is deliberate: the Plans & Billing navigation entry stays
navigation-suppressed for unlicensed self-hosted sessions, so the pointer
must not instruct users to find a hidden nav item, and it must never appear
on community builds, licensed installs, or probe failures — the self-hosted
opt-in commercial posture is unchanged.
Agent-facing action completion events and resource-context history now expose
the canonical `ActionResultV2` alongside bounded legacy projections. Consumers
must branch on execution and verification separately: a completed transport is
@@ -2117,6 +2117,16 @@ default` instead of fusing provider and badge text such as
`/api/state` runtime or inherit whatever connected systems happen to exist
on the current backend, and browser proof for `/preview/setup-complete`
must select explicit preview scenarios instead of ambient runtime state.
That determinism boundary also covers the setup completion Pro activation
pointer: every preview scenario in
`frontend-modern/src/components/SetupWizard/setupCompletionPreviewScenarios.ts`
must carry an explicit `proActivation` boolean so preview rendering never
falls through to the live license probe, and the `pro-unlicensed` scenario
is the canonical browser proof for the pointer. The pointer's localized
strings (`setup.completion.proActivation.*`) are part of the first-session
monitoring journey catalog and must stay in
`FIRST_SESSION_MONITORING_MIGRATED_MESSAGE_KEYS` with non-identical DE/ES
translations.
13. Keep AI settings setup UI backend-driven:
`frontend-modern/src/components/Settings/useAISettingsState.ts` and
`frontend-modern/src/components/Settings/AISettingsDialogs.tsx` may collect
@@ -353,6 +353,13 @@ the `white_label` branding entitlement.
only for redirect/manual-link copy and must preserve `Pulse Account`,
security/privacy disclosures, token names, API field names, route/query keys,
and purchase-return state exactly.
The first-session setup completion Pro activation pointer
(`setup.completion.proActivation.*`) is likewise product-surface copy in the
shared catalogs: it must stay a factual statement of build edition and
license state plus the activation action, must not add upgrade-marketing
language, and must not alter API token, authentication, privacy-disclosure,
or non-translatable security terms. `Pulse Pro` stays untranslated as a
product identifier across locales.
Alerts-owned localization may distinguish detector state, in-product alert
visibility, and external notification delivery, but it must preserve all
security/privacy terminology and destination confidentiality. Pausing
+28 -23
View File
@@ -1,45 +1,50 @@
{
"version": 1,
"base_sha": "cd63e0848abe224967ff5429e56bfcf06c5860c6",
"verified_at": "2026-08-09T19:42:00Z",
"base_sha": "a7849c9ef04ababee688135394c81adca61c0ea2",
"verified_at": "2026-08-09T22:58:23Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx",
"frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx",
"frontend-modern/src/components/Settings/infrastructureAgentUpdateCommandsModel.ts",
"frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx"
"frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx",
"frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx",
"frontend-modern/src/components/SetupWizard/setupCompletionModel.ts",
"frontend-modern/src/components/SetupWizard/setupCompletionPreviewScenarios.ts",
"frontend-modern/src/i18n/messages.de.ts",
"frontend-modern/src/i18n/messages.es.ts",
"frontend-modern/src/i18n/messages.ts"
],
"content_sha256": {
"frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx": "99368f451d9153a735180d0f3889bf0d7983af6bce2f9e3012f87b7ad594d265",
"frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx": "595c0509e2c93574ccea5669f06c5143d4ece7e4772906b8d41d04392e0431cc",
"frontend-modern/src/components/Settings/infrastructureAgentUpdateCommandsModel.ts": "baf908e004f6805b4dfcd013a139ee822a7e91589fa4d086a9440cabfb6c812a",
"frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx": "9a1a2c2d9f7712e1fbf4dbc462d75fe56c79c63aed809196e780b74e2cbd0381"
"frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx": "c349eb0a08633a2f331b2c79c4aa5f74f27d62c1342d2dd1d3a69558d05c6ac3",
"frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx": "15d130f8b6cad7079db2215e4df5ab9d282bdb03f0e644578e515086e47e24c8",
"frontend-modern/src/components/SetupWizard/setupCompletionModel.ts": "0111a04873eff8c028ffdeb8a9616e60ce54e72177c83445631413144eb13355",
"frontend-modern/src/components/SetupWizard/setupCompletionPreviewScenarios.ts": "224c4424ac0934c42275adc2f7dad0c536adf06b245120cfcf7cf2b83592bdf3",
"frontend-modern/src/i18n/messages.de.ts": "8f1131c143c64e207460a32a9b78a20289084c3a1ada0ea3f7581c1ef63c7abd",
"frontend-modern/src/i18n/messages.es.ts": "5595cc905c70d2a4bfc8f7541edc733896449dac8ee9af518d6824bb613295b0",
"frontend-modern/src/i18n/messages.ts": "467ceeefc62f6f65f33db541de98e830168fa195863cac3cb5afd9c0495f7142"
},
"routes": [
"/settings/infrastructure?add=linux-host",
"/settings/infrastructure?agentDoctor=1"
"/preview/setup-complete?scenario=pro-unlicensed",
"/preview/setup-complete",
"/settings/pulse-intelligence/billing/plan"
],
"viewports": [
{
"width": 1440,
"height": 900
"height": 1600
},
{
"width": 390,
"height": 844
"height": 1500
}
],
"states": [
"The host installer started with a reporting-only token and copy-ready commands, then immediately locked and removed the previous credential from every rendered command while a command-enabled replacement token was pending.",
"Successful replacement restored the advanced options with Pulse command execution checked and exposed only commands containing the new exec-scoped token.",
"A failed attempt to remove command execution restored the complete prior checked option and exec-scoped token pair, retained copy-ready commands, and surfaced the minting error instead of composing mixed state.",
"Agent Doctor rendered agent_exec_scope_missing as a critical credential diagnosis with explicit agent:exec evidence and a token-gated host-local authentication repair.",
"After Agent Doctor generated the repair credential, the request carried enableCommands=true and the unlocked repair command used an ephemeral token file plus --enable-commands."
"The pro-unlicensed preview scenario rendered the Activate Pulse Pro card between the credentials card and the next-step card, with the factual license description and the Enter activation key button, in dark desktop and light 390px mobile viewports.",
"The default preview scenario rendered the completion panel with no Activate Pulse Pro text anywhere in the document, confirming the pointer is absent for community sessions.",
"The live dev backend (Pro binary, unlicensed Community tier) served /settings/pulse-intelligence/billing/plan as the full Plans & Billing panel including the License recovery section with the Paste your license key input, confirming the pointer's destination renders the activation form rather than blanking.",
"Console showed only WebSocket sync-reconnect noise from the ad-hoc Vite proxy; no errors from the new panel code."
],
"interactions": [
"At 1440x900 generated a reporting token, opened advanced install options, enabled Pulse command execution, held the replacement API response, and confirmed the command surface locked with no old token visible before releasing the exec-scoped replacement.",
"At 390x844 forced the next replacement request to fail, confirmed the checkbox and prior token rolled back atomically, and verified the page had no document-level horizontal overflow and the error remained visible.",
"Opened Agent Doctor against a structured agent_exec_scope_missing response, expanded the critical agent row, verified the typed reason and evidence, generated its repair token, and confirmed the backend request explicitly selected command scope.",
"Verified the unlocked Agent Doctor authentication-repair command contained the fresh credential, token-file transport, and --enable-commands, then repeated the critical row inspection at 390x844 with no document-level horizontal overflow."
"Clicked Enter activation key on the pro-unlicensed preview scenario and confirmed client-side navigation landed on /settings/pulse-intelligence/billing/plan.",
"Logged into the live dev backend through the sidecar Vite proxy and loaded /settings/pulse-intelligence/billing/plan to inspect the rendered license recovery form.",
"Reloaded the preview route across dark 1440px and light 390px viewports to confirm layout and theming of the activation card."
]
}
@@ -7,20 +7,24 @@ import { getPulseBaseUrl } from '@/utils/url';
import { ActionIconButton } from '@/components/shared/Button';
import type { State } from '@/types/api';
import type { Resource } from '@/types/resource';
import type { LicenseRuntimeCapabilities, LicenseStatus } from '@/api/license';
import {
buildInfrastructureOnboardingPath,
buildInfrastructureWorkspacePath,
} from '@/components/Settings/infrastructureWorkspaceModel';
import { SELF_HOSTED_PRO_BILLING_PLAN_ROUTE } from '@/utils/pricingHandoff';
import type { WizardState } from '../SetupWizard';
import {
buildSetupCompletionConnectedSystems,
buildSetupCompletionViewModel,
shouldShowSetupProActivationPointer,
} from './setupCompletionModel';
interface CompleteStepProps {
state: WizardState;
onComplete: (nextPath?: string) => void;
connectedResourcesOverride?: readonly Resource[];
proActivationOverride?: boolean;
}
const ADD_INFRASTRUCTURE_PATH = buildInfrastructureOnboardingPath('pick');
@@ -31,10 +35,43 @@ export const SetupCompletionPanel: Component<CompleteStepProps> = (props) => {
const [copied, setCopied] = createSignal<'password' | 'admin-token' | null>(null);
let copiedTimer: ReturnType<typeof setTimeout> | undefined;
const [showCredentials, setShowCredentials] = createSignal(true);
const [showProActivation, setShowProActivation] = createSignal(false);
const [connectedSystems, setConnectedSystems] = createSignal<
ReturnType<typeof buildSetupCompletionConnectedSystems>
>([]);
createEffect(() => {
if (props.proActivationOverride !== undefined) {
// Preview scenarios force the flag so browser proof stays deterministic
// without touching live license endpoints.
setShowProActivation(props.proActivationOverride);
return;
}
void (async () => {
try {
const [capabilities, licenseStatus] = await Promise.all([
apiFetchJSON<LicenseRuntimeCapabilities>('/api/license/runtime-capabilities', {
headers: { 'X-API-Token': props.state.apiToken },
}),
apiFetchJSON<LicenseStatus>('/api/license/status', {
headers: { 'X-API-Token': props.state.apiToken },
}),
]);
setShowProActivation(
shouldShowSetupProActivationPointer({
runtimeBuild: capabilities?.runtime?.build,
licenseValid: licenseStatus?.valid,
}),
);
} catch (error) {
// A failed probe keeps the pointer hidden; community installs must
// never see it because a license endpoint errored.
logger.error('Failed to check Pro activation state:', error);
}
})();
});
createEffect(() => {
if (props.connectedResourcesOverride !== undefined) {
// Preview scenarios provide a static resource snapshot so browser proof
@@ -388,6 +425,21 @@ export const SetupCompletionPanel: Component<CompleteStepProps> = (props) => {
</Show>
</div>
<Show when={showProActivation()}>
<div class="bg-surface rounded-md border border-border p-5 sm:p-6 text-left mb-6">
<h3 class="text-sm font-semibold text-base-content">
{t('setup.completion.proActivation.title')}
</h3>
<p class="mt-1 text-xs text-muted">{t('setup.completion.proActivation.description')}</p>
<button
onClick={() => props.onComplete(SELF_HOSTED_PRO_BILLING_PLAN_ROUTE)}
class="mt-3 inline-flex items-center justify-center gap-2 rounded-md border border-border px-4 py-2.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
>
{t('setup.completion.proActivation.action')}
</button>
</div>
</Show>
<div
aria-label={t('setup.completion.nextStep.ariaLabel')}
class="bg-surface rounded-md border border-border p-5 sm:p-6 text-left mb-6"
@@ -18,6 +18,7 @@ export const SetupCompletionPreview: Component = () => {
return (
<SetupCompletionPanel
connectedResourcesOverride={previewScenario().resources}
proActivationOverride={previewScenario().proActivation}
state={previewWizardState}
onComplete={(nextPath) => {
if (nextPath) {
@@ -30,6 +30,9 @@ const migratedFirstSessionCopy = [
'Inventory and health from Proxmox, TrueNAS, VMware, PBS, or PMG.',
'Node-local telemetry for standalone hosts, services, Docker, and Kubernetes.',
'Open Add infrastructure to choose a platform API, Pulse Agent, or both.',
'Activate Pulse Pro',
'Enter the activation key from your purchase email',
'Enter activation key',
];
const migratedSources = [
@@ -196,4 +199,23 @@ describe('SetupCompletionPanel guardrails', () => {
expect(setupCompletionPanelSource).toContain('setup.completion.action.copyAdminToken');
expect(setupCompletionPanelSource).toContain('min-h-10 min-w-10');
});
it('keeps the Pro activation pointer on the direct billing route behind an explicit license gate', () => {
expect(setupCompletionPanelSource).toContain(
"import { SELF_HOSTED_PRO_BILLING_PLAN_ROUTE } from '@/utils/pricingHandoff';",
);
expect(setupCompletionPanelSource).toContain(
'props.onComplete(SELF_HOSTED_PRO_BILLING_PLAN_ROUTE)',
);
expect(setupCompletionPanelSource).toContain("'/api/license/runtime-capabilities'");
expect(setupCompletionPanelSource).toContain("'/api/license/status'");
expect(setupCompletionPanelSource).toContain('shouldShowSetupProActivationPointer');
expect(setupCompletionPanelSource).toContain('props.proActivationOverride !== undefined');
expect(setupCompletionPanelSource).toContain('setup.completion.proActivation.title');
expect(setupCompletionPanelSource).toContain('setup.completion.proActivation.description');
expect(setupCompletionPanelSource).toContain('setup.completion.proActivation.action');
expect(setupCompletionModelSource).toContain(
"state.runtimeBuild === 'pro' && state.licenseValid === false",
);
});
});
@@ -71,4 +71,24 @@ describe('SetupCompletionPreview', () => {
expect(screen.getByText('VMware vSphere')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add infrastructure' })).toBeInTheDocument();
});
it('renders the pro-unlicensed scenario with the activation pointer and no license probe', () => {
locationSearch = '?scenario=pro-unlicensed';
render(() => <SetupCompletionPreview />);
expect(apiFetchJSONMock).not.toHaveBeenCalled();
expect(screen.getByText('Activate Pulse Pro')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Enter activation key' }));
expect(navigateMock).toHaveBeenCalledWith('/settings/pulse-intelligence/billing/plan');
});
it('keeps the activation pointer out of the default preview scenario', () => {
render(() => <SetupCompletionPreview />);
expect(apiFetchJSONMock).not.toHaveBeenCalled();
expect(screen.queryByText('Activate Pulse Pro')).not.toBeInTheDocument();
});
});
@@ -0,0 +1,186 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
import { SetupCompletionPanel } from '../SetupCompletionPanel';
import { shouldShowSetupProActivationPointer } from '../setupCompletionModel';
import type { WizardState } from '../SetupWizard';
const apiFetchJSONMock = vi.fn();
vi.mock('@/utils/clipboard', () => ({
copyToClipboard: vi.fn().mockResolvedValue(true),
}));
vi.mock('@/utils/apiClient', () => ({
apiFetchJSON: (...args: unknown[]) => apiFetchJSONMock(...args),
}));
vi.mock('@/utils/url', () => ({
getPulseBaseUrl: () => 'https://pulse.example.com',
}));
vi.mock('@/utils/logger', () => ({
logger: {
error: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
debug: vi.fn(),
},
}));
const baseState: WizardState = {
username: 'admin',
password: 'password',
apiToken: 'setup-token',
};
const mockLicenseEndpoints = ({
build,
licenseValid,
}: {
build?: string;
licenseValid?: boolean;
}) => {
apiFetchJSONMock.mockImplementation((path: string) => {
if (path === '/api/license/runtime-capabilities') {
return Promise.resolve({
capabilities: [],
limits: [],
runtime: build ? { build, label: `Pulse ${build} runtime` } : undefined,
});
}
if (path === '/api/license/status') {
return Promise.resolve({
valid: licenseValid,
tier: licenseValid ? 'pro' : 'free',
is_lifetime: false,
days_remaining: 0,
features: [],
});
}
return Promise.resolve({ resources: [] });
});
};
const waitForLicenseProbe = async () => {
await waitFor(() => {
expect(apiFetchJSONMock).toHaveBeenCalledWith(
'/api/license/status',
expect.objectContaining({ headers: { 'X-API-Token': 'setup-token' } }),
);
});
};
describe('shouldShowSetupProActivationPointer', () => {
it('shows only for a Pro build with an explicitly invalid license', () => {
expect(shouldShowSetupProActivationPointer({ runtimeBuild: 'pro', licenseValid: false })).toBe(
true,
);
expect(shouldShowSetupProActivationPointer({ runtimeBuild: 'pro', licenseValid: true })).toBe(
false,
);
expect(
shouldShowSetupProActivationPointer({ runtimeBuild: 'community', licenseValid: false }),
).toBe(false);
expect(
shouldShowSetupProActivationPointer({ runtimeBuild: undefined, licenseValid: false }),
).toBe(false);
expect(
shouldShowSetupProActivationPointer({ runtimeBuild: 'pro', licenseValid: undefined }),
).toBe(false);
});
});
describe('SetupCompletionPanel Pro activation pointer', () => {
beforeEach(() => {
vi.clearAllMocks();
apiFetchJSONMock.mockResolvedValue({ resources: [] });
});
afterEach(() => {
cleanup();
});
it('renders the pointer from the preview override without touching license endpoints', () => {
const onComplete = vi.fn();
render(() => (
<SetupCompletionPanel
state={baseState}
onComplete={onComplete}
connectedResourcesOverride={[]}
proActivationOverride={true}
/>
));
expect(apiFetchJSONMock).not.toHaveBeenCalled();
expect(screen.getByText('Activate Pulse Pro')).toBeInTheDocument();
expect(
screen.getByText(
'This server is running the Pulse Pro build without an active license. Enter the activation key from your purchase email to unlock Pro features.',
),
).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Enter activation key' }));
expect(onComplete).toHaveBeenCalledWith('/settings/pulse-intelligence/billing/plan');
});
it('hides the pointer when the preview override is false', () => {
render(() => (
<SetupCompletionPanel
state={baseState}
onComplete={vi.fn()}
connectedResourcesOverride={[]}
proActivationOverride={false}
/>
));
expect(apiFetchJSONMock).not.toHaveBeenCalled();
expect(screen.queryByText('Activate Pulse Pro')).not.toBeInTheDocument();
});
it('shows the pointer when a Pro build reports no valid license', async () => {
mockLicenseEndpoints({ build: 'pro', licenseValid: false });
render(() => <SetupCompletionPanel state={baseState} onComplete={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText('Activate Pulse Pro')).toBeInTheDocument();
});
expect(apiFetchJSONMock).toHaveBeenCalledWith(
'/api/license/runtime-capabilities',
expect.objectContaining({ headers: { 'X-API-Token': 'setup-token' } }),
);
});
it('keeps the pointer hidden on a community build', async () => {
mockLicenseEndpoints({ build: 'community', licenseValid: false });
render(() => <SetupCompletionPanel state={baseState} onComplete={vi.fn()} />);
await waitForLicenseProbe();
expect(screen.queryByText('Activate Pulse Pro')).not.toBeInTheDocument();
});
it('keeps the pointer hidden on a Pro build with an active license', async () => {
mockLicenseEndpoints({ build: 'pro', licenseValid: true });
render(() => <SetupCompletionPanel state={baseState} onComplete={vi.fn()} />);
await waitForLicenseProbe();
expect(screen.queryByText('Activate Pulse Pro')).not.toBeInTheDocument();
});
it('keeps the pointer hidden when the license probe fails', async () => {
apiFetchJSONMock.mockImplementation((path: string) => {
if (path === '/api/license/runtime-capabilities' || path === '/api/license/status') {
return Promise.reject(new Error('probe failed'));
}
return Promise.resolve({ resources: [] });
});
render(() => <SetupCompletionPanel state={baseState} onComplete={vi.fn()} />);
await waitForLicenseProbe();
expect(screen.queryByText('Activate Pulse Pro')).not.toBeInTheDocument();
});
});
@@ -250,3 +250,20 @@ export function buildSetupCompletionViewModel(
showAgentInstallAction: false,
};
}
export interface SetupCompletionProActivationState {
runtimeBuild?: string;
licenseValid?: boolean;
}
// The Pro build is only distributed to paying customers, and its Plans &
// Billing navigation entry stays hidden until a license activates
// (hideWhenUpgradeHidden), so the completion screen is the one guaranteed
// in-product surface that can point a fresh Pro install at the activation
// form. licenseValid must be explicitly false: a failed or missing probe keeps
// the pointer hidden so community sessions can never see it by accident.
export function shouldShowSetupProActivationPointer(
state: SetupCompletionProActivationState,
): boolean {
return state.runtimeBuild === 'pro' && state.licenseValid === false;
}
@@ -2,11 +2,14 @@ import type { Resource } from '@/types/resource';
const PREVIEW_TIMESTAMP_MS = Date.UTC(2026, 3, 10, 12, 0, 0, 0);
export type SetupCompletionPreviewScenarioId = 'empty' | 'vmware-api-backed';
export type SetupCompletionPreviewScenarioId = 'empty' | 'vmware-api-backed' | 'pro-unlicensed';
export interface SetupCompletionPreviewScenario {
id: SetupCompletionPreviewScenarioId;
resources: readonly Resource[];
// Always explicit so the preview never falls through to the live license
// probe: the panel treats undefined as "fetch", and preview must not fetch.
proActivation: boolean;
}
const SETUP_COMPLETION_PREVIEW_SCENARIOS: Record<
@@ -16,6 +19,7 @@ const SETUP_COMPLETION_PREVIEW_SCENARIOS: Record<
empty: {
id: 'empty',
resources: [],
proActivation: false,
},
'vmware-api-backed': {
id: 'vmware-api-backed',
@@ -35,6 +39,12 @@ const SETUP_COMPLETION_PREVIEW_SCENARIOS: Record<
},
},
],
proActivation: false,
},
'pro-unlicensed': {
id: 'pro-unlicensed',
resources: [],
proActivation: true,
},
};
@@ -45,7 +55,7 @@ export const getSetupCompletionPreviewScenario = (
search: string | null | undefined,
): SetupCompletionPreviewScenario => {
const scenario = new URLSearchParams(search || '').get('scenario');
if (scenario === 'vmware-api-backed') {
if (scenario === 'vmware-api-backed' || scenario === 'pro-unlicensed') {
return SETUP_COMPLETION_PREVIEW_SCENARIOS[scenario];
}
return SETUP_COMPLETION_PREVIEW_SCENARIOS[DEFAULT_SETUP_COMPLETION_PREVIEW_SCENARIO_ID];
@@ -242,6 +242,22 @@ describe('i18n foundation', () => {
}
});
it('keeps the setup completion Pro activation pointer in the localized first-session journey', () => {
expect(LOCALIZED_FIRST_SESSION_MONITORING_JOURNEY_KEYS).toEqual(
expect.arrayContaining([
'setup.completion.proActivation.action',
'setup.completion.proActivation.description',
'setup.completion.proActivation.title',
]),
);
for (const locale of FIRST_LOCALIZATION_LOCALES) {
expect(I18N_MESSAGES[locale]['setup.completion.proActivation.title']).toContain('Pulse Pro');
expect(I18N_MESSAGES[locale]['setup.completion.proActivation.description']).toContain(
'Pulse Pro',
);
}
});
it('keeps machine-facing identifiers unchanged in first-session monitoring catalog copy', () => {
for (const locale of FIRST_LOCALIZATION_LOCALES) {
const telemetryNotice = I18N_MESSAGES[locale]['setup.welcome.telemetryNotice.description'];
+4
View File
@@ -197,6 +197,10 @@ export const DE_MESSAGE_OVERRIDES = {
'Oeffnen Sie Infrastruktur hinzufuegen, um eine Plattform-API, Pulse Agent oder beides zu waehlen.',
'setup.completion.nextStep.title.connected': 'Infrastruktur oeffnen',
'setup.completion.nextStep.title.empty': 'Strategie fuer erste Quelle waehlen',
'setup.completion.proActivation.action': 'Aktivierungsschluessel eingeben',
'setup.completion.proActivation.description':
'Dieser Server laeuft mit dem Pulse Pro Build, aber ohne aktive Lizenz. Geben Sie den Aktivierungsschluessel aus Ihrer Kauf-E-Mail ein, um die Pro-Funktionen freizuschalten.',
'setup.completion.proActivation.title': 'Pulse Pro aktivieren',
'setup.completion.resource.unknownName': 'Unbekannt',
'setup.completion.sourceOptions.agent.description':
'Knotenlokale Telemetrie fuer eigenstaendige Hosts, Dienste, Docker und Kubernetes.',
+4
View File
@@ -191,6 +191,10 @@ export const ES_MESSAGE_OVERRIDES = {
'Abre Agregar infraestructura para elegir una API de plataforma, Pulse Agent o ambos.',
'setup.completion.nextStep.title.connected': 'Abrir Infraestructura',
'setup.completion.nextStep.title.empty': 'Elige la estrategia de primera fuente',
'setup.completion.proActivation.action': 'Introducir clave de activación',
'setup.completion.proActivation.description':
'Este servidor ejecuta la compilación Pulse Pro sin una licencia activa. Introduce la clave de activación de tu correo de compra para desbloquear las funciones Pro.',
'setup.completion.proActivation.title': 'Activar Pulse Pro',
'setup.completion.resource.unknownName': 'Desconocido',
'setup.completion.sourceOptions.agent.description':
'Telemetría local del nodo para hosts independientes, servicios, Docker y Kubernetes.',
+7
View File
@@ -56,6 +56,10 @@ export const EN_MESSAGES = {
'Open Add infrastructure to choose a platform API, Pulse Agent, or both.',
'setup.completion.nextStep.title.connected': 'Open Infrastructure',
'setup.completion.nextStep.title.empty': 'Choose the first source strategy',
'setup.completion.proActivation.action': 'Enter activation key',
'setup.completion.proActivation.description':
'This server is running the Pulse Pro build without an active license. Enter the activation key from your purchase email to unlock Pro features.',
'setup.completion.proActivation.title': 'Activate Pulse Pro',
'setup.completion.resource.unknownName': 'Unknown',
'setup.completion.sourceOptions.agent.description':
'Node-local telemetry for standalone hosts, services, Docker, and Kubernetes.',
@@ -591,6 +595,9 @@ export const FIRST_SESSION_MONITORING_MIGRATED_MESSAGE_KEYS = [
'setup.completion.nextStep.summary.empty',
'setup.completion.nextStep.title.connected',
'setup.completion.nextStep.title.empty',
'setup.completion.proActivation.action',
'setup.completion.proActivation.description',
'setup.completion.proActivation.title',
'setup.completion.resource.unknownName',
'setup.completion.sourceOptions.agent.description',
'setup.completion.sourceOptions.agent.title',