Skip to content

Tags: Automattic/jetpack

Tags

pr-update-to-projects/packages/activity-log

Toggle pr-update-to-projects/packages/activity-log's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Activity Log: Port AL into wp-admin as a native page (#48244)

* Activity Log: Phase 0 package scaffold

Introduce a new projects/packages/activity-log/ package mirroring the
Backup package's structure, and wire it into the main Jetpack plugin's
late_initialization() so it registers its admin page and REST namespace
on every request.

Phase 0 only lays the foundation: a placeholder Admin component renders
at admin.php?page=jetpack-activity-log, the jetpack/v4/activity-log
REST namespace is reserved (no handlers yet), and the menu item is
gated the same way the existing my-jetpack "Activity Log ↗" item is
(connected user + non-multisite + manage_options).

The old my-jetpack Cloud-redirect menu item is intentionally left in
place for this phase — Phase 1 removes it.

Refs #48242.

* Activity Log: Phase 1 menu swap

Retire the legacy "Activity Log ↗" Cloud-redirect item that my-jetpack
was registering. The Phase 0 package now solely owns the menu entry at
position 14; no more duplicate entries, no more ↗ arrow.

Changes:
- Delete projects/packages/my-jetpack/src/class-activitylog.php and its
  test. Remove the Activitylog::init() call from the package's
  Initializer. The class had one job (register an external-redirect
  menu item) and that job is gone.
- On WPCOM hosts, jetpack-mu-wpcom's wpcom-admin-menu now hides the new
  jetpack-activity-log slug instead of the now-defunct
  cloud-activity-log-wp-menu redirect URL, preserving the existing
  WPCOM behavior where wordpress.com/activity-log/<domain> is the
  active link on those sites.

Self-hosted users: single "Activity Log" item, routes to the local
(placeholder) admin page.
WPCOM-hosted users: single "Activity Log" item, routes to
wordpress.com/activity-log (unchanged from before this PR).

Refs #48242.

* Activity Log: Phase 2 REST surface

Replace the Phase 0 REST_Controller stub with real handlers backing the
two endpoints the Calypso Dashboard Activity Log UI actually calls:

  GET /jetpack/v4/activity-log
  GET /jetpack/v4/activity-log/count/group

Both are thin proxies to wpcom/v2 /sites/{blog_id}/activity[/count/group]
via Client::wpcom_json_api_request_as_blog(), and they return the raw
WPCOM response shape so the ported @automattic/api-core fetcher in
Phase 3 keeps its existing `response.current.orderedItems → activityLogs`
transform unchanged.

Permission callback is `manage_options`, matching the menu gate. The
Calypso UI has no single-event lookup, so this phase does not add
/activity-log/{id} — a trimmed scope from the original plan in #48242.

Accepted query params match Calypso's ActivityLogParams 1:1: number,
page, sort_order, after, before, group[], not_group[], text_search
(plus the group-counts subset). Unknown params are dropped server-side,
so the UI can forward its filter state verbatim.

Refs #48242.

* Activity Log: Sign REST proxies as the user, not the blog

The Phase 2 endpoints were signing with the blog token via
`wpcom_json_api_request_as_blog()`, and WPCOM's `/sites/{id}/activity`
rejects that with "Only Administrators can query information about the
current site." — the upstream endpoint needs to know *which* admin is
asking.

Switch to `wpcom_json_api_request_as_user()` (matching the existing
`/jetpack/v4/site/activity` proxy in
`class.core-rest-api-endpoints.php:2041`), pass API version `'2'`, and
forward the visitor IP via `X-Forwarded-For` for parity.

Also tighten the permission callback: admins without a user-level
WPCOM connection now get a clear 403 with `activity_log_user_not_connected`
instead of a confusing forwarded "Only Administrators…" error.

Refs #48242.

* Activity Log: Phase 3 — port the DataViews UI

Ported Calypso Dashboard's `client/dashboard/sites/logs-activity/dataviews/`
into the activity-log package. The admin page now renders a real table
wired to the Phase 2 REST endpoints via `@wordpress/api-fetch`:

- DataViews table (search, activity-type filter, sort, pagination)
- ActivityActor + ActivityEvent cell renderers (avatar, gridicon→WP
  icon map, activity title + formatted description)
- Simplified FormattedBlock renderer: text decorators (strong/em/pre/
  filepath) + Link when `range.url` is present. Entity renderers
  (post/comment/person/plugin/theme/backup) render children-only
  because those Calypso routes don't resolve inside wp-admin.
- Local TanStack Query factories (activityLogQuery,
  activityLogGroupCountsQuery) that mirror the Calypso api-core
  fetcher shapes (including the `current.orderedItems → activityLogs`
  unwrap), so future ports stay aligned.
- DataViews stylesheet imported via Sass (it's a bundled WP package in
  jetpack-webpack-config, not externalized — same pattern as
  `projects/packages/forms/routes/shared.scss`).
- `jetpack-admin-page-layout` mixin scoped to the Activity Log body
  class.
- Initial_State seeds `gmtOffset`, `timezoneString`, `slug`, `locale`
  for date-cell formatting.

Deliberate scope simplifications vs. Calypso (each tracked in the PR body):
- No date range picker (was a sibling control in the parent logs shell).
- No URL-persistent view state.
- No analytics.
- No tier gating / upsell → Phase 4.
- Backup row action stubbed (disabled) → Phase 5.

Refs #48242.

* Activity Log: Polish — AdminPage header, full-bleed table, 32px icons

Feedback-driven polish on the Phase 3 UI:

- Wrap the page in `<AdminPage>` from `@automattic/jetpack-components`
  so the Jetpack masthead + footer + viewport-pinned scroll column
  come for free. Same pattern SEO (#48154) and the new Backup
  overview (#48236) adopted.
- Drop the 24px inset around the DataViews table — it now runs
  full-bleed inside AdminPage's content column. The wrapper keeps a
  flex-column layout so DataViews' internal scroll continues to work
  under `jetpack-admin-page-layout`.
- Lock the event-icon tile to a fixed 32x32 (box-sizing: border-box,
  explicit width/height/min-width + 4px padding over the 24px SVG).
  Matches Calypso's visual spec and stays stable against surrounding
  cascade.

Column widths were already in views.ts via DataViews' documented
`layout.styles` per-field object — same mechanism Calypso uses.

Refs #48242.

* Activity Log: Tweak event-icon fill, hide footer, tighten columns

Follow-up polish from design feedback on Phase 3:

- Event-icon fill → #757575.
- AdminPage `showFooter={ false }`: the full-bleed DataViews page
  doesn't need Jetpack's footer eating vertical space.
- Column minimum widths in views.ts:
    published, published_utc: 200
    actor: 100
    event: 520
  (Dropped the maxWidth pairs — columns flex above these minima.)

Refs #48242.

* Activity Log: Widen date and event minimum column widths

published / published_utc → 240px (was 200)
event → 580px (was 520)

Refs #48242.

* Activity Log: Let the Event column absorb remaining width

Give event `width: 100%` alongside its minWidth. Classic HTML-table
trick: 100%-width column consumes leftover space when siblings have
fixed widths, so Event now stretches to fill the viewport while Date
and User stay at their minima.

Refs #48242.

* Activity Log: Use the real Jetpack logo for the "Jetpack" actor

When the actor is "Jetpack" / "Jetpack Boost" / Happiness Engineer, the
avatar was falling back to the WordPress icon. Swap in JetpackLogo from
@automattic/jetpack-components (already a dep) so those rows show the
proper brand.

Refs #48242.

* Activity Log: wire Reset view + persist view options

Rename the broken `onResetView` prop to DataViews' actual `onReset`
with the `false | function` semantics so the built-in Reset view
button renders in the cog popover (disabled until the view is
modified, indicator dot on the cog when modified). Extract the view
state into a `usePersistentView` hook that mirrors Calypso's
behavior: persist the non-transient bits (fields, density, sort,
perPage, layout) to `localStorage` under `jetpack-activity-log:view`,
strip `page`/`search`/empty `filters` before writing, and clear the
entry when the view returns to default. The hook's interface leaves
room to swap the backing store to user-meta later without touching
the component.

Refs #48242.

* Activity Log: document the UI primitives preference order

Add a package-level AGENTS.md that steers future UI work toward the
WordPress Design System in the right order: `@wordpress/ui` first,
`@wordpress/components` only where `@wordpress/ui` has no stable
equivalent, `@wordpress/dataviews` sub-components for data-presentation
extensions, and `@wordpress/admin-ui` (via `AdminPage`) for page
layout. Also flags the `@wordpress/design-system-mcp` server as the
canonical lookup path for component/token metadata.

Guidance follows the April 2026 WordPress Design System P2 post.

Refs #48242.

* Activity Log: adopt --wpds-* design tokens in SCSS

Swap the remaining hard-coded hex values in activity-actor.scss and
activity-event.scss for WordPress Design System semantic tokens,
keeping the original hex as each `var()` fallback so rendering stays
identical when the tokens aren't in scope:

- `#dcdcde`, `#f0f0f1` → `--wpds-color-bg-surface-neutral-weak`
- `#1e1e1e` → `--wpds-color-fg-content-neutral`
- `#50575e`, `#757575` → `--wpds-color-fg-content-neutral-weak`

The `@wordpress/dataviews` bundled stylesheet already declares the
wpds tokens at :root, so the values take effect immediately rather
than waiting on a separate `@wordpress/theme` provider. Expect a
small shift: the actor icon bubble becomes slightly lighter
(#dcdcde → #f4f4f4) and the two variants of medium grey unify on
#707070. Primary text (#1e1e1e) and event-icon background
(#f0f0f1 → #f4f4f4) are visually unchanged.

Refs #48242.

* Activity Log: link event-description entities to wp-admin screens

The parser already produces typed entity tokens (post, person,
comment, plugin, theme) with the IDs and slugs needed to deep-link
into wp-admin. Wire each one to its matching core screen so users can
jump straight from a log row to the edited object:

- post    → post.php?post={id}&action=edit
- person  → user-edit.php?user_id={id}
- comment → comment.php?action=editcomment&c={id}
- plugin  → plugins.php?s={slug}
- theme   → themes.php?theme={slug}

The admin URL prefix comes from the Initial_State `siteData.adminUrl`
value so non-standard admin paths (subdirectory installs, custom
`admin_url` filters) are respected instead of hard-coding `/wp-admin/`.
Entities with no wp-admin analog (site, backup) stay plain-strong.
Actor-column linking would need a `wp_user_id` in the activity-log
API actor payload and is out of scope here.

Refs #48242.

* Activity Log: route entity tokens to wp-admin, not wordpress.com

Two fixes on top of the entity-link pass:

1. The WPCOM activity-log API wraps typed entity ranges (post,
   person, plugin, theme, comment) in an outer anchor whose href
   points at the `https://wordpress.com/…` equivalent. That outer
   `Link` was winning and sending users to wordpress.com before the
   inner `EntityLink` could emit the local wp-admin URL. Match
   Calypso's Jetpack Cloud guard: if the anchor URL is a
   wordpress.com URL, drop it and render children only so the inner
   renderer takes over. This is what makes post entities link to
   `post.php?post=…` and user entities stop escaping to wordpress.com.

2. Drop the `<strong>` wrapper from entity rendering (both linked
   and unlinked paths) — entity names now render at the same weight
   as surrounding prose; the anchor alone signals interactivity.

Refs #48242.

* Activity Log: link entities that don't surface as typed ranges

Real payloads from /jetpack/v4/activity-log expose entity identity
three ways. The previous pass only handled typed ranges (type:
'post'/'person'/…); this adds the other two:

Section-tagged anchor ranges. The login event's content.ranges is a
single range with type: 'a', url pointing at wordpress.com, and
`section: 'user'` + `id: 1`. The parser now preserves `id` and
`site_id` on link nodes, `buildAdminLink` accepts link/a nodes and
routes by `section`, and the Link renderer tries the local link
before its wordpress.com-drop guard. Net effect: "keoshi successfully
logged in…" now links the username to user-edit.php.

Entry-level object fallback. post__published events return no
content.ranges at all — the post title lives only in the top-level
`object: { type: 'Article', object_id, name }`. Activity now carries
`activityObject` end-to-end, a new `buildObjectAdminLink` maps
Article→post.php and Person→user-edit.php, and ActivityEvent wraps
the whole description in a link when there's a linkable object and
the description has no ranges of its own.

Refs #48242.

* Activity Log: Phase 4 — tier gating and upsell

Gate the full Activity Log behind a paid Backup-enabled plan. Mirrors
Calypso's gating (free tier: last 20 events only, no search/filters/
sort/pagination) while adding a real server-side cap so the limit
can't be bypassed from DevTools.

Server-side:
- `REST_Controller::has_activity_logs_access()` calls WPCOM's
  `/sites/{id}/rewind` endpoint (same signal Jetpack_Backup uses) and
  caches the boolean in a site transient for 5 minutes, keyed on
  blog_id. Fail-closed (no access) on error, with a short 1-min cache
  so transient WPCOM hiccups don't hammer the endpoint.
- `get_activity_log` clamps `number` to 20 and forces `page=1` when
  access is false, regardless of what the caller sent. `wp.apiFetch`
  from the console can't page past the free-tier boundary.
- `Initial_State.siteData.hasActivityLogsAccess` exposes the same
  boolean to the React bundle so the UI starts in the right state on
  page load.

Client-side:
- Read `hasActivityLogsAccess` from the initial state; when false,
  force `config.perPageSizes = [ 20 ]` on DataViews, zero out
  `paginationInfo.totalPages`, and replace the default UI via
  `children={<DataViews.Layout />}` (hides search, filters, sort,
  view-config). Same switches as Calypso at
  wp-calypso:client/dashboard/sites/logs-activity/dataviews/
  index.tsx:201-208.
- `UpsellCallout` renders beneath the table when gated and `logData`
  is non-empty. Title, body copy, and the illustration SVG are a 1:1
  port of Calypso's ActivityLogsCallout. CTA flows through Jetpack's
  standard `useProductCheckoutWorkflow` with
  `productSlug: 'jetpack_security_t1_yearly'` (Security bundle
  unlocks 30 days of history per cloud.jetpack.com/features/comparison)
  and `from: 'activity-log-page-purchase'` as the checkout source.

Closes Phase 4 of #48242.

Refs #48242.

* Activity Log: retarget upsell callout copy at the wp-admin context

Two Calypso-origin strings didn't translate to the self-hosted
Jetpack upgrade flow we actually route to:

- Title "Track every action with Jetpack Activity" → "…with Jetpack
  Activity Log". Matches the product name in wp-admin menus and this
  package's own i18n domain; "Jetpack Activity" read as truncated.
- "Available on WordPress.com paid plans." → "Available on the
  Jetpack Security and Complete plans." The CTA already goes to
  `jetpack_security_t1_yearly`; the plan-availability line now names
  the Jetpack bundles that actually unlock Activity Log (per
  cloud.jetpack.com/features/comparison).

Refs #48242.

* Activity Log: drop the access cache on return from checkout

Pattern borrowed verbatim from Jetpack Social
(`Publicize\Social_Admin_Page::admin_init`): hand WordPress.com a
`redirect_to` that carries `refresh_access=1` + a `_wpnonce`, and on
page load the admin-init hook verifies the nonce and calls
`REST_Controller::clear_access_cache()` before `Initial_State::get_data()`
runs. The Initial_State re-seeds `hasActivityLogsAccess` with a fresh
WPCOM fetch, so a just-upgraded site stops showing the upsell callout
the moment the browser lands back on the Activity Log page — no more
waiting out the 5-minute transient.

- `Jetpack_Activity_Log::REFRESH_ACCESS_NONCE_ACTION` constant +
  `admin_init` nonce check (silent `wp_verify_nonce` instead of
  `check_admin_referer`'s die-on-failure so a stale bookmark doesn't
  show an error page).
- `Initial_State` seeds the nonce under `nonces.refreshAccess`.
- `UpsellCallout` passes `redirectUrl = currentUrl + ?refresh_access=1
  &_wpnonce=<nonce>` to `useProductCheckoutWorkflow` via
  `@wordpress/url`'s `addQueryArgs`.

The cache only gets cleared on upgrade, not on downgrade — a removed
plan still takes up to 5 minutes to reflect. Downgrades are rare
enough and don't hurt as much as the upgrade-lag did.

Refs #48242.

* Activity Log: stop pre-marking the view as modified on load

`isViewModified` used to fall out of a full-view `fastDeepEqual`. When
DataViews' first render normalized anything inside `view.layout` the
compare flipped even though the user hadn't touched a thing, so the
Reset view button was enabled (and the cog carried a dot) on a cold
page load.

Narrow the compare to a signature of the fields the settings-cog
actually exposes — sort, order, properties, density, items-per-page,
filters. Everything else (DataViews layout subfields, future internal
additions) is ignored for the modified-ness decision. Storage still
writes the full stripped view, so restoration keeps its full shape.

Also self-heal on mount: if an earlier session wrote a
"not-really-modified" entry to localStorage (pre-fix), drop it and boot
from the default view.

Refs #48242.

* Activity Log: add a date-range picker above the table

1:1 port of Calypso's `components/date-range-picker/` — Dropdown
toggle with a calendar-icon button, popover with a preset sidebar
(Today, Yesterday, Last 7/30 days, Month/Year to date, Last 12 months,
Last 3 years, Custom), dual-month calendar (collapses to one on
mobile), and start/end InputControls. Five TS files + one SCSS + a
slim local `datetime.ts` lifted from Calypso's utils for
self-contained helpers.

New deps: `@automattic/ui` (for `DateRangeCalendar` + `TZDate`) and
`date-fns` — both already resolved in the monorepo `node_modules`.

Wiring: `dateRange` is client-only state (no localStorage) defaulting
to last-7-days anchored at the site's calendar today, not the
browser's. The range threads into the REST list params (`after` /
`before`) and the group-counts query so filter dropdown totals stay
in sync with the displayed window. `end` stretches to 23:59:59.999
before ISO-encoding so single-day ranges like "Today" aren't empty.
Changing the range snaps pagination to page 1 (same convention as
perPage/sort/filters/search already use in `onChangeView`).

Only renders when `hasActivityLogsAccess` is true — on the free tier
the row is hidden, matching the rest of the Phase 4 gating.

Package-level `eslint.config.mjs` relaxes a few rules for the ported
directory (`jsdoc/require-description`, `react/jsx-no-bind`, and
`@wordpress/no-unused-vars-before-return`) so upstream re-syncs stay
mechanical rather than requiring a JSDoc audit on every refresh.

Refs #48242.

* Activity Log: move date-range picker into the AdminPage header

Two polish fixes for the picker landed in the previous commit:

1. Move it into the page header via the admin-ui `actions` slot
   (AdminPage threads `actions` straight into `@wordpress/admin-ui`'s
   `Page`), so the picker sits alongside the title/subtitle instead
   of floating on its own row above the table. Matches MSD's layout
   for the logs pages.

2. Import `@automattic/ui/style.css`. The JS bundle for
   `@automattic/ui`'s `DateRangeCalendar` doesn't carry its own
   styles — the published package ships them as a separate entry in
   its `exports` map. Without it the calendar cells render with
   wp-admin's default button styling (each day is a boxed button
   outline); with it we get the clean text-only numbers that Calypso
   and MSD both show.

Refs #48242.

* Activity Log: add tracks events for user interactions

Wires `@automattic/jetpack-analytics` (already the canonical tracker
for Jetpack wp-admin packages — Backup, Search, Publicize, etc.) into
the Activity Log page so product / growth can see which affordances
people actually use. Small local `use-analytics` hook initializes the
tracker with the connected WPCOM user identity, same pattern as
`projects/packages/backup/src/js/hooks/useAnalytics.js`.

Seven events, all namespaced `jetpack_activity_log_*`:

Calypso parity (same breakdown as wp-calypso:client/dashboard/sites/
logs-activity/dataviews/index.tsx:146-174):
- `per_page_changed` — `{ per_page }`
- `filter_changed` — `{ num_groups_selected, num_total_activities_selected, group_<id>: bool }`
- `search` — `{ has_query }` (boolean so the query text never leaks)
- `page_changed` — `{ page }`

New for this port:
- `date_range_changed` — `{ days_in_range }` (sidecar date picker)
- `reset_view_click` — `{}` (wraps DataViews' onReset)
- `upsell_cta_click` — `{ source: 'free_tier_callout' }` (paid-plan CTA)

Deferred: sort change (Calypso doesn't track), entity-link clicks in
the description (too noisy), Phase 5 restore-point click (blocked on
#48236).

Also refreshes the stale header comment in ActivityLog/index.tsx so
it reflects the current scope-simplifications set (drops "no date
range picker", "no analytics events"; keeps localStorage, unlinked
actor, disabled backup action).

Refs #48242.

* Activity Log: fix CI — stub CHANGELOG.md, changelog/.gitkeep, TS error

Three tightly-related CI fails on #48244 and #48264:

1. `Changelogger validity` — \`projects/packages/activity-log/CHANGELOG.md\`
   didn't exist. Seeded an initial file pointing at the 0.1.0-alpha
   pre-release version this branch is cutting.
2. `Project structure` — lint requires each package's \`changelog/\`
   folder to carry a \`.gitkeep\` so Release doesn't remove the
   directory when the queue is empty.
3. `Type checking` — tsgo types `__()`'s return as a branded
   `TransformedText<…>`, which made the later
   `actorName = name || actorName` reassignment fail type-check.
   Annotate the variable as `string` so the widening happens at
   declaration. (`Build all projects` fail was the same TS error
   cascading through the plugin compile; it clears with this fix.)

Refs #48242.

* Activity Log: stop the prod i18n check flagging @automattic/ui strings

The `DateRangeCalendar` component we pull in from `@automattic/ui`
calls `__('Date calendar')` / `__('%s, selected')` / etc. without a
textdomain argument. Dev builds don't care; the `Build all projects`
CI job runs the production build, which trips
`i18n-check-webpack-plugin`'s strict validation and fails with seven
`domain argument (index 2) is missing` errors.

Same recipe the `@wordpress/*` bundled packages use in
`jetpack-webpack-config`'s `BundledWpPkgsTranspileRules`: run the
`@automattic/babel-plugin-replace-textdomain` babel plugin over
`@automattic/ui` source at bundle time so every bare `__()` gets our
`jetpack-activity-log` textdomain wired in.

Verified locally with `NODE_ENV=production BABEL_ENV=production pnpm
run build-client` — clean compile.

Refs #48242.

* Activity Log: address engineering-review findings from #48244

Three real fixes + two documentation passes in response to @CGastrell's
review.

1. Drop `staleTime: Infinity` on the TanStack client (src/js/index.js).
   The activity log is append-only; an infinitely-stale cache meant new
   events landing upstream while the page stayed open never surfaced.
   Also reproduced the "Last 7 days missing today's event, Last 30 days
   includes it" symptom: the Last-7 query was cached before the event
   existed, Last-30 re-fetched after, and flipping back served the
   pre-event snapshot. 60s + `refetchOnWindowFocus` strikes the right
   balance.
2. Default `view.page` to 1 in `listParams` so the key is stable
   across the initial render → first-user-interaction transition
   (`view.page` goes `undefined` → `1`). Eliminates one WPCOM round-
   trip per page load.
3. Strip `filters` unconditionally in `stripTransient` — previously
   only empty-array filters were dropped, so a "Plugins" selection
   survived across reloads even though the PR described filters as
   transient. Matches `search`'s existing behavior.

Plus three comment-only improvements flagged in the nitpicks list:

- Free-tier clamp in `get_activity_log()` documents that the in-place
  `set_param` is deliberate (security property: no downstream filter
  can undo the cap).
- `get_activity_log_group_counts()` explains why counts are
  deliberately *not* tier-clamped (they're cosmetic, and stable
  full-history counts keep the filter dropdown from flickering as
  users type).
- `Initial_State::get_data()` notes the synchronous WPCOM call on
  cache miss and its ~200ms–2s render-blocking window.
- `use-activity-log.ts::buildPath` notes that `apiFetch` /
  `@wordpress/url` re-serialize arrays as indexed brackets
  (`group[0]=plugin`) on the wire — both forms round-trip through
  the REST controller.

Refs #48242, review in #48244.

* Activity Log: offer DataViews' Activity layout alongside Table

`@wordpress/dataviews@14.1.0` ships an Activity timeline layout as a
first-class option alongside Table / List / Grid (see the Gutenberg
storybook story `dataviews-dataviews--layout-activity`). For an
append-only event log, the timeline shape reads more naturally than a
dense table — so add it as a toggle rather than a replacement. Users
flip between the two via the layout switcher in the cog popover.

Minimal wiring for the first pass:
- `defaultLayouts.activity.titleField: 'event'` — reuses the existing
  composite `ActivityEvent` render (icon + title + formatted
  description stacked) as the timeline item's title block.
- Leaves `mediaField` / `descriptionField` unset. The layout falls
  back to its default bullet + stacks Date/User below as "other
  fields" with visually-hidden labels.
- Date, User, and filter state (activity type) stay unchanged — the
  cog still exposes them, both layouts render them.

Visual polish (splitting the event-icon into a dedicated mediaField,
moving the date into a dedicated descriptionField, etc.) is a follow-
up once this lands and we can compare side-by-side.

Refs #48242.

* Activity Log: wire the Activity layout's media/title/description slots

The first pass reused the composite `event` field as both
`view.titleField` *and* one of the `view.fields`, which made
DataViews render the event body twice (once in the title block, once
as an "other fields" row) and kept the gridicon trapped inside the
title stack rather than in the layout's left-hand media slot.

Fix: split `ActivityEvent` into three exported sub-components
(`ActivityEventIcon`, `ActivityEventTitle`,
`ActivityEventDescription`), expose each as its own DataViews field
(`event_icon`, `event_title`, `event_description`), and update
`defaultLayouts.activity` so those three sit in `mediaField` /
`titleField` / `descriptionField` respectively while `view.fields`
lists only Date + User (rendered as the subdued metadata row below
the title). Table layout keeps the composite `event` field
unchanged.

DataViews' `getHideableFields` helper already excludes the three
slot-bound fields from the Properties toggle list, so the cog
popover still shows a clean Title / Media / Description lock group
on top of the regular Date / User toggles.

Refs #48242.

* Activity Log: address layout-review feedback from #48244

Four related fixes, all introduced by the commit that added the Activity
layout toggle (DataViews' titleField/mediaField/descriptionField slots):

- Table titles stopped being dark. The event-title class sat on a
  <span> wrapper, which matched the
  .site-activity-logs__event-content > span { color: grey } rule
  meant for the description. Move the class onto the <strong> itself
  so the selector only catches the description's span.

- Activity-layout icons weren't perfect circles. Our
  .site-activity-logs__event-icon class sets min-width: 32px, which
  beat DataViews' width: 100% rule (25px at balanced density) and
  produced a 32x25 rounded-rect instead of a circle. Scope the tile
  styling to the Table layout's composite .site-activity-logs__event
  wrapper; in the Activity layout the same <ActivityEventIcon> is
  rendered standalone and now picks up DataViews' own circular bullet
  styling.

- Columns broke after Activity -> Table. DataViews' layout switcher
  does onChangeView({ ...view, type, ...defaultLayouts[type] }), so
  any key *not* present in the target layout's defaults carries over.
  Activity sets titleField/mediaField/descriptionField, and the Table
  layout separately renders a composite "primary column" when those
  are set, producing a ghost Title column before the regular Event
  column. Move default layouts into ./views.ts and explicitly set
  those slot refs (and groupBy) to undefined on the Table variant so
  the spread clears them. Also re-declare the Table's column-width
  styles in the default so the switcher restores them (it deletes
  view.layout on every switch).

- Day grouping in the Activity timeline. Add a hidden published_date
  field that returns the formatted calendar day in the site's
  timezone, and wire groupBy: { field: 'published_date' } into the
  Activity defaults. Events on the same day now collapse under a
  single "April 24, 2026" header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Activity Log: center the Activity-layout meta row and anchor the actor right

DataViews renders the per-item "other fields" row as a flex container
with the default `align-items: stretch`, which left-aligns the date
text to the top of the cell while the actor's HStack (icon + name)
sits vertically centered — producing a visible baseline mismatch in
the screenshot-shared feedback. Scope an `align-items: center` onto
`.dataviews-view-activity__item-fields` so every cell shares a single
midline, and push the trailing cell (actor) to the far right with
`margin-inline-start: auto`.

Keeping the rule inside `.jp-activity-log__dataviews-wrapper` so it
doesn't leak to any other DataViews instance in the wp-admin shell.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Activity Log: track layout toggles between Table and Activity

Layout switches flow through the same DataViews onChangeView callback
as every other view mutation, but until now we only fired events for
the per-page / filter / search / page / date-range / reset cases.
The Jetpack port is the only Activity Log surface that exposes the
DataViews Activity layout alongside the default Table, so add a
`jetpack_activity_log_layout_changed` event (with the resolved
`next.type` as the `layout` prop) so we can tell which layout users
actually stick with.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Activity Log: address package-structure review feedback from #48244

Four follow-ups from @anomiex's review of the package layout, plus a
nitpick from @CGastrell's earlier walkthrough.

1. Drop the `V0001` namespace suffix on the three package PHP classes
   and the consuming `use` in the Jetpack plugin. The `VXXX` pattern
   was lifted from Backup, where it grew out of an autoloader-version
   incident (see peaFOp-2ar-p2). For a brand-new package with no
   incident history yet, the suffix just adds friction. Package_Version
   stays unsuffixed for the same reason it does in Backup — the
   `jetpack_package_versions` filter expects to find the class at a
   stable name across upgrades.

2. Replace the autoloaded top-level `actions.php` with an
   `add_filter()` call inside `Jetpack_Activity_Log::initialize()`.
   Matches the videopress / connection / search / sync / stats
   convention. The composer `autoload.files` entry is dropped along
   with the file.

3. Site-scope the localStorage key in `usePersistentView`. The single
   `'jetpack-activity-log:view'` key meant an admin who manages
   multiple Jetpack-connected sites in the same browser shared one
   view across all of them; key now includes the WPCOM blog ID seeded
   by Initial_State and falls back to `default` when the global is
   absent (storybook/tests).

4. Delete the `.babelrc`. The package has no Jest tests today (the
   follow-up tests PR #48264 only adds PHPUnit), so the
   `@babel/preset-env` config it carried wasn't being consumed.
   `babel.config.js` continues to drive the webpack build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Activity Log: default to the Activity timeline layout

Flip DEFAULT_VIEW from Table → Activity so the page boots into
DataViews' built-in timeline (grouped by day, icon + title +
description in a left rail) instead of the table. Table is still
available from the cog popover's layout switcher.

The Activity layout is closer to the "every change in one searchable
timeline" framing in the page subtitle and matches the pattern users
see in WordPress.com's logs surfaces. Users with persisted view state
keep what they had — only fresh loads pick up the new default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Activity Log: hide the cog's "modified" indicator dot

DataViews renders a small dot on the view-options cog whenever the
current view differs from the default (via our `onReset` prop). The
same signal also gates the "Reset view" button inside the cog
popover, which is enough on its own — the dot just adds visual noise
on a page where toggling sort order, density, or filters is routine.

Hide the indicator via SCSS scoped to our wrapper. The reset
functionality is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Activity Log: tooltip the disabled "Manage backup" action

The Phase 5 stub renders "Manage backup" as a disabled primary
action so its column space stays committed and the planned feature
is discoverable. Without any hint, users see a dead button and have
to guess why.

DataViews' `Action` type has no tooltip prop and the action button
is rendered by the library, not us — so attach a native `title`
attribute via a small effect rooted on the dataviews wrapper. The
matching is keyed on the localized "Manage backup" label so other
disabled actions (if any land later) don't get the same hint by
accident. A MutationObserver re-applies the title after pagination
or filter changes when DataViews replaces the row DOM.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Activity Log: collapse empty description rows

DataViews' Activity layout always renders the descriptionField slot
wrapper, even when our `ActivityEventDescription` returns `null` for
events without description text (e.g. "User removed", which has only
a title). The empty wrapper still participates in the column stack's
gap, leaving a visible blank line under the title.

Hide it via `:empty` so the row collapses tightly. Scoped to the
Activity Log wrapper so we don't disturb other DataViews instances.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Activity Log: collapse changelog entries to single end-user-facing notes

Per anomiex review on #48244 — package changelog dirs aren't a
commit log; consolidate the 30 iteration entries into one initial
release note, and rewrite the jetpack-plugin entry to describe the
feature instead of the package wiring.

* admin-page-layout mixin: extend flex chain into AdminPage's Container/Col

`<AdminPage>`'s title-branch wraps children in
`<Container fluid horizontalSpacing={0}><Col>{children}</Col></Container>`,
which is `display: grid` plus a content-sized grid cell. The mixin's
`flex: 1 1 auto; overflow: auto` already bounded Container, but the chain
broke at Col — any inner `flex: 1 1 auto; min-height: 0` on a consumer's
wrapper was inert under a non-flex parent. DataViews-style pages (Activity
Log) ended up letting their content grow to its natural size and the outer
Container scrolled the whole thing instead of letting DataViews's own
`.dataviews-layout__container` scroll the table body.

Force the outer Container/Col pair to flex column so consumers can fill
their bounded slot. Form-style pages (My Jetpack, Newsletter, Social)
keep working: their content stays content-sized via default
`flex: 0 1 auto` and Container's `overflow: auto` still catches anything
taller than the slot.

Verified on a Jurassic Ninja site at 1440x900 and 1440x500 across:
Activity Log (DataViews internal scroll now lives at .dataviews-layout__container,
header + toolbar pinned), My Jetpack, Social, Newsletter, Forms — no
regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Activity Log: harden the free-tier server-side gate

Two follow-ups from @douglas's review of #48244 to
projects/packages/activity-log/src/class-rest-controller.php.

1. Free-tier bypass via filter inputs. The previous clamp limited only
   `number` and `page` — `after`/`before`/`text_search`/`group`/
   `not_group` were forwarded to WPCOM verbatim. A free-tier caller via
   DevTools could date-walk the entire history 20 rows at a time by
   resetting `before` to the timestamp of the oldest visible event, or
   full-text-search the full log via `text_search`. Now `get_activity_log()`
   nulls out everything outside `FREE_TIER_ALLOWED_PARAMS`
   (`number` / `page` / `sort_order`) before delegating to `proxy_get`,
   so the response is bounded to "the 20 most recent events overall" —
   the same dataset the locked-down UI shows.

2. Failure-cache TTL of 60s downgraded paying admins for too long. A
   one-second WPCOM blip persisted "no" for a full minute; long enough
   that an admin who refreshed during the window saw the full free-tier
   UI flip-flop. Cut to 10s — still enough to keep a flapping endpoint
   from getting hammered, short enough that paid customers recover almost
   immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Activity Log: use esc_url_raw for the JSON-bound adminUrl

`esc_url()` is for HTML-attribute contexts — it escapes `&` to
`&amp;`. The `adminUrl` value flows through `wp_json_encode()` and
into JS that concatenates it into hrefs, so an HTML-escaped
ampersand would survive into the final URL. Today `admin_url()`
rarely contains `&` so the bug is invisible, but the inconsistency
with the sibling `WP_API_root` (which already uses `esc_url_raw()`)
is the kind of thing that bites later. Drop the now-unused import
while we're here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Activity Log: tidy two React hook side-effects

Two small follow-ups from review:

* `usePersistentView` was calling `writePersistedView( null )` from
  inside the `useState` lazy initializer for the self-heal branch.
  That's a side effect during render — React 18 strict-mode double-
  invokes lazy initializers and would write twice. Move the cleanup
  into a one-shot mount `useEffect`; the lazy initializer now only
  picks the right starting view.

* `useAnalytics` re-called `jetpackAnalytics.initialize()` every time
  any consumer's effect re-fired with the same identity. Guard with a
  module-level `identifiedFor` flag so the singleton is only re-
  identified when the (id, login) pair actually changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Activity Log: mark the Manage-backup tooltip hack as #48236 follow-up

The `MutationObserver` that retro-fits a `title="Coming soon"` onto
the disabled "Manage backup" row action is a stopgap until #48236
lands — at which point the action becomes a real enabled link and
the entire effect goes away. Add a TODO with the issue reference so
the next reader knows it's intentionally temporary, not a pattern
to extend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Activity Log: align tanstack range; bump my-jetpack changelog

Two metadata fix-ups from review:

* `@tanstack/react-query` was pinned to `5.90.8` in the new package's
  package.json while existing siblings (backup, my-jetpack, mu-wpcom)
  use `^5.15.5`. Both ranges are satisfied by 5.90.8 today, but a
  fixed pin on a fast-moving lib while siblings carry a caret risks
  future split resolutions. Switch to `^5.90.8` — the resolver still
  picks 5.90.8 (no downgrade), and now every consumer's range narrows
  to the same minimum.

* Bump the my-jetpack changelog entry from `patch` to `minor`. The
  removal drops the public `Automattic\Jetpack\My_Jetpack\Activitylog`
  class — even though only the package's own `Initializer` calls it
  in-tree, it's part of the package's exported namespace, so a
  consumer outside the monorepo could in principle have wired
  `Activitylog::init()` into their own boot path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* AdminPage: add `unwrapped` prop for full-bleed pages

Adds an opt-in `unwrapped` prop to `<AdminPage>` (default false). When
true, children render directly inside admin-ui's `<Page>` instead of
inside the default
`<Container fluid horizontalSpacing={0}><Col>{children}</Col></Container>`
wrap. Use for full-bleed surfaces (DataViews-based admin pages, full-app
dashboards) that own their own scroll/layout model and don't want the
outer Container's grid layout to break their flex chain.

Activity Log opts in: with `unwrapped`, `.admin-ui-page` directly contains
the wrapper, the mixin's `overflow: auto` slot lands on the wrapper, and
DataViews's own `.dataviews-layout__container` handles internal scroll
for the table body — header, date picker, and DataViews toolbar stay
pinned on short viewports.

Verified on a Jurassic Ninja site at 1440x900 and 1440x500.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: ilonagl <ilona.jaudzemyte@automattic.com>
Co-authored-by: Christian Gastrell <cgastrell@gmail.com>
Co-authored-by: Douglas <douglas.henri@automattic.com>

packages/videopress@0.36.6

Toggle packages/videopress@0.36.6's commit message
Changelog and readme.txt edits.

packages/search@0.57.0

Toggle packages/search@0.57.0's commit message
Changelog and readme.txt edits.

packages/publicize@0.78.2

Toggle packages/publicize@0.78.2's commit message
Changelog and readme.txt edits.

packages/paypal-payments@0.6.17

Toggle packages/paypal-payments@0.6.17's commit message
Changelog and readme.txt edits.

packages/newsletter@0.8.5

Toggle packages/newsletter@0.8.5's commit message
Changelog and readme.txt edits.

packages/my-jetpack@5.35.0

Toggle packages/my-jetpack@5.35.0's commit message
Changelog and readme.txt edits.

packages/masterbar@0.27.22

Toggle packages/masterbar@0.27.22's commit message
Changelog and readme.txt edits.

packages/forms@7.20.0

Toggle packages/forms@7.20.0's commit message
Changelog and readme.txt edits.

packages/external-media@0.8.14

Toggle packages/external-media@0.8.14's commit message
Changelog and readme.txt edits.