diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 082505634..7d3c3f67c 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -1317,6 +1317,10 @@ The v6 welcome surface is one guided spotlight tour, not a modal plus a second dashboard-only migration hint: it must dim the live app, glow the real primary-navigation target being described, and keep route-orientation copy on the existing welcome flow instead of layering a duplicate in-product banner. +Its primary job is rapid v5-to-v6 reorientation. The modal should explain +where familiar v5 destinations moved and what each v6 top-level area is for, +so operators who feel briefly lost after upgrading can rebuild their mental +map in one pass. The guided stop map inside that welcome surface is interactive, not decorative: operators must be able to jump directly to any tour step from the stop list, and desktop layouts may widen the panel enough to keep step labels readable @@ -1329,6 +1333,9 @@ language. The shell, step map, telemetry note, and supporting actions should use bordered flat fills and normal app radii instead of gradient washes, glassmorphism, or other marketing-style promo chrome that drifts from the rest of the product. +Secondary disclosures such as telemetry must stay subordinate to that +orientation job: keep them compact, link to the canonical privacy/settings +surfaces, and do not let them crowd out the migration wayfinding copy. That state owner now also owns public-demo suppression: the modal must stay closed until `sessionPresentationPolicyResolved()` is true and must fail closed when `presentationPolicyIsDemoMode()` resolves true, so the public demo does diff --git a/frontend-modern/src/components/shared/WhatsNewModal.tsx b/frontend-modern/src/components/shared/WhatsNewModal.tsx index f7a377bcf..9f04823b3 100644 --- a/frontend-modern/src/components/shared/WhatsNewModal.tsx +++ b/frontend-modern/src/components/shared/WhatsNewModal.tsx @@ -11,17 +11,22 @@ import XIcon from 'lucide-solid/icons/x'; import { WHATS_NEW_BACK_LABEL, WHATS_NEW_CLOSE_LABEL, + WHATS_NEW_CURRENT_STEP_LABEL, WHATS_NEW_DOCS_LABEL, WHATS_NEW_DOCS_URL, WHATS_NEW_DO_NOT_SHOW_LABEL, WHATS_NEW_FEATURE_CARDS, + WHATS_NEW_KICKER_LABEL, WHATS_NEW_NEXT_LABEL, WHATS_NEW_PRIMARY_ACTION_LABEL, WHATS_NEW_PRIVACY_URL, WHATS_NEW_SKIP_LABEL, + WHATS_NEW_STEP_MAP_HELPER, + WHATS_NEW_STEP_MAP_LABEL, WHATS_NEW_SUBTITLE, WHATS_NEW_TELEMETRY_COPY, WHATS_NEW_TELEMETRY_ENV_VAR, + WHATS_NEW_TELEMETRY_LABEL, WHATS_NEW_TELEMETRY_PRIVACY_LABEL, WHATS_NEW_TELEMETRY_SETTINGS_PATH, WHATS_NEW_TELEMETRY_TITLE, @@ -90,7 +95,7 @@ export function WhatsNewModal() {
- Guided Welcome Tour + {WHATS_NEW_KICKER_LABEL}
Step {state.stepIndex() + 1} of {state.stepCount()} @@ -118,7 +123,7 @@ export function WhatsNewModal() {
- Now showing + {WHATS_NEW_CURRENT_STEP_LABEL}
{step().title}

{step().description}

@@ -129,11 +134,11 @@ export function WhatsNewModal() {
- In This Tour + {WHATS_NEW_STEP_MAP_LABEL}
-

Jump ahead or follow the highlighted path.

+

{WHATS_NEW_STEP_MAP_HELPER}

{(card, index) => ( @@ -165,40 +170,42 @@ export function WhatsNewModal() {
-
-
-
+
+
+
-
-
- Privacy note +
+
+ {WHATS_NEW_TELEMETRY_LABEL}
-
+
{WHATS_NEW_TELEMETRY_TITLE}
-

- {WHATS_NEW_TELEMETRY_COPY[0]} +

+ {WHATS_NEW_TELEMETRY_COPY[0]}{' '} + + {WHATS_NEW_TELEMETRY_PRIVACY_LABEL} +

+

+ {WHATS_NEW_TELEMETRY_COPY[1]} +

+
+ + {WHATS_NEW_TELEMETRY_SETTINGS_PATH} + + + {WHATS_NEW_TELEMETRY_ENV_VAR} + +
-

- {WHATS_NEW_TELEMETRY_COPY[1]} You can disable it any time in{' '} - {WHATS_NEW_TELEMETRY_SETTINGS_PATH} or by - setting{' '} - - {WHATS_NEW_TELEMETRY_ENV_VAR} - - .{' '} - - {WHATS_NEW_TELEMETRY_PRIVACY_LABEL} - -

diff --git a/frontend-modern/src/components/shared/__tests__/WhatsNewModal.test.tsx b/frontend-modern/src/components/shared/__tests__/WhatsNewModal.test.tsx index 12d0eeb62..0d7addc1d 100644 --- a/frontend-modern/src/components/shared/__tests__/WhatsNewModal.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/WhatsNewModal.test.tsx @@ -56,9 +56,11 @@ describe('WhatsNewModal', () => { expect(whatsNewModalModelSource).toContain('WHATS_NEW_PRIVACY_URL'); expect(whatsNewModalModelSource).toContain('MIGRATION_GUIDE_DOC_URL'); expect(whatsNewModalModelSource).toContain('PRIVACY_DOC_URL'); - expect(whatsNewModalModelSource).toContain('rotating install ID'); - expect(whatsNewModalModelSource).toContain('normalized release identity'); - expect(whatsNewModalModelSource).toContain('IP addresses are not stored in telemetry rows'); + expect(whatsNewModalModelSource).toContain('anonymous daily ping'); + expect(whatsNewModalModelSource).toContain('nothing is gone'); + expect(whatsNewModalModelSource).toContain('WHATS_NEW_KICKER_LABEL'); + expect(whatsNewModalModelSource).toContain('WHATS_NEW_STEP_MAP_LABEL'); + expect(whatsNewModalModelSource).toContain('WHATS_NEW_TELEMETRY_LABEL'); 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'); @@ -71,8 +73,9 @@ describe('WhatsNewModal', () => { const dialog = await screen.findByRole('dialog'); expect(dialog).toBeInTheDocument(); expect(within(dialog).getByText('Welcome to Pulse v6')).toBeInTheDocument(); + expect(within(dialog).getByText(/nothing is gone/i)).toBeInTheDocument(); expect(within(dialog).getByText('Step 1 of 5')).toBeInTheDocument(); - expect(within(dialog).getByText('In This Tour')).toBeInTheDocument(); + expect(within(dialog).getByText('Where Things Moved')).toBeInTheDocument(); expect(within(dialog).queryByText(/Stop 1/i)).not.toBeInTheDocument(); expect(within(dialog).getAllByText('Dashboard')).toHaveLength(2); }); @@ -127,26 +130,26 @@ describe('WhatsNewModal', () => { it('advances through the guided tour and finishes on the last step', async () => { render(() => ); - expect(await screen.findByText(/problem-focused summary/i)).toBeInTheDocument(); + expect(await screen.findByText(/overall picture: health, alerts, capacity/i)).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Next' })); - expect(await screen.findByText(/platform roots live here/i)).toBeInTheDocument(); + expect(await screen.findByText(/systems themselves: Proxmox nodes, Docker hosts/i)).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Next' })); - expect(await screen.findByText(/docker update status now share/i)).toBeInTheDocument(); + expect(await screen.findByText(/guests or Docker workloads in v5/i)).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Next' })); - expect(await screen.findByText(/Storage is now a top-level destination/i)).toBeInTheDocument(); + expect(await screen.findByText(/Datastores, pools, disks, and capacity moved here/i)).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Next' })); - expect(await screen.findByText(/Recovery events \(backups, snapshots, and replication\)/i)).toBeInTheDocument(); + expect(await screen.findByText(/Backups, snapshots, and replication moved here/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: "Let's go" })).toBeInTheDocument(); }); it('lets the user jump to a tour stop directly from the stop map', async () => { render(() => ); - expect(await screen.findByText(/problem-focused summary/i)).toBeInTheDocument(); + expect(await screen.findByText(/overall picture: health, alerts, capacity/i)).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /Workloads/i })); - expect(await screen.findByText(/docker update status now share/i)).toBeInTheDocument(); + expect(await screen.findByText(/guests or Docker workloads in v5/i)).toBeInTheDocument(); expect(screen.getByText('Step 3 of 5')).toBeInTheDocument(); }); diff --git a/frontend-modern/src/components/shared/whatsNewModalModel.ts b/frontend-modern/src/components/shared/whatsNewModalModel.ts index 4f681e643..d658cb5bb 100644 --- a/frontend-modern/src/components/shared/whatsNewModalModel.ts +++ b/frontend-modern/src/components/shared/whatsNewModalModel.ts @@ -15,7 +15,7 @@ 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 a problem-focused summary. This is the landing page now, not the old Proxmox overview.', + 'Start here first when you want the overall picture: health, alerts, capacity, and recent activity across your estate.', icon: 'dashboard', target: 'dashboard', title: 'Dashboard', @@ -23,7 +23,7 @@ export const WHATS_NEW_FEATURE_CARDS: WhatsNewFeatureCard[] = [ { accent: 'border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900', description: - 'Platform roots live here: Proxmox nodes, Docker hosts, Kubernetes clusters, PBS, PMG, TrueNAS, and more.', + 'Use this when you want the systems themselves: Proxmox nodes, Docker hosts, Kubernetes clusters, PBS, PMG, TrueNAS, and other platform roots.', icon: 'infrastructure', target: 'infrastructure', title: 'Infrastructure', @@ -31,14 +31,15 @@ export const WHATS_NEW_FEATURE_CARDS: WhatsNewFeatureCard[] = [ { accent: 'border-purple-200 bg-purple-50 dark:border-purple-800 dark:bg-purple-900', description: - 'VMs, containers, pods, and Docker update status now share one unified workloads surface.', + 'VMs, containers, and pods live here now. If you used to drill into guests or Docker workloads in v5, this is the new starting point.', icon: 'workloads', target: 'workloads', title: 'Workloads', }, { accent: 'border-emerald-200 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-900', - description: 'Storage is now a top-level destination across all systems.', + description: + 'Datastores, pools, disks, and capacity moved here so storage is one destination across platforms.', icon: 'storage', target: 'storage', title: 'Storage', @@ -46,20 +47,25 @@ export const WHATS_NEW_FEATURE_CARDS: WhatsNewFeatureCard[] = [ { accent: 'border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-900', description: - 'Recovery events (backups, snapshots, and replication) are now first-class pages.', + 'Backups, snapshots, and replication moved here. Open this when you want restore posture or recent recovery activity.', icon: 'recovery', target: 'recovery', title: 'Recovery', }, ]; +export const WHATS_NEW_KICKER_LABEL = 'V5 to V6 Guide'; +export const WHATS_NEW_CURRENT_STEP_LABEL = 'Now showing'; +export const WHATS_NEW_STEP_MAP_LABEL = 'Where Things Moved'; +export const WHATS_NEW_STEP_MAP_HELPER = 'Jump ahead or follow the highlighted path.'; export const WHATS_NEW_TITLE = 'Welcome to Pulse v6'; export const WHATS_NEW_SUBTITLE = - 'Everything is now organized by what you want to do, not where the data comes from.'; -export const WHATS_NEW_TELEMETRY_TITLE = 'Anonymous outbound telemetry'; + "If you're coming from v5, nothing is gone. Pulse is now grouped by task so you can find things faster."; +export const WHATS_NEW_TELEMETRY_LABEL = 'Telemetry note'; +export const WHATS_NEW_TELEMETRY_TITLE = 'Anonymous telemetry'; export const WHATS_NEW_TELEMETRY_COPY = [ - 'Pulse now sends a lightweight anonymous ping once a day — just a rotating install ID, normalized release identity, platform, resource counts, and feature flags. No hostnames, credentials, or personal information are sent, and IP addresses are not stored in telemetry rows.', - 'This helps the developer understand how Pulse is used and prioritise what to build next.', + 'Pulse also sends a lightweight anonymous daily ping. No hostnames, credentials, or personal information are sent.', + 'You can turn it off any time in Settings → System → General or with PULSE_TELEMETRY=false.', ]; export const WHATS_NEW_TELEMETRY_SETTINGS_PATH = 'Settings → System → General'; export const WHATS_NEW_TELEMETRY_ENV_VAR = 'PULSE_TELEMETRY=false'; diff --git a/tests/integration/tests/19-telemetry-disclosure.spec.ts b/tests/integration/tests/19-telemetry-disclosure.spec.ts index 46e2d1459..bf902a459 100644 --- a/tests/integration/tests/19-telemetry-disclosure.spec.ts +++ b/tests/integration/tests/19-telemetry-disclosure.spec.ts @@ -153,18 +153,15 @@ test.describe('Telemetry disclosure', () => { ); await expect(dialog.getByText('Welcome to Pulse v6')).toBeVisible(); await expect(dialog.getByText('Step 1 of 5')).toBeVisible(); + await expect(dialog.getByText(/nothing is gone/i)).toBeVisible(); await expect(assistantLauncher).toBeHidden(); await expect(spotlight).toHaveAttribute('data-tour-step', 'dashboard'); await expect(dialog).toHaveAttribute('data-tour-step', 'dashboard'); await expectSpotlightAround(spotlight, dashboardTab); - await expect( - dialog.getByText( - /rotating install ID, normalized release identity, platform, resource counts, and feature flags/i, - ), - ).toBeVisible(); - await expect( - dialog.getByText(/IP addresses are not stored in telemetry rows/i), - ).toBeVisible(); + await expect(dialog.getByText('Where Things Moved')).toBeVisible(); + await expect(dialog.getByText('Telemetry note')).toBeVisible(); + await expect(dialog.getByText(/lightweight anonymous daily ping/i)).toBeVisible(); + await expect(dialog.getByText('Settings → System → General', { exact: true })).toBeVisible(); const privacyLink = dialog.getByRole('link', { name: 'Full details' }); await expect(privacyLink).toHaveAttribute('href', '/docs/PRIVACY.md');