A fleet-wide view of which WordPress plugins are installed, which need updates, and which security plugins are active per site. Updates can be applied per-site through Companion’s Plugin_Upgrader flow.
Where to look
/settings/wordpress-plugins— fleet inventory. Per-site rows with Companion, Gatekeeper (Active/Inactive, fromSite::gatekeeperEnabled()) and Wordfence state, plus SpinupWP update flags. Summary tiles count Companion, Gatekeeper, Wordfence, and “No Wordfence or Gatekeeper”; filter tabs include Missing Companion, Companion installed, and Missing Gatekeeper. Pressable sites don’t appear on this page at all —WordPressPluginsControllerstill querieswhereHas('server', ...), which no Pressable site ever matches (noserverrow). Unlike the other fleet-wide monitoring commands, this controller wasn’t switched toSite::hostMonitored()during the Pressable rollout, so this looks like an overlooked gap rather than a deliberate exclusion — worth revisiting if Pressable plugin inventory needs to show up here too./sites/{id}/updates— per-site Updates tab. Reads fromcompanion_snapshot.plugins. Shows outdated plugins, lets you tick which to update.- Issues page → “WordPress plugins out of date” — counts sites with
companion_snapshot.plugins.counts.updates_available > 0.
Two data paths
Where Companion is installed, plugin data comes from the /snapshot route — full per-plugin inventory (slug, name, version, active, update_available, new_version, auto_update). Cached in sites.companion_snapshot JSON, refreshed nightly at 01:30 ET by clockwork:refresh-companion-snapshot (plus on-demand after any update batch from the Updates page).
Where Companion isn’t installed, we fall back to an SSH + wp-cli probe (clockwork:detect-wp-plugins, daily 04:45 UTC) — server-hosted sites only (any site with a Server row: SpinupWP, GridPane, Cloudways), no Pressable transport exists for this probe today since Pressable sites have no Server row to SSH against. Scoped to the slugs we care about (limit-login-attempts-reloaded, wordfence) — full inventory requires Companion. The on-disk WordPress path comes from Site::resolveWpPath() (the recorded wp_path, or a per-provider convention — /sites/{domain}/files for SpinupWP, /var/www/{domain}/htdocs for GridPane); a site with neither fails cleanly rather than probing a guessed, possibly-wrong path. SpinupWP’s update booleans (wp_core_update, wp_theme_updates, wp_plugin_updates) are still imported and surface as a coarse “updates available” badge — always false for Pressable sites regardless of real update state, same caveat as the per-site Overview tab pills (see Features → Security scans).
Per-site updates
The Updates tab lists outdated active plugins (and lets you optionally include outdated inactive ones). Tick the boxes, click Update selected:
- Browser JS iterates the selection, POSTs each slug one at a time to
sites.companion.plugin-update. - Per-plugin progress renders inline (“5.3.7 → 5.4.0 ✓ (2.4s)”).
- Companion’s
PluginUpdateRoutevalidates, runsPlugin_Upgrader::upgrade(), captures before/after versions, re-activates if it was active before, returns the result. - After the run, JS fires a fire-and-forget snapshot refresh so subsequent page loads see the post-update state.
Updates are synchronous — the user watches them happen. No background job, no batching. Per-plugin live feedback is more useful than a five-minute spinner.
Re-activation matters
Plugin_Upgrader::upgrade() deactivates the plugin during the upgrade and does not re-activate it. If you don’t call activate_plugin() yourself, the site is left running with the plugin off — silent until someone notices. The runner records is_plugin_active($slug) before, then re-activates after if it was active. The response includes was_active and reactivated; the UI flags re-activation failures with a red pill.
Care plan banner
The Updates tab shows a banner: “Care Plan Active” (care plan) or “Hosting Tier” (standard hosting). The care_plan_enabled flag is purely informational here — it doesn’t gate manual updates. Anyone on the team can update plugins on any site; the banner simply denotes tier coverage.
Gatekeeper replaced LLAR
Through 1.10.0 this page had an LLAR column and a per-site “Install LLAR” button. Both are gone as of 1.10.1, along with POST /sites/{id}/install-llar, App\Services\Sites\LlarInstaller, clockwork:install-llar, the modules/Llar package, and the llar_installed chat event. Login brute-force protection is Gatekeeper, built into Companion — to protect a site, install Companion and make sure Gatekeeper is enabled (fleet default or per-site override, see Features → Gatekeeper). The SSH probe still records llar_enabled so any site that still has LLAR active is visible in Weird Stats.
Manual probe
# SSH-based probe (LLAR + Wordfence active-state flags):
php artisan clockwork:detect-wp-plugins
# Per-site Companion-based snapshot refresh:
php artisan clockwork:refresh-companion-snapshot --site=42
What this isn’t
- Not a theme inventory. Themes change rarely; not worth the build cost.
- Not a core update path. WordPress core auto-updates handle minor releases. Major upgrades are still manual on the SpinupWP side.
- No fleet-wide pivot. “Update Akismet across all 12 sites” needs a queue table + drainer + new UI. Defer until someone actually asks.
- No pre-update backup trigger. Would pair beautifully but Companion doesn’t have a backup-trigger capability yet. The Backups page shows recent SpinupWP backups so you can verify recency before clicking.
Gotchas
- Plugin updates and transients: The nightly refresh calls
/plugins(which triggersTransientRefreshervia an HMAC-signed loopbackwp_remote_post()to force a fresh wordpress.org check, rate-limited to once per 30 min per site) immediately before/snapshot. This ensures cached update inventories reflect current available versions. See Features → Updates for details. - SpinupWP can’t enumerate plugins.
wp_plugin_updatesis a count, not a list. The full list comes from Companion (or the SSH probe for the security plugins). echonotprintf %swhen feeding sudo’s stdin password duringwp-cliprobes.printf %s(no trailing newline) makes sudo wait on EOF andwpexits silently with empty output. The detector hides this footgun via a trailing| grep ... || true; don’t strip it without switching toecho.
