From 2f8e5184bd7beaf5f87f466aa6c35a4b48fe8593 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 6 May 2026 09:49:15 +0100 Subject: [PATCH] Remove navigation guide modal and reopen control The four-step coachmark over the top tabs was a tour pretending to be guidance: each step duplicated the tab title in one sentence, and the Reopen control on /settings/system-general spawned a centered panel with no spotlight target because the tabs only exist on dashboard routes. Delete the modal, the localStorage dismissal key, the reopen event, the Reopen row in General settings, and the matching guardrails so the shared-primitives tests stop pinning the deleted owner split. Drop the WhatsNew dismissal helpers and addInitScript bypasses from the integration suite, and the dedicated tour test in 19-telemetry-disclosure. --- .../subsystems/frontend-primitives.md | 59 +--- .../v6/internal/subsystems/registry.json | 1 - .../internal/subsystems/security-privacy.md | 9 +- frontend-modern/src/App.tsx | 2 - .../Settings/GeneralSettingsPanel.tsx | 27 -- .../SharedPrimitives.guardrails.test.ts | 36 --- .../src/components/shared/WhatsNewModal.tsx | 195 -------------- .../shared/__tests__/WhatsNewModal.test.tsx | 185 ------------- .../shared/useWhatsNewModalState.ts | 254 ------------------ .../components/shared/whatsNewModalModel.ts | 57 ---- .../frontendResourceTypeBoundaries.test.ts | 27 -- frontend-modern/src/utils/localStorage.ts | 1 - tests/integration/tests/04-mobile.spec.ts | 9 +- .../integration/tests/06-theme-visual.spec.ts | 1 - .../tests/19-telemetry-disclosure.spec.ts | 83 ------ .../tests/45-workloads-memory-tail.spec.ts | 10 - .../46-storage-summary-continuity.spec.ts | 3 - .../53-demo-mode-commercial-boundary.spec.ts | 7 - .../54-monitored-system-billing-focus.spec.ts | 4 - .../55-self-hosted-upgrade-return.spec.ts | 4 - .../tests/57-release-candidate-shell.spec.ts | 4 - .../tests/59-workloads-column-layout.spec.ts | 3 - .../61-diagnostics-commercial-funnel.spec.ts | 1 - .../61-infrastructure-column-layout.spec.ts | 1 - ...2-runtime-home-onboarding-contract.spec.ts | 6 - .../tests/62-storage-growth-column.spec.ts | 4 - ...orkloads-proxmox-refresh-stability.spec.ts | 3 - ...65-offline-proxmox-node-visibility.spec.ts | 4 - ...6-organization-sharing-approval-ui.spec.ts | 1 - .../68-infrastructure-onboarding.spec.ts | 4 - tests/integration/tests/helpers.ts | 20 -- 31 files changed, 9 insertions(+), 1016 deletions(-) delete mode 100644 frontend-modern/src/components/shared/WhatsNewModal.tsx delete mode 100644 frontend-modern/src/components/shared/__tests__/WhatsNewModal.test.tsx delete mode 100644 frontend-modern/src/components/shared/useWhatsNewModalState.ts delete mode 100644 frontend-modern/src/components/shared/whatsNewModalModel.ts diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 990667005..91f80304f 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -1062,7 +1062,6 @@ and existing alert-history shells instead of introducing VMware-only labels, badges, or panel copy just because the underlying signal came from vSphere. That same shared settings and modal boundary now also owns the public usage-data vocabulary. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx`, -`frontend-modern/src/components/shared/whatsNewModalModel.ts`, `frontend-modern/src/components/Settings/useSystemSettingsState.ts`, and `frontend-modern/src/utils/systemSettingsPresentation.ts` must present one explicit `Usage data and privacy` model centered on `Anonymous outbound @@ -1725,60 +1724,10 @@ runtime, and `frontend-modern/src/components/shared/searchTipsPopoverModel.ts` owns trigger variant, label/id defaults, hover policy, and trigger/popover class selection. Future search-tips work should extend those owners instead of pushing listener lifecycle or trigger policy back into the shared shell. -The shared what's-new modal now follows that same owner split. -`frontend-modern/src/components/shared/WhatsNewModal.tsx` stays the render -shell, `frontend-modern/src/components/shared/useWhatsNewModalState.ts` owns -local-storage dismissal, session dismissal, step progression, spotlight target -resolution, direct stop selection, and overlay placement/runtime behavior, and -`frontend-modern/src/components/shared/whatsNewModalModel.ts` owns the feature -tour catalog, telemetry copy, labels, and canonical docs/privacy links. Future -what's-new work should extend those owners instead of pushing dismissal state, -spotlight runtime, product copy, or external links back into the shared shell. -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 fast route orientation. The modal should explain what each -top-level area is for in plain product language, so operators can understand -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 -controls. It must not grow back into a large sectioned explainer when one -sentence would do the job. -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 -without overlapping or collapsing into clipped pills. That map should read as -numbered wayfinding, not placeholder onboarding chrome: concise numeric badges -plus section titles are preferred over repeated `Stop N` copy or other filler -labels that add noise without helping orientation. -That same welcome surface must stay inside Pulse's existing flat visual -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 as footer-level links into the canonical -privacy/settings surfaces, and do not let them crowd out the migration -wayfinding copy. The supporting docs CTA on that surface should likewise stay -route-oriented: use a neutral `Navigation guide` label and plain present-tense -copy that helps operators understand the current IA, rather than reviving -`Migration guide` branding that pulls the tour back into v5 historical framing. -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 -not front-load product migration onboarding ahead of the actual surface. -Canonical customer disclosures inside those shared shells now route through -`frontend-modern/src/utils/docsLinks.ts`, so settings and what's-new privacy -links resolve to shipped `/docs/...` assets instead of hard-coded GitHub -`main` URLs that can drift from the running build. +Canonical customer disclosures inside shared shells route through +`frontend-modern/src/utils/docsLinks.ts`, so settings privacy links resolve to +shipped `/docs/...` assets instead of hard-coded GitHub `main` URLs that can +drift from the running build. The shared summary strip primitives now follow that same owner split. `frontend-modern/src/components/shared/SummaryPanel.tsx` and `frontend-modern/src/components/shared/SummaryMetricCard.tsx` stay the render diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 3b38a4ee0..330b3f390 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -3553,7 +3553,6 @@ "frontend-modern/src/components/shared/__tests__/TagBadges.test.tsx", "frontend-modern/src/components/shared/__tests__/UpgradeLink.test.tsx", "frontend-modern/src/components/shared/__tests__/WebInterfaceUrlField.test.tsx", - "frontend-modern/src/components/shared/__tests__/WhatsNewModal.test.tsx", "frontend-modern/src/components/shared/ColumnPicker.test.tsx", "frontend-modern/src/components/shared/FilterToolbar.test.tsx", "frontend-modern/src/components/shared/PageControls.guardrails.test.ts", diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index 6c16a4a46..de84f5da7 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -315,11 +315,10 @@ abuse controls, `docs/PRIVACY.md` and the shipped `frontend-modern/public/docs/PRIVACY.md` copy must say so explicitly rather than implying the server stores nothing at all. That same rule also applies to the short in-product summary on the shared -General settings privacy surface and the whats-new disclosure copy. Those -surfaces may stay concise, but they must not claim a stronger privacy posture -than the governed docs; if telemetry rows are retained for a fixed window and -IP addresses are not stored rather than “never seen,” the summary copy must -say that plainly. +General settings privacy surface. That surface may stay concise, but it must +not claim a stronger privacy posture than the governed docs; if telemetry rows +are retained for a fixed window and IP addresses are not stored rather than +“never seen,” the summary copy must say that plainly. That same shared trust boundary now also owns the TLS floor used by pinned- fingerprint runtime clients. `pkg/tlsutil/fingerprint.go` may support certificate-fingerprint capture and verification for self-signed deployments, diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx index 75636e48f..f13f099a8 100644 --- a/frontend-modern/src/App.tsx +++ b/frontend-modern/src/App.tsx @@ -9,7 +9,6 @@ import { logger } from './utils/logger'; import { UpdateBanner } from './components/UpdateBanner'; import { DemoBanner } from './components/DemoBanner'; import { GitHubStarBanner } from './components/GitHubStarBanner'; -import { WhatsNewModal } from './components/shared/WhatsNewModal'; import { KeyboardShortcutsModal } from './components/shared/KeyboardShortcutsModal'; import { CommandPaletteModal } from './components/shared/CommandPaletteModal'; import { dialogStackHasBlockingDialog } from './components/shared/useDialogState'; @@ -379,7 +378,6 @@ function App() { - {/* Main layout container - flexbox to allow AI panel to push content */} diff --git a/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx b/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx index d1ddcec7f..9582b8eb8 100644 --- a/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx +++ b/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx @@ -9,10 +9,8 @@ import Sun from 'lucide-solid/icons/sun'; import Moon from 'lucide-solid/icons/moon'; import Thermometer from 'lucide-solid/icons/thermometer'; import Maximize2 from 'lucide-solid/icons/maximize-2'; -import Compass from 'lucide-solid/icons/compass'; import { temperatureStore } from '@/utils/temperature'; import { layoutStore } from '@/utils/layout'; -import { WHATS_NEW_REOPEN_EVENT } from '@/components/shared/whatsNewModalModel'; import { PVE_POLLING_MAX_SECONDS, PVE_POLLING_MIN_SECONDS, @@ -167,31 +165,6 @@ export const GeneralSettingsPanel: Component = (props onChange={() => layoutStore.toggle()} /> - - {/* Reopen Navigation Guide */} -
-
-
- -
-
-

Navigation guide

-

- Replay the four-stop walkthrough of Infrastructure, Workloads, Storage, and - Recovery. -

-
-
- -
{/* Usage Data + Privacy Card */} diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 4dc5f08ab..a5f111e44 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -36,8 +36,6 @@ import infrastructureSelectorSource from '@/components/shared/InfrastructureSele import pulseDataGridSource from '@/components/shared/PulseDataGrid.tsx?raw'; import pulseDataGridModelSource from '@/components/shared/pulseDataGridModel.ts?raw'; import progressBarSource from '@/components/shared/ProgressBar.tsx?raw'; -import whatsNewModalSource from '@/components/shared/WhatsNewModal.tsx?raw'; -import whatsNewModalModelSource from '@/components/shared/whatsNewModalModel.ts?raw'; import searchFieldSource from '@/components/shared/SearchField.tsx?raw'; import searchFieldModelSource from '@/components/shared/searchFieldModel.ts?raw'; import searchInputSource from '@/components/shared/SearchInput.tsx?raw'; @@ -102,7 +100,6 @@ import infrastructureDetailsDrawerStateSource from '@/components/shared/useInfra import mobileNavBarStateSource from '@/components/shared/useMobileNavBarState.ts?raw'; import infrastructureSelectorStateSource from '@/components/shared/useInfrastructureSelectorState.ts?raw'; import pulseDataGridStateSource from '@/components/shared/usePulseDataGridState.ts?raw'; -import whatsNewModalStateSource from '@/components/shared/useWhatsNewModalState.ts?raw'; import searchFieldStateSource from '@/components/shared/useSearchFieldState.ts?raw'; import searchInputStateSource from '@/components/shared/useSearchInputState.ts?raw'; import searchInputEnhancementsStateSource from '@/components/shared/useSearchInputEnhancements.ts?raw'; @@ -1309,39 +1306,6 @@ describe('shared primitive guardrails', () => { expect(searchTipsPopoverModelSource).toContain('shouldSearchTipsPopoverOpenOnHover'); }); - it('keeps whats new modal on shell, runtime, and model owners', () => { - expect(whatsNewModalSource).toContain('useWhatsNewModalState'); - expect(whatsNewModalSource).toContain('useDialogState'); - expect(whatsNewModalSource).toContain('WHATS_NEW_FEATURE_CARDS'); - expect(whatsNewModalSource).toContain('Portal'); - expect(whatsNewModalSource).not.toContain('createLocalStorageBooleanSignal'); - 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(whatsNewModalStateSource).toContain('export function useWhatsNewModalState'); - expect(whatsNewModalStateSource).toContain('createLocalStorageBooleanSignal'); - expect(whatsNewModalStateSource).toContain('createSignal'); - expect(whatsNewModalStateSource).toContain('createMemo'); - expect(whatsNewModalStateSource).toContain('STORAGE_KEYS.WHATS_NEW_NAV_V2_SHOWN'); - expect(whatsNewModalStateSource).toContain('sessionPresentationPolicyResolved'); - expect(whatsNewModalStateSource).toContain('presentationPolicyIsDemoMode'); - expect(whatsNewModalStateSource).toContain('handleClose'); - expect(whatsNewModalStateSource).toContain('handleNext'); - expect(whatsNewModalStateSource).toContain('spotlightStyle'); - - expect(whatsNewModalModelSource).toContain('WHATS_NEW_FEATURE_CARDS'); - expect(whatsNewModalModelSource).toContain('WHATS_NEW_DOCS_URL'); - expect(whatsNewModalModelSource).toContain('WHATS_NEW_PRIVACY_URL'); - expect(whatsNewModalModelSource).toContain('WHATS_NEW_DOCS_LABEL'); - expect(whatsNewModalModelSource).toContain('MIGRATION_GUIDE_DOC_URL'); - expect(whatsNewModalModelSource).toContain('Telemetry details'); - expect(whatsNewModalModelSource).toContain("title: 'Infrastructure'"); - }); - it('keeps dialog stack visibility in the shared dialog runtime', () => { expect(dialogStateSource).toContain('export function dialogStackHasBlockingDialog'); expect(dialogStateSource).toContain('createSignal'); diff --git a/frontend-modern/src/components/shared/WhatsNewModal.tsx b/frontend-modern/src/components/shared/WhatsNewModal.tsx deleted file mode 100644 index fc9fa0f80..000000000 --- a/frontend-modern/src/components/shared/WhatsNewModal.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { For, Show } from 'solid-js'; -import { Portal } from 'solid-js/web'; -import ServerIcon from 'lucide-solid/icons/server'; -import BoxesIcon from 'lucide-solid/icons/boxes'; -import HardDriveIcon from 'lucide-solid/icons/hard-drive'; -import ShieldCheckIcon from 'lucide-solid/icons/shield-check'; -import XIcon from 'lucide-solid/icons/x'; -import { - WHATS_NEW_BACK_LABEL, - WHATS_NEW_CLOSE_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_PROGRESS_PREFIX, - WHATS_NEW_PRIVACY_URL, - WHATS_NEW_TELEMETRY_LINK_LABEL, - WHATS_NEW_TITLE, - type WhatsNewFeatureCard, -} from './whatsNewModalModel'; -import { useDialogState } from './useDialogState'; -import { useWhatsNewModalState } from './useWhatsNewModalState'; - -function WhatsNewFeatureIcon(props: { card: WhatsNewFeatureCard }) { - switch (props.card.icon) { - case 'infrastructure': - return ; - case 'workloads': - return ; - case 'storage': - return ; - case 'recovery': - return ; - } -} - -export function WhatsNewModal() { - const state = useWhatsNewModalState(); - const dialogState = useDialogState({ - get isOpen() { - return state.isOpen(); - }, - onClose: state.handleClose, - }); - const step = () => state.currentStep(); - const setPanelRef = (element: HTMLDivElement) => { - state.setPanelRef(element); - dialogState.setPanelRef(element); - }; - - return ( - - -
-
- - {(style) => ( -
- )} - - -
- -
- ); -} - -export default WhatsNewModal; diff --git a/frontend-modern/src/components/shared/__tests__/WhatsNewModal.test.tsx b/frontend-modern/src/components/shared/__tests__/WhatsNewModal.test.tsx deleted file mode 100644 index 89e8d4a6b..000000000 --- a/frontend-modern/src/components/shared/__tests__/WhatsNewModal.test.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { cleanup, fireEvent, render, screen, waitFor, within } from '@solidjs/testing-library'; -import { WhatsNewModal } from '@/components/shared/WhatsNewModal'; -import whatsNewModalSource from '@/components/shared/WhatsNewModal.tsx?raw'; -import whatsNewModalModelSource from '@/components/shared/whatsNewModalModel.ts?raw'; -import whatsNewModalStateSource from '@/components/shared/useWhatsNewModalState.ts?raw'; -import { STORAGE_KEYS } from '@/utils/localStorage'; - -const presentationPolicyIsDemoModeMock = vi.hoisted(() => vi.fn(() => false)); -const sessionPresentationPolicyResolvedMock = vi.hoisted(() => vi.fn(() => true)); - -vi.mock('@/stores/sessionPresentationPolicy', () => ({ - presentationPolicyIsDemoMode: presentationPolicyIsDemoModeMock, - sessionPresentationPolicyResolved: sessionPresentationPolicyResolvedMock, -})); - -describe('WhatsNewModal', () => { - beforeEach(() => { - window.history.pushState({}, '', '/'); - localStorage.clear(); - presentationPolicyIsDemoModeMock.mockReturnValue(false); - sessionPresentationPolicyResolvedMock.mockReturnValue(true); - }); - - afterEach(() => { - cleanup(); - }); - - it('keeps whats new modal on shell, runtime, and model owners', () => { - expect(whatsNewModalSource).toContain('useWhatsNewModalState'); - expect(whatsNewModalSource).toContain('useDialogState'); - expect(whatsNewModalSource).toContain('WHATS_NEW_FEATURE_CARDS'); - expect(whatsNewModalSource).toContain('Portal'); - expect(whatsNewModalSource).not.toContain('createLocalStorageBooleanSignal'); - 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('bg-gradient'); - expect(whatsNewModalSource).not.toContain('backdrop-blur-sm'); - - expect(whatsNewModalStateSource).toContain('export function useWhatsNewModalState'); - expect(whatsNewModalStateSource).toContain('createLocalStorageBooleanSignal'); - expect(whatsNewModalStateSource).toContain('createSignal'); - expect(whatsNewModalStateSource).toContain('createMemo'); - expect(whatsNewModalStateSource).toContain('handleNext'); - expect(whatsNewModalStateSource).toContain('handlePrevious'); - expect(whatsNewModalStateSource).toContain('spotlightStyle'); - expect(whatsNewModalStateSource).toContain('STORAGE_KEYS.WHATS_NEW_NAV_V2_SHOWN'); - expect(whatsNewModalStateSource).toContain('sessionPresentationPolicyResolved'); - expect(whatsNewModalStateSource).toContain('presentationPolicyIsDemoMode'); - expect(whatsNewModalStateSource).toContain('handleClose'); - - expect(whatsNewModalModelSource).toContain('WHATS_NEW_FEATURE_CARDS'); - expect(whatsNewModalModelSource).toContain('WHATS_NEW_DOCS_URL'); - expect(whatsNewModalModelSource).toContain('WHATS_NEW_PRIVACY_URL'); - expect(whatsNewModalModelSource).toContain('MIGRATION_GUIDE_DOC_URL'); - expect(whatsNewModalModelSource).toContain('PRIVACY_DOC_URL'); - expect(whatsNewModalModelSource).toContain('Telemetry details'); - 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).toContain('WHATS_NEW_DOCS_LABEL'); - expect(whatsNewModalModelSource).toContain("title: 'Infrastructure'"); - }); - - it('renders when the navigation modal has not been seen yet', async () => { - render(() => ); - - const dialog = await screen.findByRole('dialog', { name: 'Pulse navigation guide' }); - expect(dialog).toBeInTheDocument(); - expect(within(dialog).getByText('Step 1 of 4')).toBeInTheDocument(); - expect(within(dialog).getByText('Nav guide')).toBeInTheDocument(); - expect( - within(dialog).getByText(/Start here to add, inspect, and manage infrastructure sources/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(); - }); - - it('stays hidden for public demo sessions', async () => { - presentationPolicyIsDemoModeMock.mockReturnValue(true); - - render(() => ); - - await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - }); - }); - - it('closes on backdrop click and records the modal as seen by default', async () => { - render(() => ); - - const backdrop = await waitFor(() => { - const element = document.querySelector('[data-dialog-backdrop]') as HTMLElement | null; - expect(element).not.toBeNull(); - return element!; - }); - - fireEvent.click(backdrop); - - await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - }); - expect(localStorage.getItem(STORAGE_KEYS.WHATS_NEW_NAV_V2_SHOWN)).toBe('true'); - }); - - it('supports a session-only dismissal when "Don\'t show again" is unchecked', async () => { - render(() => ); - - const checkbox = await screen.findByRole('checkbox', { name: "Don't show again" }); - fireEvent.click(checkbox); - fireEvent.click(screen.getByRole('button', { name: 'Close' })); - - await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - }); - expect(localStorage.getItem(STORAGE_KEYS.WHATS_NEW_NAV_V2_SHOWN)).toBe('false'); - - cleanup(); - render(() => ); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toBeInTheDocument(); - }); - }); - - it('advances through the guided tour and finishes on the last step', async () => { - render(() => ); - - expect( - await screen.findByText(/Start here to add, inspect, and manage infrastructure sources/i), - ).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: 'Next' })); - 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 pools, datastores, disks/i)).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: 'Next' })); - 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(() => ); - - expect( - await screen.findByText(/Start here to add, inspect, and manage infrastructure sources/i), - ).toBeInTheDocument(); - - fireEvent.click(screen.getByRole('button', { name: /Workloads/i })); - - expect(await screen.findByText(/Use this for VMs, containers, pods/i)).toBeInTheDocument(); - expect(screen.getByText('Step 2 of 4')).toBeInTheDocument(); - }); - - it('routes the docs CTA through the navigation guide', async () => { - render(() => ); - - const docsLink = await screen.findByRole('link', { name: 'Navigation guide' }); - expect(docsLink).toHaveAttribute('href', '/docs/MIGRATION_UNIFIED_NAV.md'); - }); - - it('starts the navigation guide on recovery when opened from recovery', async () => { - window.history.pushState({}, '', '/recovery?rollupId=res%3Asystem-container-1'); - - render(() => ); - - const dialog = await screen.findByRole('dialog', { name: 'Pulse navigation guide' }); - expect(within(dialog).getByText('Step 4 of 4')).toBeInTheDocument(); - expect( - within(dialog).getByText(/Use this for backup coverage, snapshots, replication/i), - ).toBeInTheDocument(); - }); -}); diff --git a/frontend-modern/src/components/shared/useWhatsNewModalState.ts b/frontend-modern/src/components/shared/useWhatsNewModalState.ts deleted file mode 100644 index f5343fa5f..000000000 --- a/frontend-modern/src/components/shared/useWhatsNewModalState.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js'; -import { createLocalStorageBooleanSignal, STORAGE_KEYS } from '@/utils/localStorage'; -import { - presentationPolicyIsDemoMode, - sessionPresentationPolicyResolved, -} from '@/stores/sessionPresentationPolicy'; -import { WHATS_NEW_FEATURE_CARDS, WHATS_NEW_REOPEN_EVENT } from './whatsNewModalModel'; - -type SpotlightRect = { - top: number; - left: number; - width: number; - height: number; -}; - -const DESKTOP_TAB_SELECTOR_BY_TARGET = { - infrastructure: '[role="tab"][title="All agents and nodes across platforms"]', - workloads: '[role="tab"][title="VMs, containers, and Kubernetes workloads"]', - storage: '[role="tab"][title="Storage pools, disks, and datastores"]', - recovery: '[role="tab"][title="Backup, snapshot, and replication activity"]', -} as const; - -const MOBILE_TAB_SELECTOR_BY_TARGET = { - infrastructure: 'button[data-tab-id="infrastructure"]', - workloads: 'button[data-tab-id="workloads"]', - storage: 'button[data-tab-id="storage"]', - recovery: 'button[data-tab-id="recovery"]', -} as const; - -const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); - -const getInitialStepIndexForPath = (): number => { - if (typeof window === 'undefined') return 0; - const path = window.location.pathname.toLowerCase(); - const target = (() => { - if (path.startsWith('/recovery')) return 'recovery'; - if (path.startsWith('/storage')) return 'storage'; - if (path.startsWith('/workloads')) return 'workloads'; - if (path.startsWith('/infrastructure')) return 'infrastructure'; - return 'infrastructure'; - })(); - const index = WHATS_NEW_FEATURE_CARDS.findIndex((card) => card.target === target); - return index >= 0 ? index : 0; -}; - -const isVisibleElement = (element: Element | null): element is HTMLElement => { - if (!(element instanceof HTMLElement)) return false; - const rect = element.getBoundingClientRect(); - if (rect.width <= 0 || rect.height <= 0) return false; - const style = window.getComputedStyle(element); - return style.display !== 'none' && style.visibility !== 'hidden'; -}; - -const selectFirstVisible = (...selectors: string[]): HTMLElement | null => { - for (const selector of selectors) { - const visible = Array.from(document.querySelectorAll(selector)).find((candidate) => - isVisibleElement(candidate), - ); - if (visible && visible instanceof HTMLElement) { - return visible; - } - } - return null; -}; - -export function useWhatsNewModalState() { - const [hasSeen, setHasSeen] = createLocalStorageBooleanSignal( - STORAGE_KEYS.WHATS_NEW_NAV_V2_SHOWN, - false, - ); - const [dontShowAgain, setDontShowAgain] = createSignal(true); - const [dismissedForSession, setDismissedForSession] = createSignal(false); - const [stepIndex, setStepIndex] = createSignal(getInitialStepIndexForPath()); - const [panelRef, setPanelRef] = createSignal(null); - const [spotlightRect, setSpotlightRect] = createSignal(null); - - const isOpen = () => - sessionPresentationPolicyResolved() && - !presentationPolicyIsDemoMode() && - !hasSeen() && - !dismissedForSession(); - - const currentStep = createMemo(() => { - const index = clamp(stepIndex(), 0, WHATS_NEW_FEATURE_CARDS.length - 1); - return WHATS_NEW_FEATURE_CARDS[index]; - }); - - const isFirstStep = createMemo(() => stepIndex() === 0); - const isLastStep = createMemo(() => stepIndex() >= WHATS_NEW_FEATURE_CARDS.length - 1); - - const resetTourState = () => { - setStepIndex(getInitialStepIndexForPath()); - setSpotlightRect(null); - }; - - const closeTour = () => { - resetTourState(); - if (dontShowAgain()) { - setHasSeen(true); - return; - } - - setDismissedForSession(true); - }; - - const handleClose = () => { - closeTour(); - }; - - const handleNext = () => { - if (isLastStep()) { - closeTour(); - return; - } - setStepIndex((current) => clamp(current + 1, 0, WHATS_NEW_FEATURE_CARDS.length - 1)); - }; - - const handlePrevious = () => { - setStepIndex((current) => clamp(current - 1, 0, WHATS_NEW_FEATURE_CARDS.length - 1)); - }; - - const handleSelectStep = (index: number) => { - setStepIndex(clamp(index, 0, WHATS_NEW_FEATURE_CARDS.length - 1)); - }; - - if (typeof window !== 'undefined') { - const handleReopen = () => { - setHasSeen(false); - setDismissedForSession(false); - setStepIndex(getInitialStepIndexForPath()); - }; - window.addEventListener(WHATS_NEW_REOPEN_EVENT, handleReopen); - onCleanup(() => window.removeEventListener(WHATS_NEW_REOPEN_EVENT, handleReopen)); - } - - createEffect(() => { - if (!isOpen()) return; - - const updateSpotlight = () => { - const step = currentStep(); - const target = selectFirstVisible( - DESKTOP_TAB_SELECTOR_BY_TARGET[step.target], - MOBILE_TAB_SELECTOR_BY_TARGET[step.target], - ); - if (!target) { - setSpotlightRect(null); - return; - } - - const rect = target.getBoundingClientRect(); - const padding = 10; - setSpotlightRect({ - top: Math.max(12, rect.top - padding), - left: Math.max(12, rect.left - padding), - width: rect.width + padding * 2, - height: rect.height + padding * 2, - }); - }; - - updateSpotlight(); - - const resizeObserver = - typeof ResizeObserver === 'undefined' - ? null - : new ResizeObserver(() => { - updateSpotlight(); - }); - const panel = panelRef(); - if (resizeObserver && panel) { - resizeObserver.observe(panel); - } - - window.addEventListener('resize', updateSpotlight); - window.addEventListener('scroll', updateSpotlight, true); - - onCleanup(() => { - resizeObserver?.disconnect(); - window.removeEventListener('resize', updateSpotlight); - window.removeEventListener('scroll', updateSpotlight, true); - }); - }); - - const panelStyle = createMemo(() => { - if (typeof window === 'undefined') { - return { - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - }; - } - - const rect = spotlightRect(); - const panel = panelRef(); - const desktopWidth = window.innerWidth >= 1024 ? 376 : 344; - const panelWidth = Math.min(desktopWidth, window.innerWidth - 32); - const panelHeight = panel?.offsetHeight ?? 260; - - if (!rect) { - return { - width: `${panelWidth}px`, - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - }; - } - - const spaceBelow = window.innerHeight - (rect.top + rect.height); - const prefersAbove = spaceBelow < panelHeight + 24 && rect.top > panelHeight + 24; - const unclampedTop = prefersAbove ? rect.top - panelHeight - 20 : rect.top + rect.height + 20; - const maxTop = Math.max(16, window.innerHeight - panelHeight - 16); - const top = clamp(unclampedTop, 16, maxTop); - const unclampedLeft = rect.left + rect.width / 2 - panelWidth / 2; - const maxLeft = Math.max(16, window.innerWidth - panelWidth - 16); - const left = clamp(unclampedLeft, 16, maxLeft); - - return { - width: `${panelWidth}px`, - top: `${top}px`, - left: `${left}px`, - }; - }); - - const spotlightStyle = createMemo(() => { - const rect = spotlightRect(); - if (!rect) return null; - - return { - top: `${rect.top}px`, - left: `${rect.left}px`, - width: `${rect.width}px`, - height: `${rect.height}px`, - 'box-shadow': - '0 0 0 9999px rgba(15, 23, 42, 0.78), 0 0 0 2px rgba(255, 255, 255, 0.4), 0 0 40px rgba(96, 165, 250, 0.75)', - }; - }); - - return { - currentStep, - dontShowAgain, - handleClose, - handleNext, - handlePrevious, - handleSelectStep, - isFirstStep, - isLastStep, - isOpen, - panelStyle, - setDontShowAgain, - setPanelRef, - spotlightStyle, - stepCount: () => WHATS_NEW_FEATURE_CARDS.length, - stepIndex, - }; -} diff --git a/frontend-modern/src/components/shared/whatsNewModalModel.ts b/frontend-modern/src/components/shared/whatsNewModalModel.ts deleted file mode 100644 index a53ede371..000000000 --- a/frontend-modern/src/components/shared/whatsNewModalModel.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { MIGRATION_GUIDE_DOC_URL, PRIVACY_DOC_URL } from '@/utils/docsLinks'; - -export interface WhatsNewFeatureCard { - accent: string; - description: string; - icon: 'infrastructure' | 'workloads' | 'storage' | 'recovery'; - target: 'infrastructure' | 'workloads' | 'storage' | 'recovery'; - title: string; -} - -export const WHATS_NEW_DOCS_URL = MIGRATION_GUIDE_DOC_URL; -export const WHATS_NEW_PRIVACY_URL = PRIVACY_DOC_URL; - -export const WHATS_NEW_FEATURE_CARDS: WhatsNewFeatureCard[] = [ - { - accent: 'border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900', - description: - 'Start here to add, inspect, 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, 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 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 backup coverage, snapshots, replication, and restore readiness.', - icon: 'recovery', - target: 'recovery', - title: 'Recovery', - }, -]; - -export const WHATS_NEW_REOPEN_EVENT = 'pulse:reopen-nav-guide'; - -export const WHATS_NEW_KICKER_LABEL = 'Nav guide'; -export const WHATS_NEW_TITLE = 'Pulse navigation guide'; -export const WHATS_NEW_PROGRESS_PREFIX = 'Step'; -export const WHATS_NEW_BACK_LABEL = 'Back'; -export const WHATS_NEW_CLOSE_LABEL = 'Close'; -export const WHATS_NEW_DOCS_LABEL = 'Navigation guide'; -export const WHATS_NEW_DO_NOT_SHOW_LABEL = "Don't show again"; -export const WHATS_NEW_NEXT_LABEL = 'Next'; -export const WHATS_NEW_PRIMARY_ACTION_LABEL = 'Done'; -export const WHATS_NEW_TELEMETRY_LINK_LABEL = 'Telemetry details'; diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index 219ae6757..c338fece0 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -36,8 +36,6 @@ import mobileNavBarModelSource from '@/components/shared/mobileNavBarModel.ts?ra import infrastructureSelectorSource from '@/components/shared/InfrastructureSelector.tsx?raw'; import pulseDataGridSource from '@/components/shared/PulseDataGrid.tsx?raw'; import pulseDataGridModelSource from '@/components/shared/pulseDataGridModel.ts?raw'; -import whatsNewModalSource from '@/components/shared/WhatsNewModal.tsx?raw'; -import whatsNewModalModelSource from '@/components/shared/whatsNewModalModel.ts?raw'; import searchFieldSource from '@/components/shared/SearchField.tsx?raw'; import searchFieldModelSource from '@/components/shared/searchFieldModel.ts?raw'; import searchInputSource from '@/components/shared/SearchInput.tsx?raw'; @@ -77,7 +75,6 @@ import historyChartStateSource from '@/components/shared/useHistoryChartState.ts import mobileNavBarStateSource from '@/components/shared/useMobileNavBarState.ts?raw'; import infrastructureSelectorStateSource from '@/components/shared/useInfrastructureSelectorState.ts?raw'; import pulseDataGridStateSource from '@/components/shared/usePulseDataGridState.ts?raw'; -import whatsNewModalStateSource from '@/components/shared/useWhatsNewModalState.ts?raw'; import searchFieldStateSource from '@/components/shared/useSearchFieldState.ts?raw'; import searchInputStateSource from '@/components/shared/useSearchInputState.ts?raw'; import searchInputEnhancementsStateSource from '@/components/shared/useSearchInputEnhancements.ts?raw'; @@ -2916,30 +2913,6 @@ describe('frontend resource type boundaries', () => { expect(monitoredSystemPresentationSource).toContain( 'export function getMonitoredSystemDisclosureToggleLabel', ); - expect(whatsNewModalSource).toContain('useWhatsNewModalState'); - expect(whatsNewModalSource).toContain('useDialogState'); - expect(whatsNewModalSource).toContain('WHATS_NEW_FEATURE_CARDS'); - expect(whatsNewModalSource).toContain('Portal'); - expect(whatsNewModalSource).not.toContain('createLocalStorageBooleanSignal'); - 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(whatsNewModalStateSource).toContain('createLocalStorageBooleanSignal'); - expect(whatsNewModalStateSource).toContain('createSignal'); - expect(whatsNewModalStateSource).toContain('createMemo'); - expect(whatsNewModalStateSource).toContain('STORAGE_KEYS.WHATS_NEW_NAV_V2_SHOWN'); - expect(whatsNewModalStateSource).toContain('handleClose'); - expect(whatsNewModalStateSource).toContain('handleNext'); - expect(whatsNewModalStateSource).toContain('spotlightStyle'); - expect(whatsNewModalModelSource).toContain('WHATS_NEW_FEATURE_CARDS'); - expect(whatsNewModalModelSource).toContain('WHATS_NEW_TELEMETRY_LINK_LABEL'); - expect(whatsNewModalModelSource).toContain('WHATS_NEW_DOCS_URL'); - expect(whatsNewModalModelSource).toContain('WHATS_NEW_PRIVACY_URL'); - expect(whatsNewModalModelSource).toContain('WHATS_NEW_DOCS_LABEL'); - expect(whatsNewModalModelSource).toContain('MIGRATION_GUIDE_DOC_URL'); expect(tooltipSource).toContain('useTooltipState'); expect(tooltipSource).toContain('createTooltipSystemState'); expect(tooltipSource).not.toContain('createSignal'); diff --git a/frontend-modern/src/utils/localStorage.ts b/frontend-modern/src/utils/localStorage.ts index c0c3c8c61..b4bd48d76 100644 --- a/frontend-modern/src/utils/localStorage.ts +++ b/frontend-modern/src/utils/localStorage.ts @@ -208,7 +208,6 @@ export const STORAGE_KEYS = { // Feature discovery DISMISSED_FEATURE_TIPS: 'pulse-dismissed-feature-tips', - WHATS_NEW_NAV_V2_SHOWN: 'pulse_whats_new_v2_shown', DEBUG_MODE: 'pulse_debug_mode', // GitHub star prompt diff --git a/tests/integration/tests/04-mobile.spec.ts b/tests/integration/tests/04-mobile.spec.ts index db3561dce..0fd4f61a3 100644 --- a/tests/integration/tests/04-mobile.spec.ts +++ b/tests/integration/tests/04-mobile.spec.ts @@ -1,5 +1,5 @@ import { test, expect, devices } from '@playwright/test'; -import { dismissWhatsNewModal, ensureAuthenticated } from './helpers'; +import { ensureAuthenticated } from './helpers'; const getViewportWidth = async (page: import('@playwright/test').Page): Promise => { const size = page.viewportSize(); @@ -13,7 +13,6 @@ test.describe('Mobile viewport flows', () => { }); test('bottom nav bar is visible on mobile', async ({ page }) => { - await dismissWhatsNewModal(page); await page.goto('/infrastructure'); await expect(page.locator('#root')).toBeVisible(); @@ -39,7 +38,6 @@ test.describe('Mobile viewport flows', () => { }); test('MobileNavBar has safe-area padding on nav', async ({ page }) => { - await dismissWhatsNewModal(page); await page.goto('/infrastructure'); await expect(page.locator('#root')).toBeVisible(); @@ -55,8 +53,6 @@ test.describe('Mobile viewport flows', () => { }); test('Infrastructure filter bar does not overflow horizontally', async ({ page }) => { - // Prevent WhatsNew modal from blocking the page. - await dismissWhatsNewModal(page); await page.goto('/infrastructure'); await expect(page.getByTestId('infrastructure-page')).toBeVisible(); @@ -71,7 +67,6 @@ test.describe('Mobile viewport flows', () => { }); test('Infrastructure table wrapper enables horizontal overflow when needed', async ({ page }) => { - await dismissWhatsNewModal(page); await page.goto('/infrastructure'); await expect(page.getByTestId('infrastructure-page')).toBeVisible(); @@ -104,8 +99,6 @@ test.describe('Mobile viewport flows', () => { }); test('Tapping a resource row opens the detail drawer', async ({ page }) => { - // Prevent WhatsNew modal from intercepting row clicks. - await dismissWhatsNewModal(page); await page.goto('/infrastructure'); await expect(page.getByTestId('infrastructure-page')).toBeVisible(); diff --git a/tests/integration/tests/06-theme-visual.spec.ts b/tests/integration/tests/06-theme-visual.spec.ts index e46696717..266d2f220 100644 --- a/tests/integration/tests/06-theme-visual.spec.ts +++ b/tests/integration/tests/06-theme-visual.spec.ts @@ -53,7 +53,6 @@ async function applyThemePreference( localStorage.setItem('pulseThemePreference', pref); localStorage.setItem('darkMode', String(pref === 'dark')); localStorage.removeItem('pulse_dark_mode'); - localStorage.setItem('pulse_whats_new_v2_shown', 'true'); if (forceLoggedOut) { localStorage.setItem('just_logged_out', 'true'); } diff --git a/tests/integration/tests/19-telemetry-disclosure.spec.ts b/tests/integration/tests/19-telemetry-disclosure.spec.ts index 2bff608b3..cf970e90a 100644 --- a/tests/integration/tests/19-telemetry-disclosure.spec.ts +++ b/tests/integration/tests/19-telemetry-disclosure.spec.ts @@ -50,27 +50,6 @@ async function expectPopupDoc( await popup.close(); } -async function expectSpotlightAround(spotlight: Locator, target: Locator) { - await expect(spotlight).toBeVisible(); - await expect(target).toBeVisible(); - await expect - .poll(async () => { - const spotlightBox = await spotlight.boundingBox(); - const targetBox = await target.boundingBox(); - if (!spotlightBox || !targetBox) { - return false; - } - - return ( - spotlightBox.x <= targetBox.x + 1 && - spotlightBox.y <= targetBox.y + 1 && - spotlightBox.x + spotlightBox.width >= targetBox.x + targetBox.width - 1 && - spotlightBox.y + spotlightBox.height >= targetBox.y + targetBox.height - 1 - ); - }) - .toBe(true); -} - async function readTelemetryPreview(page: Page) { const preview = page.locator('pre[aria-label="Telemetry payload preview"]'); await expect(preview).toBeVisible(); @@ -132,66 +111,4 @@ test.describe('Telemetry disclosure', () => { }) .not.toBe(initialPreview.install_id); }); - - test('whats-new tour opens shipped privacy and navigation guide pages', async ({ page }, testInfo) => { - test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop-only telemetry disclosure coverage'); - - await page.addInitScript(() => { - localStorage.removeItem('pulse_whats_new_v2_shown'); - }); - - await page.goto('/infrastructure', { waitUntil: 'domcontentloaded' }); - - const dialog = page.getByRole('dialog'); - const spotlight = page.locator('[data-tour-spotlight]'); - const assistantLauncher = page.getByRole('button', { name: 'Expand Pulse Assistant' }); - const infrastructureTab = page.locator( - '[role="tab"][title="All agents and nodes across platforms"]', - ); - await expect(dialog).toHaveAttribute('aria-label', 'Pulse navigation guide'); - await expect(dialog.getByText('Step 1 of 4')).toBeVisible(); - await expect(dialog.getByText('Nav guide')).toBeVisible(); - await expect(assistantLauncher).toBeHidden(); - await expect(spotlight).toHaveAttribute('data-tour-step', 'infrastructure'); - await expect(dialog).toHaveAttribute('data-tour-step', 'infrastructure'); - await expectSpotlightAround(spotlight, infrastructureTab); - await expect( - dialog.getByText(/Start here to add, inspect, and manage infrastructure sources/i), - ).toBeVisible(); - await expect(dialog.getByRole('link', { name: 'Telemetry details' })).toBeVisible(); - - const privacyLink = dialog.getByRole('link', { name: 'Telemetry details' }); - await expect(privacyLink).toHaveAttribute('href', '/docs/PRIVACY.md'); - await expectPopupDoc( - page, - privacyLink, - '/docs/PRIVACY.md', - 'Pulse currently has two usage-data scopes', - ); - - const docsLink = dialog.getByRole('link', { name: 'Navigation guide' }); - await expect(docsLink).toHaveAttribute('href', '/docs/MIGRATION_UNIFIED_NAV.md'); - await expectPopupDoc( - page, - docsLink, - '/docs/MIGRATION_UNIFIED_NAV.md', - 'Migration Guide: Unified Navigation', - ); - - for (let step = 0; step < 3; step += 1) { - await dialog.getByRole('button', { name: 'Next' }).click(); - } - await dialog.getByRole('button', { name: 'Done' }).click(); - await expect(dialog).not.toBeVisible(); - await expect(assistantLauncher).toBeVisible(); - - await assistantLauncher.click(); - await expect(page.getByRole('heading', { name: 'Pulse Assistant' })).toBeVisible(); - await expect( - page.getByText('Observed context, provider-backed reasoning, and governed actions.'), - ).toBeVisible(); - - await page.getByTitle('Pulse Assistant sessions').click(); - await expect(page.getByText('No previous assistant sessions')).toBeVisible(); - }); }); diff --git a/tests/integration/tests/45-workloads-memory-tail.spec.ts b/tests/integration/tests/45-workloads-memory-tail.spec.ts index 1358f5dbd..01ccd3795 100644 --- a/tests/integration/tests/45-workloads-memory-tail.spec.ts +++ b/tests/integration/tests/45-workloads-memory-tail.spec.ts @@ -68,15 +68,6 @@ async function ensureMockModeEnabled(page: import('@playwright/test').Page): Pro } } -async function dismissWhatsNewModal(page: import('@playwright/test').Page): Promise { - const modalTitle = page.getByText('Welcome to Pulse v6'); - if (!(await modalTitle.isVisible().catch(() => false))) { - return; - } - await page.getByRole('button', { name: 'Skip tour' }).click(); - await expect(modalTitle).toHaveCount(0); -} - function average(values: number[]): number { if (values.length === 0) return 0; return values.reduce((sum, value) => sum + value, 0) / values.length; @@ -161,7 +152,6 @@ test.describe.serial('Workloads memory tail', () => { await page.reload({ waitUntil: 'domcontentloaded' }); await expect(page.getByTestId('workloads-summary')).toBeVisible(); - await dismissWhatsNewModal(page); const response = await responsePromise; expect(response.ok()).toBeTruthy(); diff --git a/tests/integration/tests/46-storage-summary-continuity.spec.ts b/tests/integration/tests/46-storage-summary-continuity.spec.ts index 3d89a0b43..5cd55b3be 100644 --- a/tests/integration/tests/46-storage-summary-continuity.spec.ts +++ b/tests/integration/tests/46-storage-summary-continuity.spec.ts @@ -98,9 +98,6 @@ test.describe.serial('Storage summary chart continuity', () => { fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); - await page.addInitScript(() => { - localStorage.setItem('pulse_whats_new_v2_shown', 'true'); - }); await page.goto('/storage', { waitUntil: 'domcontentloaded' }); await expect(page).toHaveURL(/\/storage/); await expect(page.getByTestId('storage-summary')).toBeVisible(); diff --git a/tests/integration/tests/53-demo-mode-commercial-boundary.spec.ts b/tests/integration/tests/53-demo-mode-commercial-boundary.spec.ts index 940fbd0a8..c5ae0041b 100644 --- a/tests/integration/tests/53-demo-mode-commercial-boundary.spec.ts +++ b/tests/integration/tests/53-demo-mode-commercial-boundary.spec.ts @@ -231,10 +231,6 @@ base.describe('Demo mode commercial boundary', () => { let billingStateRequests = 0; let checkoutStartRequests = 0; - await page.addInitScript(() => { - localStorage.setItem('pulse_whats_new_v2_shown', 'true'); - }); - await page.route('**/api/security/status', async (route) => { await route.fulfill({ status: 200, @@ -424,9 +420,6 @@ base.describe('Managed demo runtime commercial boundary', () => { base( 'hides commercial surfaces and APIs without browser route stubs', async ({ page }) => { - await page.addInitScript(() => { - localStorage.setItem('pulse_whats_new_v2_shown', 'true'); - }); await ensureAuthenticated(page); const hiddenCommercialRequests = trackBrowserRequests( diff --git a/tests/integration/tests/54-monitored-system-billing-focus.spec.ts b/tests/integration/tests/54-monitored-system-billing-focus.spec.ts index 674ab17ff..81e79391f 100644 --- a/tests/integration/tests/54-monitored-system-billing-focus.spec.ts +++ b/tests/integration/tests/54-monitored-system-billing-focus.spec.ts @@ -173,10 +173,6 @@ async function configureMonitoredSystemBillingFixtures( const runtimeCapabilities = fixtures.runtimeCapabilities ?? MONITORED_SYSTEM_RUNTIME_CAPABILITIES; - await page.addInitScript(() => { - localStorage.setItem("pulse_whats_new_v2_shown", "true"); - }); - await page.route("**/api/security/status", async (route) => { await route.fulfill({ status: 200, diff --git a/tests/integration/tests/55-self-hosted-upgrade-return.spec.ts b/tests/integration/tests/55-self-hosted-upgrade-return.spec.ts index 8839fa657..35f61b4e0 100644 --- a/tests/integration/tests/55-self-hosted-upgrade-return.spec.ts +++ b/tests/integration/tests/55-self-hosted-upgrade-return.spec.ts @@ -351,10 +351,6 @@ async function configureBillingFixtures( ) { const activationState = options.activationState ?? { current: false }; - await page.addInitScript(() => { - localStorage.setItem("pulse_whats_new_v2_shown", "true"); - }); - await context.route("**/api/security/status", async (route) => { await fulfillJSON(route, { hasAuthentication: true, diff --git a/tests/integration/tests/57-release-candidate-shell.spec.ts b/tests/integration/tests/57-release-candidate-shell.spec.ts index 4309ba2c3..6c99bd20f 100644 --- a/tests/integration/tests/57-release-candidate-shell.spec.ts +++ b/tests/integration/tests/57-release-candidate-shell.spec.ts @@ -18,10 +18,6 @@ test.describe('Release candidate shell', () => { await waitForPulseReady(page); - await page.addInitScript(() => { - localStorage.setItem('pulse_whats_new_v2_shown', 'true'); - }); - await page.route('**/api/security/status', (route) => route.fulfill({ status: 200, diff --git a/tests/integration/tests/59-workloads-column-layout.spec.ts b/tests/integration/tests/59-workloads-column-layout.spec.ts index a5331c40c..f6ba3db69 100644 --- a/tests/integration/tests/59-workloads-column-layout.spec.ts +++ b/tests/integration/tests/59-workloads-column-layout.spec.ts @@ -139,9 +139,6 @@ test.describe.serial('Workloads column layout', () => { test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop runtime proof'); await page.setViewportSize({ width: 1440, height: 1200 }); - await page.addInitScript(() => { - localStorage.setItem('pulse_whats_new_v2_shown', 'true'); - }); await ensureMockModeEnabled(page); await page.goto('/workloads', { waitUntil: 'domcontentloaded' }); diff --git a/tests/integration/tests/61-diagnostics-commercial-funnel.spec.ts b/tests/integration/tests/61-diagnostics-commercial-funnel.spec.ts index 31d913369..4e22bc4b6 100644 --- a/tests/integration/tests/61-diagnostics-commercial-funnel.spec.ts +++ b/tests/integration/tests/61-diagnostics-commercial-funnel.spec.ts @@ -165,7 +165,6 @@ test("renders the commercial funnel diagnostics card in the browser", async ({ }), ); sessionStorage.setItem("pulse_auth_user", "admin"); - localStorage.setItem("pulse_whats_new_v2_shown", "true"); }); await page.route("**/api/security/status", async (route) => { diff --git a/tests/integration/tests/61-infrastructure-column-layout.spec.ts b/tests/integration/tests/61-infrastructure-column-layout.spec.ts index 513dea87d..55ba2c72f 100644 --- a/tests/integration/tests/61-infrastructure-column-layout.spec.ts +++ b/tests/integration/tests/61-infrastructure-column-layout.spec.ts @@ -110,7 +110,6 @@ test.describe.serial('Infrastructure column layout', () => { await page.setViewportSize({ width: 1920, height: 1200 }); await page.addInitScript(() => { - localStorage.setItem('pulse_whats_new_v2_shown', 'true'); localStorage.setItem('fullWidthMode', 'full-width'); }); diff --git a/tests/integration/tests/62-runtime-home-onboarding-contract.spec.ts b/tests/integration/tests/62-runtime-home-onboarding-contract.spec.ts index 3768bfd1d..9ffb940d0 100644 --- a/tests/integration/tests/62-runtime-home-onboarding-contract.spec.ts +++ b/tests/integration/tests/62-runtime-home-onboarding-contract.spec.ts @@ -39,12 +39,6 @@ const test = base.extend<{}, WorkerFixtures>({ test.describe("runtime-home onboarding contract", () => { test.setTimeout(180_000); - test.beforeEach(async ({ page }) => { - await page.addInitScript(() => { - localStorage.setItem("pulse_whats_new_v2_shown", "true"); - }); - }); - test("normalizes the agent install handoff onto the shared infrastructure workspace", async ({ page, }) => { diff --git a/tests/integration/tests/62-storage-growth-column.spec.ts b/tests/integration/tests/62-storage-growth-column.spec.ts index 0917d899d..d6cccab71 100644 --- a/tests/integration/tests/62-storage-growth-column.spec.ts +++ b/tests/integration/tests/62-storage-growth-column.spec.ts @@ -150,10 +150,6 @@ test.describe.serial('Storage growth column', () => { fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); - await page.addInitScript(() => { - localStorage.setItem('pulse_whats_new_v2_shown', 'true'); - }); - await page.goto('/storage', { waitUntil: 'domcontentloaded' }); await expect(page).toHaveURL(/\/storage/); await expect(page.getByTestId('storage-summary')).toBeVisible(); diff --git a/tests/integration/tests/64-workloads-proxmox-refresh-stability.spec.ts b/tests/integration/tests/64-workloads-proxmox-refresh-stability.spec.ts index 6c61eca51..3c53828d4 100644 --- a/tests/integration/tests/64-workloads-proxmox-refresh-stability.spec.ts +++ b/tests/integration/tests/64-workloads-proxmox-refresh-stability.spec.ts @@ -139,9 +139,6 @@ test.describe.serial("Workloads Proxmox refresh stability", () => { ); await ensureMockModeEnabled(page); - await page.addInitScript(() => { - localStorage.setItem("pulse_whats_new_v2_shown", "true"); - }); await page.goto("/workloads?type=vm&platform=proxmox-pve", { waitUntil: "domcontentloaded", diff --git a/tests/integration/tests/65-offline-proxmox-node-visibility.spec.ts b/tests/integration/tests/65-offline-proxmox-node-visibility.spec.ts index 116205cbb..d4fe77085 100644 --- a/tests/integration/tests/65-offline-proxmox-node-visibility.spec.ts +++ b/tests/integration/tests/65-offline-proxmox-node-visibility.spec.ts @@ -134,10 +134,6 @@ test.describe('Offline Proxmox node visibility', () => { test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop runtime proof'); fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); - await page.addInitScript(() => { - localStorage.setItem('pulse_whats_new_v2_shown', 'true'); - }); - await page.route('**/api/resources**', async (route) => { const requestUrl = new URL(route.request().url()); diff --git a/tests/integration/tests/66-organization-sharing-approval-ui.spec.ts b/tests/integration/tests/66-organization-sharing-approval-ui.spec.ts index 356daade7..b2a817407 100644 --- a/tests/integration/tests/66-organization-sharing-approval-ui.spec.ts +++ b/tests/integration/tests/66-organization-sharing-approval-ui.spec.ts @@ -81,7 +81,6 @@ test.describe('Organization sharing approval UI', () => { }; await page.addInitScript((orgId) => { - localStorage.setItem('pulse_whats_new_v2_shown', 'true'); sessionStorage.setItem('pulse_org_id', orgId); localStorage.setItem('pulse_org_id', orgId); document.cookie = `pulse_org_id=${encodeURIComponent(orgId)}; Path=/; SameSite=Lax`; diff --git a/tests/integration/tests/68-infrastructure-onboarding.spec.ts b/tests/integration/tests/68-infrastructure-onboarding.spec.ts index 00d6d026a..86c7181f6 100644 --- a/tests/integration/tests/68-infrastructure-onboarding.spec.ts +++ b/tests/integration/tests/68-infrastructure-onboarding.spec.ts @@ -88,10 +88,6 @@ async function recordUpgradeMetricEvents( } async function prepareOnboardingPage(page: Page): Promise { - await page.addInitScript(() => { - localStorage.setItem("pulse_whats_new_v2_shown", "true"); - }); - await stubConnectionsList(page); } diff --git a/tests/integration/tests/helpers.ts b/tests/integration/tests/helpers.ts index 72c96ef2b..3b397a6ca 100644 --- a/tests/integration/tests/helpers.ts +++ b/tests/integration/tests/helpers.ts @@ -359,9 +359,6 @@ export async function ensureFirstRunExperience( page: Page, options: CompleteSetupWizardOptions = {}, ) { - await page.addInitScript(() => { - localStorage.setItem("pulse_whats_new_v2_shown", "true"); - }); await waitForPulseReady(page); const completionTarget = options.completionTarget ?? "install"; @@ -506,24 +503,7 @@ export async function login(page: Page, credentials = E2E_CREDENTIALS) { .toBe("authenticated"); } -/** - * Dismiss the WhatsNew modal that appears on first visit by marking it as seen - * in localStorage. This prevents the "fixed inset-0 z-50" overlay from blocking - * clicks (logout button, row clicks, etc.) in tests. - */ -export async function dismissWhatsNewModal(page: Page): Promise { - await page.evaluate(() => { - localStorage.setItem("pulse_whats_new_v2_shown", "true"); - }); -} - export async function ensureAuthenticated(page: Page) { - // Pre-set the WhatsNew modal localStorage key via an init script that runs before - // any page script on every navigation. This prevents the "fixed inset-0 z-50" - // overlay from appearing and blocking clicks (logout, row taps, etc.) in tests. - await page.addInitScript(() => { - localStorage.setItem("pulse_whats_new_v2_shown", "true"); - }); await waitForPulseReady(page); await maybeCompleteSetupWizard(page); await login(page);