Automattic / Automattic/jetpack

Port Scan overview to Jetpack wp-admin as a native page

Open
#48,456 0 comments 0 reactions 1 assignee Claimed by @ilonagl View on GitHub
Dominant language
PHP
Stars
1.8k
Forks
898
Avg merge
1d 18h
Merged PRs (30d)
774

Description

Replace the standalone Scan UI inside the Jetpack Protect plugin with the **MSD-aligned overview ported from Calypso's Dashboard**. Architecture follows `projects/packages/activity-log/` (per [#48244](https://github.com/Automattic/jetpack/pull/48244)) and `projects/packages/backup/` (per [#48236](https://github.com/Automattic/jetpack/pull/48236) / [#48297](https://github.com/Automattic/jetpack/issues/48297)) β€” a new bundled `projects/packages/scan/` package with a wp-admin shell, TanStack Query data layer, and `@wordpress/dataviews` UI.

## Status (2026-05-02)

Implementation lives on [#48458](https://github.com/Automattic/jetpack/pull/48458) (`try/jetpack-scan-new-ui`). Progress vs. the 8-phase plan:

- βœ… **Phases 0, 1, 2, 3, 4, 5, 6, 7** β€” fully shipped.
- 🟑 **Phase 8** β€” bridge permission tests (9/9 phpunit) + `isFixComplete` (5/5 jest) shipped; broader bridge Γ— happy/error coverage and Playwright e2e deferred.

**Build pipeline**: migrated from webpack + `@automattic/jetpack-webpack-config` to `@wordpress/build` (mirrors Newsletter [#48420](https://github.com/Automattic/jetpack/pull/48420) and Forms). Routes now live under `routes/index/` (`route.tsx`, `stage.tsx`, `route.scss`, `package.json`); the page chrome moved to `_inc/components/scan-page.{tsx,scss}`. Routing switched from `react-router`'s `useSearchParams` to `@wordpress/route`'s `useSearch` / `useNavigate`. PHP-side, `Jetpack_Scan` loads `build/build.php`, registers polyfills via `WP_Build_Polyfills`, and bridges the user-facing `?page=jetpack-scan` slug onto wp-build's auto-generated `jetpack-scan-wp-admin` enqueue.

## Source

- Calypso shell: `client/dashboard/sites/scan/` ([wp-calypso](https://github.com/Automattic/wp-calypso/tree/trunk/client/dashboard/sites/scan))
- Calypso DataViews tables: `client/dashboard/sites/scan-active/` and `client/dashboard/sites/scan-history/`
- Calypso REST surface: `wpcom/v2 /sites/:siteId/scan*` and `…/alerts/*` (see `packages/api-queries/src/site-scan.ts`)
- Existing component library: `projects/js-packages/scan/` (`ThreatsDataViews`, `ThreatSeverityBadge`, `Threat` types) β€” reused as building blocks
- Pin: Calypso source SHA at port start, recorded in the package's `AGENTS.md`

> Note: Calypso's Scan dashboard is **single-pane tabbed** (Active / History) with threat details in a Modal via DataViews `RenderModal` β€” not dual-pane like Backup. The port preserves this layout. See decision #1 below if we want to deliberately diverge.

## Architectural rules

- **Package location:** new `projects/packages/scan/` (sibling to `activity-log`, `backup`). Existing `projects/plugins/protect/` is left untouched in this PR; future deprecation tracked separately.
- **Package name:** `@automattic/jetpack-scan-page` (avoids clash with existing `@automattic/jetpack-scan` js-package).
- **Build:** `@wordpress/build` (`wp-build`) β€” Newsletter / Forms pattern. Drops webpack + `@automattic/jetpack-webpack-config` for this package; the plugin's webpack chain still consumes the wp-build `build/` output via the auto-generated `build/build.php`.
- **Shell pattern (post wp-build migration):**
- `src/class-jetpack-scan.php` β€” wp-admin submenu (`?page=jetpack-scan`), `WP_Build_Polyfills::register`, `require build/build.php`, `bridge_wp_build_enqueue`, `fix_boot_import_map_ordering` (work-around for `WordPress/gutenberg#76870`)
- `src/class-initial-state.php`, `src/class-rest-controller.php`
- `routes/index/{route.tsx,stage.tsx,route.scss,package.json}` β€” the wp-build entry. Stage mounts `QueryClientProvider` + `HeaderActionsProvider` and renders `` with `Tabs.Panel` children.
- `_inc/components/scan-page.{tsx,scss}` β€” page chrome (`@wordpress/admin-ui` `Page` + `Tabs.Root` + sticky tab row anchored to `--jetpack-scan-page-header-height`)
- **Data layer:** TanStack Query factories matching Calypso names (`siteScanQuery`, `siteScanHistoryQuery`, `siteScanCountsQuery`, `siteScanEnqueueMutation`, `fixThreatMutation`, `ignoreThreatMutation`, `unignoreThreatMutation`, `fixThreatsMutation`, `fixThreatsStatusQuery`).
- **REST bridges (admin-only) under `jetpack/v4/site/scan/*`:**
- `GET /site/scan` ← `/sites/:siteId/scan` *(blog auth β€” matches Protect's `Threats::fetch_status()`)*
- `GET /site/scan/history` ← `/sites/:siteId/scan/history` *(blog auth)*
- `GET /site/scan/counts` ← `/sites/:siteId/scan/counts` *(blog auth)*
- `POST /site/scan/enqueue` ← `/sites/:siteId/scan/enqueue` *(blog auth β€” matches Protect's `Threats::scan()`)*
- `POST /site/scan/threat/{id}/fix` (and `/ignore`, `/unignore`) ← `/sites/:siteId/alerts/:threatId?...` *(user auth)*
- `POST /site/scan/threats/fix` ← `/sites/:siteId/alerts/fix` *(user auth)*
- `GET /site/scan/threats/fix-status` ← `/sites/:siteId/alerts/fix?threat_ids[]=…` *(user auth)*

Implementation: each route proxies to WPCOM via `Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_blog()` for site-level reads / scan enqueue and `…_as_user()` for alert mutations. Permission callback admin-only, validated by phpunit.
- **Routing:** `@wordpress/route` (`useNavigate` / `useSearch`). Single overview route at `/`; `?tab=active|history` switches panels, `?jps-mock=1` for mock mode. (Was originally planned as `createHashRouter` from `react-router@7`; switched as part of the wp-build migration to match Newsletter / Forms.)
- **Mock mode:** `?jps-mock=1` short-circuits all gates so design iteration works without a Scan plan or WPCOM connection. Mock fixtures live in `src/js/data/mock/`.
- **Components:** `@wordpress/dataviews@14`, `@wordpress/components`, `@wordpress/ui`, `Page` from `@wordpress/admin-ui`, WPDS tokens (`--wpds-*`).
- **i18n:** textdomain `jetpack-scan-page`. `__`, `_n`, `_x`, `sprintf` from `@wordpress/i18n`.
- **Suppress** `admin_notices` / `all_admin_notices` on the Scan page (Forms-style).
- **Drop Calypso-only concerns:** `HostingFeatureGatedWithCallout`, `JETPACK_CONTACT_SUPPORT` link, multi-site routing (`siteRoute.useParams`, `useRouter().navigate('/sites/...')`), `TimeMismatchNotice`, `isEnabled('dashboard/omnibar')`, `PerformanceTrackerStop`, `calypso_dashboard_*` event names.
- **Reuse** `@automattic/jetpack-scan` (existing js-package) for `ThreatSeverityBadge` + `Threat` types. Calypso's modals are richer than what that package ships β€” Scan's row-action modals (`fix-threat-modal.tsx`, `ignore-threat-modal.tsx`, `unignore-threat-modal.tsx`) are ported into the new package and wired via the upstream's `RenderFixModal` / `RenderIgnoreModal` / `RenderUnignoreModal` props (added in #48458).

## Plan

### Phase 0: Shell + data layer skeleton

- [x] Scaffold `projects/packages/scan/` (`package.json`, `composer.json`, `AGENTS.md`)
- [x] PHP shell: `class-jetpack-scan.php`, `class-initial-state.php` (`JPSCAN_INITIAL_STATE`), `class-rest-controller.php` (empty handlers)
- [x] JS shell: ~~`index.js`, `admin.tsx` (hash router), `shell.tsx` (`AdminPage` + `HeaderActionsProvider`), `providers.tsx` (`QueryClient` + `ThemeProvider`), `gates.tsx`, `routes.ts`~~ β€” replaced by wp-build entry: `routes/index/{route,stage,route.scss,package.json}.tsx` + `_inc/components/scan-page.{tsx,scss}` + `src/js/header-actions-context.tsx` + `gates.tsx` (now a pass-through)
- [x] Data skeleton: `data/{fetchers,query-options,types,use-site-data,use-track-event}.ts`
- [x] Mock mode: `?jps-mock=1` flag, `data/mock/fixtures.ts`, `mock-banner.tsx` (5 active + 4 history fixtures)
- [x] wp-admin menu entry registered, blank "Hello Scan" page renders

### Phase 1: Active threats

- [x] Port `client/dashboard/sites/scan-active/index.tsx` β†’ `screens/overview/active-threats.tsx`
- [x] Port `dataviews/{fields,actions}.tsx` for active list (lives in upstream `@automattic/jetpack-scan`'s `ThreatsDataViews`)
- [x] Port `severity-badge.tsx`, `utils.ts` (`getThreatType`/`getThreatIcon`) β€” reused from `@automattic/jetpack-scan`
- [x] Port `use-scan-state.ts` state machine (`idle|enqueued|running|success|error`)
- [x] Wire `useSiteScanQuery` over `jetpack/v4/site/scan` REST bridge
- [x] Tabbed shell rendering Active tab; History tab wired
- [x] Header actions: `ScanNowButton` + conditional `Auto-fix N threats` button (modal wired in Phase 4)
- [x] `usePersistentView` for view persistence β€” implemented as `persistKey` prop on upstream `ThreatsDataViews`. Both panels pass stable namespaced keys (`jetpack-scan:active-threats:view`, `jetpack-scan:scan-history:view`); filters / sort / search / page / layout round-trip across reloads

### Phase 2: Scan history

- [x] Port `client/dashboard/sites/scan-history/index.tsx`
- [x] Port history `dataviews/{fields,actions}.tsx`
- [x] Wire `useSiteScanHistoryQuery` over `jetpack/v4/site/scan/history`
- [x] History tab rendering

### Phase 3: Single-threat modals

- [x] Port `fix-threat-modal.tsx`, `fix-threat-confirmation.tsx`
- [x] Port `ignore-threat-modal.tsx`, `unignore-threat-modal.tsx`
- [x] Wire `fixThreatMutation`, `ignoreThreatMutation`, `unignoreThreatMutation` over threat-id REST bridges
- [x] Port `useFixThreats` orchestration hook (2s polling for fixer status)
- [x] Wire row actions in `dataviews/actions.tsx` (via new `Render*Modal` props on upstream `ThreatsDataViews`)
- [x] Handle `useFixThreatsStatusQuery` `isError` state in fix / bulk-fix modals (P2 review)
- [x] Don't render `done` step on initial bulk-fix POST failure (P2 review)

### Phase 4: Bulk fix + view-details

- [x] Port `bulk-fix-threats-modal.tsx`
- [x] Port `view-details-modal.tsx` (lean version β€” title + severity + signature + description + file path / context block / extension version / first-detected / fixed-on metadata + fix-description summary)
- [x] Wire `view-details-modal` as DataViews `RenderModal` action β€” added `RenderViewModal` prop on upstream `ThreatsDataViews` (always-eligible, `large` modal size). Wired on both panels.
- [x] Wire `fixThreatsMutation` + `fixThreatsStatusQuery` polling
- [x] "Auto-fix N threats" header CTA opens bulk-fix modal

### Phase 5: Notices, scan-now, in-progress UI

- [x] Port `scan-notices.tsx`, `scan-now-button.tsx`, `scan-status.tsx`
- [x] Active tab toggles between `` (running) and ``
- [x] Wire `siteScanEnqueueMutation` over `jetpack/v4/site/scan/enqueue`
- [x] Snackbar notices via `@wordpress/notices`
- [ ] Copy in `scan-callout-illustration.svg`, `scan-scanning-illustration.svg` β€” blocked on decision #7 (asset clearance)

### Phase 6: Polish

- [x] Drop `HostingFeatureGated`, `TimeMismatchNotice`, `PerformanceTrackerStop`, `isEnabled('dashboard/omnibar')`
- [x] Replace `JETPACK_CONTACT_SUPPORT` with Jetpack-equivalent support URL
- [ ] Decide on Codeable upsell (see decision #4)
- [x] Suppress `admin_notices` / `all_admin_notices` on the page
- [x] WPDS tokens applied; full-width layout (BoundedLayout 1344px cap deferred)
- [x] DataViews empty body anchored to `calc(100vh - 320px)`; page chain anchored to viewport so the footer pins to the bottom even on tall screens
- [x] In-table active/historic status toggle hidden via new `showStatusFilter` prop on `ThreatsDataViews` (page-level tabs already cover that dimension)
- [ ] Mobile responsiveness verification
- [x] Accessibility pass on modals (DataViews-managed; uses native `Modal` from `@wordpress/components`)
- [x] Empty / loading / error state copy

### Phase 7: Analytics

- [x] `data/use-track-event.ts` wraps `@automattic/jetpack-analytics`'s `tracks.recordEvent` (same client Forms / Backup / Activity Log use)
- [x] Rename Calypso events: `calypso_dashboard_scan_*` β†’ `jetpack_scan_*`
- [x] `jetpack_scan_scan_now`
- [x] `jetpack_scan_fix_threats_cta_click` (`{ threat_count }`)
- [x] `jetpack_scan_bulk_fix_threats_modal_open` / `_click` / `_success` / `_failed`
- [x] `jetpack_scan_{fix,ignore,unignore}_threat_modal_open` / `_click` / `_success` / `_failed` (Phase 3)
- [x] `jetpack_scan_view_details_modal_open` (Phase 4)
- [ ] `jetpack_scan_codeable_estimate_click` β€” pending decision #4
- [x] DataViews canonical: `_view_change`, `_filter_change`, `_search` (`{ has_query: boolean }`), `_page_change`, `_layout_changed` β€” added `onTrackEvent` callback prop on the upstream `ThreatsDataViews` (Protect can adopt the same hook with its own prefix). `_reset_view_click` not yet β€” DataViews doesn't expose a distinct hook for "reset view" beyond the diffs the canonical events already cover.

### Phase 8: Tests

- [x] phpunit `tests/php/Jetpack_Scan_Bridges_Test.php` β€” admin permission callback + route registration (9/9)
- [x] Jest unit tests: `isFixComplete` polling-terminator from `useFixThreatsStatusQuery` (5/5)
- [ ] Broader Jest coverage: bulk-fix orchestration, fix-status `isError` paths, modal render tests
- [ ] E2E (Playwright) happy paths: scan-now, single-threat fix, bulk-fix
- [ ] Bridge Γ— happy/error path coverage in a follow-up PR

## Decisions to make

- [ ] **#1** Faithful tabbed-with-modal port vs deliberately add a Backup-style side-pane?
- [x] **#2** DataViews configs: thin wrapper around existing `ThreatsDataViews` from `js-packages/scan` (decided β€” added `RenderFixModal` / `RenderIgnoreModal` / `RenderUnignoreModal` / `showStatusFilter` / `empty` props upstream)
- [x] **#3** Surface as a separate wp-admin entry (`?page=jetpack-scan`) β€” decided.
- [ ] **#4** Keep or drop the Codeable freelancer upsell from `threat-description.tsx`?
- [ ] **#5** Deprecate `jetpack-protect/v1/*` REST surface or keep it side-by-side with `jetpack/v4/site/scan/*`?
- [x] **#6** Confirm package name `@automattic/jetpack-scan-page` β€” confirmed.
- [ ] **#7** Confirm asset clearance for the two Calypso SVG illustrations.
- [ ] **#8** What happens to WAF + Account Protection in `plugins/protect` post-port (status quo, also port, or deprecate the plugin)?

## Open follow-ups (from #48458 review)

- **Mirror repo**: empty `github.com/Automattic/jetpack-scan-page` needs to be created (org-admin task) so the **Mirror repo check** passes. Same gating Activity Log had.
- **Phase 8 broader tests**: Jest render tests for the four modals, bulk-fix orchestration, and fix-status `isError` paths; Playwright e2e for scan-now / single-fix / bulk-fix.

## Risks

- **API contract drift** between WPCOM `wpcom/v2 /sites/:siteId/scan*` and our bridges β€” pin the Calypso commit we ported from in `AGENTS.md`.
- **Scan plan gating** β€” Calypso uses `HostingFeatureGated`; wp-admin needs an equivalent via `Plan` from `protect-status`.
- **Modal stacking** β€” bulk-fix during in-progress fixer poll could collide with view-details; verify under wp-admin.
- **Polling collisions** β€” `useScanStatusQuery` polling vs user view changes; reset page=1 on dataset changes (Activity Log pattern).
- **Asset clearance** β€” confirm the SVG illustrations can ship under Jetpack's license/branding.
- **`@wordpress/dataviews` version skew** β€” confirm parity with Activity Log + Backup; investigate `@automattic/dataviews` ([#40612](https://github.com/Automattic/jetpack/pull/40612) closing comment) if versions diverge.

## References

- [#48458](https://github.com/Automattic/jetpack/pull/48458) β€” implementation PR (this issue)
- [#48420](https://github.com/Automattic/jetpack/pull/48420) β€” Newsletter unified page (canonical wp-build pattern)
- [#48244](https://github.com/Automattic/jetpack/pull/48244) β€” Activity Log wp-admin port
- [#48236](https://github.com/Automattic/jetpack/pull/48236) β€” Backup wp-admin port
- [#48297](https://github.com/Automattic/jetpack/issues/48297) β€” Backup port tracking issue
- [#48160](https://github.com/Automattic/jetpack/issues/48160) β€” Jetpack UI Modernization umbrella
- [#48156](https://github.com/Automattic/jetpack/pull/48156) β€” `ThreatSeverityBadge` already migrated to `@wordpress/ui`
- [#48154](https://github.com/Automattic/jetpack/pull/48154) β€” Shared `BoundedLayout` + sticky `JetpackFooter`
- Calypso source: [`client/dashboard/sites/scan/`](https://github.com/Automattic/wp-calypso/tree/trunk/client/dashboard/sites/scan), [`scan-active/`](https://github.com/Automattic/wp-calypso/tree/trunk/client/dashboard/sites/scan-active), [`scan-history/`](https://github.com/Automattic/wp-calypso/tree/trunk/client/dashboard/sites/scan-history)
- Companion plan docs: `protect-scan-port-plan.md`, `protect-dataviews-plan.md`

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.