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);