Add FOSSE admin UI with Connection_Provider architecture#27
Conversation
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.
There was a problem hiding this comment.
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_Registryand a new FOSSE top-level admin menu with Setup/Status pages. - Implements
AP_Provider(settings form + status card) and projects FOSSE options into ActivityPub viapre_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. |
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().
Cast $_POST['fosse_ap_support_post_types'] to array before passing to array_map, preventing a TypeError if the input is malformed.
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.
|
@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. |
There was a problem hiding this comment.
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
is_admin()gates the projection filters — AP reads its own options on WebFinger, outbox, REST, and cron. Inline onfosse.php:132. This is the big one.
Major — needs a plan, not necessarily this PR
- 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
- Provider registration race when providers arrive after the
do_actionfires (Menu.php:30). Solvable alongside #1. Extra Fieldssubmenu suppression matches a rendered URL string; brittle but low-risk (Menu.php:99).BLOG_USER_IDconstant-absence fallback to0can surface a bogus row (AP_Provider.php:377).- In pure
actormode 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_savecapability + 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.
| * | ||
| * Providers call Connection_Provider_Registry::register( $this ) here. | ||
| */ | ||
| do_action( 'fosse_register_providers' ); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| </tr> | ||
| <?php | ||
| } else { | ||
| $count = \Activitypub\Collection\Followers::count( get_current_user_id() ); |
There was a problem hiding this comment.
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.
|
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.
kraftbj
left a comment
There was a problem hiding this comment.
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.
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.
af214e3 to
7af8576
Compare
| remove_submenu_page( 'options-general.php', 'atmosphere' ); | ||
|
|
||
| // AP top-level Dashboard page (gated by activitypub_reader_ui). | ||
| remove_menu_page( 'activitypub-social-web' ); |
There was a problem hiding this comment.
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.
| 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 );
} );|
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
Before landing we need either:
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 architectureWorth 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:
What breaks: the "Show advanced ActivityPub settings" link in Where we're going (matches your read — AP options, no
SDD amendments on PR #31 already capture this direction across (a) switch the two option writes to (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 |
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.
fosse_register_providershook.fosse_ap_*options and projects them to AP viapre_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.src/exclusion from WordPress.Files.FileName sniff for PascalCase namespaced files.What's not in this PR
redirect_urifilter and transient-persisted connect notices. Seesdd/onboarding-setup-ux/planned-decisions.mdfor details.SDD plan task mapping
3efb10dc7eb04972c86e1c7eb049c7eb0493efb10d,72c86e108c773fDeviation from SDD
Menu suppression now also removes
activitypub-blocked-actors-listand the Extra Fields CPT redirect under Users, which were not listed in the original SDD plan (@kraftbj's review only found the entries inclass-menu.phplines 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— cleanpnpm run format:check— cleanRefs: DOTCOM-16793, DOTCOM-16800, DOTCOM-16802, DOTCOM-16803