Skip to content

Add FOSSE admin UI with Connection_Provider architecture#27

Merged
kraftbj merged 21 commits intotrunkfrom
add/onboarding-setup-ux
Apr 24, 2026
Merged

Add FOSSE admin UI with Connection_Provider architecture#27
kraftbj merged 21 commits intotrunkfrom
add/onboarding-setup-ux

Conversation

@RCowles
Copy link
Copy Markdown
Contributor

@RCowles RCowles commented Apr 23, 2026

Summary

Implements the FOSSE admin UI per the onboarding-setup-ux SDD (merged in #25). Replaces the bundled plugins' fragmented settings pages with a single FOSSE-owned admin surface.

  • Connection_Provider interface + registry — extensible abstraction so future protocols (Bluesky, Leaflet.pub, etc.) register as providers without restructuring the UI. Third-party plugins can also register providers via the fosse_register_providers hook.
  • Top-level FOSSE menu with Setup and Status sub-pages. Hides all bundled-plugin admin entries (AP Settings, Dashboard, Users submenus; Atmosphere Settings) at priority 99. Pages remain accessible by direct URL for power users.
  • AP_Provider with option projection — inline configuration for actor mode and post types. Stores fosse_ap_* options and projects them to AP via pre_option_* filters (priority 20, so AP's constant-based overrides at priority 10 take precedence). Status card shows actor mode, post types, fediverse address, and follower count.
  • PHPCS src/ exclusion from WordPress.Files.FileName sniff for PascalCase namespaced files.

What's not in this PR

  • Bluesky_Provider (Task 5) — blocked on upstream Atmosphere PRs for a redirect_uri filter and transient-persisted connect notices. See sdd/onboarding-setup-ux/planned-decisions.md for details.
  • Bluesky_Provider tests — will ship with Task 5.
  • Onboarding wizard — shipping in a separate stacked PR.
  • SDD docs update (Task 10) — will follow once implementation is complete.

SDD plan task mapping

Task Status Commit
1: Connection_Provider interface + registry Done 3efb10d
2: Menu + fosse.php wiring Done c7eb049
3: AP_Provider + option projection Done 72c86e1
4: Upstream Atmosphere PRs Blocked (external)
5: Bluesky_Provider Blocked on Task 4
5.5: Onboarding wizard Separate PR
6: Status_Page Done c7eb049
7: Admin CSS Done c7eb049
8: Tests (Registry + AP_Provider) Done 3efb10d, 72c86e1
9: PHPCS exclusion Done 08c773f
10: SDD docs update After implementation

Deviation from SDD

Menu suppression now also removes activitypub-blocked-actors-list and the Extra Fields CPT redirect under Users, which were not listed in the original SDD plan (@kraftbj's review only found the entries in class-menu.php lines 57-79). Discovered during Playground testing.

Test plan

  • composer run-script test-php — 39 tests pass (Registry: 5, AP_Provider: 21, existing: 13)
  • composer run-script lint-php — clean
  • pnpm run format:check — clean
  • Playground: FOSSE menu appears with Setup and Status sub-pages
  • Playground: no AP or Atmosphere entries in Settings, Users, or top-level menu
  • Playground: AP Setup section renders with actor mode, post types, fediverse address
  • Playground: saving AP settings persists and reflects on AP's own settings page (option projection)
  • Playground: Status page shows AP card with green indicator and correct data

Refs: DOTCOM-16793, DOTCOM-16800, DOTCOM-16802, DOTCOM-16803

RCowles added 4 commits April 23, 2026 12:16
FOSSE uses namespaced classes with classmap autoloading, where
PascalCase filenames matching class names is the standard PHP
convention. The Jetpack PHPCS ruleset enforces class-*.php
lowercase-hyphenated filenames, which is the WordPress convention
for non-namespaced code. The existing src/Bundled/Bootstrap.php
already uses PascalCase; this makes the exclusion explicit.
Extensible provider abstraction for federation protocols. Each
provider implements get_slug, get_name, is_available, get_status,
render_setup_section, render_status_card, and register_hooks.

Connection_Provider_Registry is a static registry with register,
get_providers, get_provider, and reset. Duplicate slugs silently
ignored (first wins). Setup and Status pages will iterate this
registry rather than hardcoding protocol-specific sections.

Includes unit tests for all registry operations.
Top-level FOSSE menu with Setup and Status sub-pages. Menu registers
at priority 9, then hides all bundled-plugin admin entries at
priority 99: AP Settings and Dashboard, AP Users submenus (Followers,
Following, Blocked Actors, Extra Fields), and Atmosphere Settings.
Pages remain accessible by direct URL for power users.

Setup_Page and Status_Page iterate Connection_Provider_Registry to
render provider sections and status cards. Status template includes
a summary row with attention state when not all protocols are
connected. Admin CSS provides status indicators, card grid, and
summary bar styling, leaning on native WP admin classes.

Wired up in fosse.php behind an is_admin() + class_exists() guard.
ActivityPub provider for the FOSSE admin UI. Self-registers on
fosse_register_providers with a class_exists guard.

Setup section renders inline configuration: actor mode radio, post
type checkboxes, fediverse address display, and a link to the
advanced AP settings page.

Option projection pattern: FOSSE stores its own options
(fosse_ap_actor_mode, fosse_ap_support_post_types) and projects
them to AP via pre_option_* filters at priority 20. AP's own
constant-based overrides at priority 10 take precedence. When the
FOSSE option is absent, AP's stored value is read from the DB.

Status card shows actor mode, post types, fediverse address, and
follower count (when the AP Followers API is available).

Includes unit tests for metadata, status shape, option projection,
filter priority, and projected status reflection.
Copilot AI review requested due to automatic review settings April 23, 2026 19:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a FOSSE-owned WordPress admin UI (Setup + Status) built around an extensible Connection_Provider architecture, and wires in an initial ActivityPub provider with option projection into the bundled ActivityPub plugin.

Changes:

  • Introduces Connection_Provider + Connection_Provider_Registry and a new FOSSE top-level admin menu with Setup/Status pages.
  • Implements AP_Provider (settings form + status card) and projects FOSSE options into ActivityPub via pre_option_* filters.
  • Adds admin CSS, PHPUnit coverage for the registry/provider behavior, and relaxes PHPCS filename sniffing for src/.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
fosse.php Boots the new admin UI in wp-admin and initializes the ActivityPub provider.
src/Admin/Connection_Provider.php Defines the provider contract for current/future federation backends.
src/Admin/Connection_Provider_Registry.php Implements a static registry for provider instances.
src/Admin/Menu.php Registers FOSSE menu pages, hides bundled-plugin menus, enqueues admin CSS.
src/Admin/Setup_Page.php Renders the Setup page template with registered providers.
src/Admin/Status_Page.php Renders the Status page template with registered providers.
src/Admin/AP_Provider.php Adds ActivityPub setup UI, option projection, and status rendering.
src/Admin/templates/setup-page.php Setup page layout and provider section rendering.
src/Admin/templates/status-page.php Status summary + provider status card rendering.
src/Admin/assets/css/admin.css Styling for setup sections and status cards/summary.
tests/php/Admin/Connection_Provider_RegistryTest.php Tests registry behavior (register/get/reset/dupes).
tests/php/Admin/AP_ProviderTest.php Tests AP_Provider metadata, status shape, and option projection behavior.
.phpcs.xml.dist Excludes src/ from the WordPress filename sniff.

Comment thread src/Admin/class-menu.php
Comment thread src/Admin/templates/setup-page.php
Comment thread src/Admin/AP_Provider.php Outdated
Comment thread src/Admin/AP_Provider.php Outdated
RCowles added 2 commits April 23, 2026 12:25
Add @before/@after annotations alongside PHP 8 attributes to satisfy
the Jetpack PHPUnit compatibility sniff.

Update bundled-backends E2E test: the ActivityPub menu is now hidden
by FOSSE's menu suppression, so assert it is hidden and FOSSE's own
menu is visible instead.
Fediverse address now shows the user's webfinger in actor mode, the
blog's in blog mode, and the user's (with blog fallback) in
actor_blog mode. Previously always used the blog actor regardless.

Follower count now uses the blog actor ID in blog mode and shows
both author and blog counts in actor_blog mode. Previously always
counted against get_current_user_id().
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment thread src/Admin/AP_Provider.php Outdated
Cast $_POST['fosse_ap_support_post_types'] to array before passing
to array_map, preventing a TypeError if the input is malformed.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Comment thread src/Admin/AP_Provider.php Outdated
Comment thread src/Admin/class-ap-provider.php Outdated
Comment thread src/Admin/class-ap-provider.php Outdated
Comment thread src/Admin/templates/status-page.php
In pure actor mode, if User::from_wp_user() fails, return empty
string instead of falling through to the blog webfinger. Blog
fallback is now only used in actor_blog mode.
@RCowles RCowles requested a review from kraftbj April 23, 2026 20:11
@RCowles
Copy link
Copy Markdown
Contributor Author

RCowles commented Apr 23, 2026

@kraftbj Ready for review. This is the architectural foundation for the onboarding UX, not the polished UI itself. The goal here is getting the right pieces in place: provider abstraction, option projection, menu suppression, and page shells that everything else builds on top of.

The Copilot review caught a few real issues (actor-mode-aware fediverse address and follower count, input type guard) which are fixed. It also suggested some overly defensive validation on the projection filters that I declined with reasoning inline.

Bluesky_Provider (Task 5) is blocked on upstream Atmosphere PRs. I can take a first pass at this.

Copy link
Copy Markdown
Contributor

@kraftbj kraftbj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up: this is a static review — I haven't user-tested any of this yet, so treat the severity calls as code-reading confidence, not behavior-confirmed. Requesting changes on one real correctness issue; the rest is commentary.

Architecturally the PR lands where it should. The Connection_Provider abstraction, option-projection pattern, and bundled-menu suppression all read well; nonces/caps/escaping are clean throughout handle_save and the templates; Copilot's four real bugs all got the right fixes (82e65a8 / 34bfd48 / 2314839) and the three declines are defensible. The one must-fix is that projection silently doesn't run outside wp-admin, which defeats the pattern.

Must fix before merge

  1. is_admin() gates the projection filters — AP reads its own options on WebFinger, outbox, REST, and cron. Inline on fosse.php:132. This is the big one.

Major — needs a plan, not necessarily this PR

  1. PascalCase filenames block WP.com/Simple inclusion. PSR-4 isn't an option over there; WPCS filenames (class-foo.php) are required. Inline on .phpcs.xml.dist.

Minor / follow-up

  1. Provider registration race when providers arrive after the do_action fires (Menu.php:30). Solvable alongside #1.
  2. Extra Fields submenu suppression matches a rendered URL string; brittle but low-risk (Menu.php:99).
  3. BLOG_USER_ID constant-absence fallback to 0 can surface a bogus row (AP_Provider.php:377).
  4. In pure actor mode the follower-count row shows the viewing admin's count under a site-wide label (AP_Provider.php:408).

Test gaps worth filling

  • Projection exercised in a non-admin request context (would have caught #1).
  • handle_save capability + nonce denial paths, plus allowlist-rejection for actor mode and post types.
  • E2E assertion that direct URLs to the suppressed bundled pages still work — the PR description promises this, but nothing pins it.

In short: fix # 1, pick a path on filenames/autoloader (Jetpack autoloader is the obvious one), and the rest can roll into follow-ups.

Comment thread fosse.php Outdated
Comment thread .phpcs.xml.dist Outdated
Comment thread src/Admin/Menu.php Outdated
*
* Providers call Connection_Provider_Registry::register( $this ) here.
*/
do_action( 'fosse_register_providers' );
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provider registration race: the do_action('fosse_register_providers') fires as soon as Menu::register() runs, and the registry is snapshotted immediately after in the foreach. Any provider whose bootstrap only becomes available on init / admin_init (e.g. classes not yet autoloaded, or a third-party provider that needs plugins_loaded itself) will miss the window and silently never register its pre_option_* or admin_post_* hooks.

Less urgent than the is_admin() issue in fosse.php:132, but worth solving at the same time — if you move registration to plugins_loaded, make sure third-party providers can still land at init if they need to.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Largely addressed by the Provider_Loader refactor (splitting provider hooks out of Menu). Both first-party providers init in the same file, so there is no race today.

For third-party providers the timing question is real — if boot() fires at plugin include time and a third-party hooks fosse_register_providers on plugins_loaded, they miss it. Worth solving when third-party registration becomes a real use case. Deferring boot() to a plugins_loaded hook at a defined priority would be the fix, but it changes init ordering for everything else in fosse.php and is not worth the risk for two first-party providers.

Comment thread src/Admin/class-menu.php
Comment thread src/Admin/AP_Provider.php Outdated
</tr>
<?php
} else {
$count = \Activitypub\Collection\Followers::count( get_current_user_id() );
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In pure actor mode this renders Followers::count( get_current_user_id() ) under the site-wide "Followers" label — so on a multi-author site two admins looking at the Status page see different numbers for the same "site." Not a correctness bug, but it misrepresents what's being shown.

Two reasonable paths: relabel to "Your followers" when $mode === 'actor', or sum across users with an AP-enabled role. The relabel is cheaper and honest.

@RCowles
Copy link
Copy Markdown
Contributor Author

RCowles commented Apr 23, 2026

Bluesky_Provider (Task 5) is blocked on upstream Atmosphere PRs. I can take a first pass at this.

Ref: Automattic/wordpress-atmosphere#33

RCowles added 3 commits April 24, 2026 08:38
Provider_Loader::boot() fires fosse_register_providers and calls
register_hooks() on each available provider. Runs unconditionally
so option-projection filters are active on every request (REST,
WebFinger, cron), not just wp-admin.

Menu::register() is now admin-only: menu pages, bundled-menu
suppression, and CSS enqueue.

Includes regression test verifying projection works without
is_admin() context.
Skip blog-count branch when BLOG_USER_ID constant is absent instead
of falling back to user ID 0 and querying a bogus row.

Label now says "Your Followers" in actor mode and "Your followers: X,
Blog: Y" in actor_blog mode, so the count is honest about what it
represents on multi-author sites.

Add source reference comment on the Extra Fields submenu suppression
noting it mirrors bundled AP's class-menu.php:94-98.
PHPUnit: five new tests for handle_save() covering valid saves,
invalid actor mode rejection, invalid post type filtering, and
non-array post type input handling.

E2E: two new Playwright tests asserting that suppressed bundled
AP and Atmosphere settings pages remain accessible by direct URL
(the menu entries are hidden but the pages still load).
Add automattic/jetpack-autoloader as a runtime dependency and switch
fosse.php to load vendor/autoload_packages.php. This positions FOSSE
for WP.com/Simple inclusion and enables safe version resolution if
AP/Atmosphere are later extracted into shared Composer packages.

Rename all src/ files to WordPress class-*.php / interface-*.php
naming convention and drop the PHPCS src/ exclusion from
WordPress.Files.FileName. Files now pass the sniff natively.

Update build-zip sanity check to verify autoload_packages.php is
present in the built archive.
Copy link
Copy Markdown
Contributor

@kraftbj kraftbj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Architecture is solid. The provider abstraction, option-projection pattern, and the Provider_Loader split (fixing the is_admin gate) are all well-done. Copilot's real bugs got fixed correctly, the jetpack-autoloader switch was the right call, and test coverage on the projection/registry/loader paths is good.

Requesting changes on one correctness issue (actor mode change doesn't propagate to federation) and a couple of UX honesty items. The rest is follow-up or suggestions. Inline comments have severity labels.

Comment thread src/Admin/class-ap-provider.php Outdated
Comment thread src/Admin/class-ap-provider.php Outdated
Comment thread src/Admin/class-ap-provider.php Outdated
Comment thread src/Admin/class-ap-provider.php Outdated
Comment thread fosse.php
Comment thread src/Admin/class-ap-provider.php
Comment thread src/Admin/class-connection-provider-registry.php
Comment thread src/Admin/class-ap-provider.php
Actor mode changes now fire update_option_activitypub_actor_mode so
AP's scheduler propagates the new topology to remote followers.
Status indicator is wired to get_status()['connected'] instead of
being hardcoded. Invalid actor mode submissions show an error notice
instead of a false success message.
@RCowles RCowles force-pushed the add/onboarding-setup-ux branch from af214e3 to 7af8576 Compare April 24, 2026 21:08
Comment thread src/Admin/class-menu.php Outdated
remove_submenu_page( 'options-general.php', 'atmosphere' );

// AP top-level Dashboard page (gated by activitypub_reader_ui).
remove_menu_page( 'activitypub-social-web' );
Copy link
Copy Markdown
Contributor

@kraftbj kraftbj Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove_menu_page('activitypub-social-web') is a no-op. AP registers Social Web via add_dashboard_page() (bundled/activitypub/includes/wp-admin/class-menu.php:43), which is a thin wrapper around add_submenu_page('index.php', ...). remove_menu_page() only walks the top-level $menu global — it can't remove a submenu of index.php.

Invisible in practice right now because activitypub_reader_ui defaults to '0', but the moment anyone opts in, the "Social Web" entry reappears under Dashboard.

Suggested change
remove_menu_page( 'activitypub-social-web' );
remove_submenu_page( 'index.php', 'activitypub-social-web' );

Related same-opt-in leak worth handling together: AP also registers an admin-bar node at priority 100 (bundled/activitypub/includes/wp-admin/class-menu.php:22). hide_bundled_menus() only hooks admin_menu, so the bar node still appears front-end and admin for any user with the activitypub cap once reader UI is on. An admin_bar_menu handler at priority > 100 calling $wp_admin_bar->remove_node('activitypub-social-web') closes it.

Test suggestion — the existing tests/e2e/bundled-backends.spec.ts only asserts the top-level "ActivityPub" label hides; reader-UI surfaces aren't exercised. Adding a test that flips reader UI on and checks both surfaces catches this class of regression:

test( 'Reader-UI surfaces stay hidden when activitypub_reader_ui is enabled', async ( {
	page,
} ) => {
	// Enable reader UI via whatever flip mechanism the blueprint exposes
	// (wp-cli option update, a seeded option in blueprint.json, etc.).
	await page.goto( '/wp-admin/' );

	// Dashboard submenu: Social Web must not appear under Dashboard.
	await expect(
		page.locator( '#adminmenu a', { hasText: 'Social Web' } )
	).toHaveCount( 0 );

	// Admin bar node: Social Web must not appear.
	await expect(
		page.locator( '#wpadminbar a', { hasText: 'Social Web' } )
	).toHaveCount( 0 );
} );

@kraftbj
Copy link
Copy Markdown
Contributor

kraftbj commented Apr 24, 2026

Fresh adversarial-review pass turned up two things that didn't come up in the earlier rounds. First is a pre-merge blocker, second is a direction-pivot clarification.

1. Atmosphere needs a replacement surface before this lands

hide_bundled_menus() removes the Atmosphere submenu (class-menu.php:76), but this branch ships no Atmosphere provider, setup section, or link anywhere in FOSSE. The only discoverable entry point to the Bluesky backend disappears after upgrade. Users can still hit options-general.php?page=atmosphere by knowing the slug, but that's not a recovery path a normal admin will find.

Before landing we need either:

  • Full Bluesky_Provider matching plan.md Task 5, or
  • Stopgap link — a placeholder card on the FOSSE Setup page that points at options-general.php?page=atmosphere with a short note that the full provider is coming. Low effort, restores discoverability without pre-empting the real provider work.

Either is fine. Just can't have FOSSE hiding Atmosphere with no alternative surface.

2. Native AP settings page becomes a dead-write trap with the current projection architecture

Worth stating explicitly because it's not obvious reading this PR in isolation and it's the same concern that drove the direction pivot on PR #31 / DOTCOM-16875.

What this PR does today:

  • FOSSE's Setup page writes to FOSSE-owned options: fosse_ap_actor_mode, fosse_ap_support_post_types.
  • AP_Provider::register_hooks() installs two filters — pre_option_activitypub_actor_mode and pre_option_activitypub_support_post_types — that shadow AP's reads with the FOSSE-projected values.
  • Result: anywhere AP calls get_option('activitypub_actor_mode') it sees FOSSE's value. Federation does the right thing.

What breaks: the "Show advanced ActivityPub settings" link in render_setup_section() points to the native AP page, which reads and writes activitypub_* directly. That page shows the FOSSE-projected value in its form, the admin changes it, submits, and AP writes to activitypub_actor_mode. Next read is shadowed again by the pre_option_* filter. Form appears to save; effective state didn't change. Silent no-op plus divergent stored state between activitypub_* and fosse_ap_*.

Where we're going (matches your read — AP options, no pre_option_* filters):

  • Drop fosse_ap_actor_mode and fosse_ap_support_post_types entirely.
  • FOSSE's Setup page writes directly to activitypub_actor_mode / activitypub_support_post_types.
  • No projection filters. Both surfaces (FOSSE and native AP) edit the same options; last write wins, admin UI never lies. Cross-network sync into Atmosphere is handled one-way by Automattic\Fosse\Post_Types in PR Add Post_Types projector; pivot onboarding SDD to direct AP option writes #31 (which also doesn't own a mirror option).

SDD amendments on PR #31 already capture this direction across spec.md, plan.md, requirements.md, and planned-decisions.md. To keep both PRs coherent this one likely needs to either:

(a) switch the two option writes to activitypub_* and drop the pre_option_* filters now (light change, converges both PRs on the same model), or

(b) remove the "Show advanced ActivityPub settings" link until (a) lands (less work on this PR, but leaves the dead-write hazard queued for later).

(a) is the cleaner path — it also removes the future "migrate fosse_ap_*activitypub_*" data-migration chore, since no fosse_ap_* values ever get written.

@kraftbj kraftbj self-requested a review April 24, 2026 22:12
@kraftbj kraftbj merged commit a1b6531 into trunk Apr 24, 2026
13 checks passed
@kraftbj kraftbj deleted the add/onboarding-setup-ux branch April 24, 2026 22:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants