PageMotor 0.8 Rollout — Integrator Report

PageMotor 0.8 Rollout — Integrator Report

Kenn Jordan · 2026-04-14

I upgraded an EP Suite ecosystem (105 sites across 6 environments) from 0.7.2 to 0.8. This report captures what a developer running a real plugin suite on a mix of VPS + shared-hosting environments actually encountered. Structured to feed your upgrade guide + SDK docs.

Scope

EnvironmentSitesResult
Localhost dev10.8 ✓
Vultr VPS (nip.io)510.8 ✓
Linode VPS (nip.io)280.8 ✓
DigitalOcean VPS (nip.io)130.8 ✓ (9 provisioned dirs without nginx configs left on 0.7.2; unreachable externally)
IONOS VPS (real domains, per-site owners)110.8 ✓ (one site on 0.6 migrated cleanly through v07 and v08)
IONOS shared hosting (SFTP-only)10.8 ✓ with opcache_invalidate guard patch + manual Focus-theme bleed threading
105 sites upgraded. Auto-update flow (Updates UI + Plugins UI → updates.elmspark.com rewritten for the 0.8 protocol) was exercised end-to-end on three canaries: localhost, Vultr Copperline, Linode Copperline. I didn’t re-run that test on every one of the 105 sites; the bulk rollout validated the upgrade path and migration, not the install flow.

Three PM 0.8 findings

1. opcache_invalidate() is unguarded

lib/themes.php:68 calls opcache_invalidate(...) directly. On hosts without the Zend OPcache extension (common on shared hosting — IONOS shared hit this), it’s a fatal.

update_core() runs only on admin page loads (themes.php:61), so the failure mode is: frontend continues to work, site owner tries to log into /admin/ after upgrading, gets Uncaught Error: Call to undefined function opcache_invalidate(). Diagnosing this from outside is frustrating because the site looks fine.

Stack:

PHP Fatal error: Uncaught Error: Call to undefined function opcache_invalidate() in lib/themes.php:68
Stack trace:
#0 lib/themes.php(48): PM_Themes->update_core()
#1 pagemotor.php(168): PM_Themes->__construct()
#2 index.php(27): PageMotor->init()

Fix:

// before
opcache_invalidate(PM_USER_THEMES. "/{$theme['folder']}/$file.php", true);
// after
if (function_exists('opcache_invalidate'))
    opcache_invalidate(PM_USER_THEMES. "/{$theme['folder']}/$file.php", true);

2. v07() replay edge case

Sites that already had update_07=1 in options (set by an older 0.7 v07()) do not re-run the 0.8 version of v07(), which is what clears Admin_Conductor_templates + PM_Seed to force the reseed that introduces the admin-updates template.

Symptom when triggered: /admin/updates/ returns HTTP 200 with an empty body, because no template matches the new admin-updates content type.

I hit this on the Vultr canary (Copperline) before I added a workaround. The bulk upgrade script I then wrote clears the same options preemptively as part of every site’s upgrade, so I can’t say how many of the 105 sites would have reproduced it without the workaround — but every site in the ecosystem had update_07=1 already, so the condition was universal.

Manual recovery (or the shape of the script’s preemptive step):

DELETE FROM {prefix}options WHERE name IN (
  'Admin_Conductor_instances','Admin_Conductor_blocks','Admin_Conductor_head',
  'Admin_Conductor_templates','Admin_Conductor_css_vars','Admin_Conductor_css',
  'Admin_Conductor_css_editor','Admin_Conductor_design','Admin_Conductor_display',
  'Admin_Conductor_data_seed','PM_Seed'
);

Suggestion: have v08() clear the same options as belt-and-suspenders, so sites that missed the new v07() get caught by 0.8’s own migration.

3. v08() bleed keys only reach Attention + Admin_Conductor

v08() threads bleed-bg-lightness, bleed-text-lightness, bleed-saturation into Attention_design and Admin_Conductor_design. Sites running a custom frontend theme (our Focus theme on oej.elmspark.com) never get those keys. The migration finishes, update_08=1 is set, but Bleed CSS variables resolve to nothing until the custom theme’s _design option is updated manually.

If this is intentional (v08 leaves custom themes to their authors), the upgrade guide should say so explicitly — integrators with custom themes will need to update each custom _design option themselves. If it’s an oversight, the fix is to expand the foreach (['Attention', 'Admin_Conductor'] ...) loop.

Update-server protocol (SDK notes)

Implementing a 0.8-compatible update server from scratch, these are the non-obvious requirements. Documenting them explicitly will save each integrator the hour I spent reverse-engineering them.

  • Response is an indexed JSON array with asset-id as a field inside each entry. A keyed associative object returns zero usable entries because lib/updates/remote.php:166-172 iterates and re-indexes by $item['asset-id']. Our first server rewrite used the obvious keyed shape and silently returned nothing.
  • Both download-url and checksum are required for install. Missing either surfaces as the generic request_failed from the installer, not a specific “missing field” error. Explicit documentation of the required shape would save debug time.
  • Checksum format is strictly sha256:{hex} — validated at lib/updates/installer.php:159-172. Any other algorithm or missing prefix fails with checksum_mismatch only after the download.
  • ZIP structure: single top-level folder, contents copied into the existing plugin directory. The installer strips the parent and calls copy_changed(). ZIPs without a single parent folder fail extraction.
  • download-url can be a redirect. The installer runs CURLOPT_FOLLOWLOCATION, so our update.php returns a local download.php?plugin={slug} URL that 302s to a GitHub Releases signed URL. Two-tier architecture (metadata API + file host) is clean and worth presenting as a supported pattern.
  • request_single() fires at install time (remote.php:145) to fetch a fresh download-url + checksum. That separation enables time-limited signed URLs, which is the right primitive for commercial plugin distribution.
  • Endpoint path tolerance: remote.php:213 does rtrim($url, '/') . '/' . $this->endpoint, so a plugin Updates: header of https://host.com/update.php produces a request to update.php/update.php. The code tolerates this via PATH_INFO on Apache/PHP built-in. Either form works; worth documenting the preferred one.

Plugins UI post-install refresh is missing a callback

Same backend install path as Updates UI, same backend result. The gap is in lib/js/plugins.js:34, which calls pm_update_components.init() with no callback, whereas lib/js/themes.js:39 passes function() { pm_themes.refresh(); }. Result: the Plugins UI button animates to “Updated!” but the row keeps the old version and stale Update button until manual refresh.

One-line fix, verified on localhost:

// plugins.js line 34
pm_update_components.init(function() { pm_plugins.refresh(); });

Wrap in an anonymous function rather than passing pm_plugins.refresh as a bare reference — the callback receives (type, button) while pm_plugins.refresh expects (message, id, target), and the mismatch dirties the toast-positioning branch.

Secondary: Updates UI wraps each row in <div data-type="plugin">; Plugins UI buttons have no such ancestor, so button.closest('[data-type]').data('type') resolves to false. Harmless today but worth unifying if future callback logic branches on type.

Environmental friction (integrator’s own problem, flagged for completeness)

None of these are PM’s issues. Any upgrade guide that advises bulk rollouts will see integrators hit them, so worth a paragraph so others know what to clean up first.

  1. macOS AppleDouble (._*) files left by Mac-origin SFTP uploads. PM_Seed’s copy() enumerates them. Clean with find $SITE -name "._*" -delete before upgrade. You already warned about Create Clean Archive in a previous release — worth keeping that callout.
  2. Ownership mismatch. auto-provision style tooling sets www-data:www-data; per-user strict setups (IONOS-style) use per-site users. If a legacy manual chown left files owned by someone other than the PHP-FPM user actually running the pool, chmod() inside theme reseed silently fails with “Operation not permitted”. And on a multi-tenant VPS, different sites may route through different FPM pools with different users — detect the right user by stat-ing the existing config.php or reading the pool’s user = directive; don’t assume www-data.
  3. Debris at user-content/plugins/ root. A botched historical deploy had put a loose plugin.php + class-ep-suite.php + css/, includes/, js/ directly under user-content/plugins/ instead of inside a plugin subdirectory. PM’s plugin loader picked them up and triggered “Cannot declare trait EP_Suite” fatals. Delete loose files at that root before upgrade.
  4. OPcache caches fatals. After fixing the underlying cause of any fatal, PHP-FPM kept serving the cached bytecode with the old error. systemctl restart php8.3-fpm (or waiting for opcache.revalidate_freq) is required to see the fix take effect.
  5. First admin hit after migration sometimes 500s. Heavy operation (migration + reseed + CSS recompile). Second hit always succeeded. Worth a one-liner in the upgrade guide: “if admin returns 500 right after upgrading, refresh once.”

Upgrade pattern that worked at scale

For integrators upgrading bulk site counts, this pattern worked reliably across Vultr, Linode, DO, and IONOS VPS:

# Stage 0.8 once on the box
rsync -a PAGEMOTOR_08_SRC/ /root/pagemotor-08/

# Per-site (driven by a script; FPM_USER detected via stat on config.php):
# 1. Save active plugins list, deactivate all except the one needed for provisioning
#    (in our case EP_Provisioning must stay active through the upgrade window)
# 2. find SITE -name "._*" -delete
# 3. Clean any debris at user-content/plugins/ root
# 4. rsync -a /root/pagemotor-08/ SITE/
#    Safe without excludes: the 0.8 release archive contains no config.php and no
#    user-content/ directory, so an overwriting rsync cannot touch site config or
#    user-installed plugins/themes. (Worth stating this property of the 0.8
#    archive explicitly in the upgrade guide.)
# 5. FPM_USER=$(stat -c '%U:%G' SITE/config.php); chown -R $FPM_USER SITE
# 6. DELETE FROM options WHERE name LIKE 'Admin_Conductor_%' OR name='PM_Seed' OR name='updates'
# 7. curl SITE/admin/  (triggers v08 + reseed; retry once if it returns 500)
# 8. Restore active plugins list

Why keep one plugin active (step 1): the admin hit during step 7 runs the full PM init path. Sites with cross-plugin provisioning dependencies (ours trigger provisioning checks on admin load) benefit from having their provisioning plugin active during the migration. Pure safety-first would deactivate everything.

Why clear Admin_Conductor_* + PM_Seed preemptively (step 6): works around finding #2 above without having to diagnose it per-site.

AI Design Conversion Skill 1.2

Deployed to all central installs we run Claude Code from: testvultr, docentral, licentral, buildtheweb, testrig, plus local ~/.claude/skills/. The skill’s requires: ">=0.8" header correctly sequences this step after the core upgrade — the skill’s output format (HEXA color values, Bleed CSS variables) matches 0.8’s Design Options and CSS variable pipeline, so it does not belong on 0.7 installs.

Summary

  • 105 live sites upgraded to 0.8 across 6 environments. Auto-update flow verified on 3 canaries, upgrade path verified on all 105.
  • 3 PM 0.8 issues surfaced with inline fixes.
  • Update-server protocol documented from an integrator perspective, input for the SDK guide.
  • Plugins UI one-line JS fix verified on localhost.
  • Skill 1.2 deployed in sequence with 0.8 on all central servers.

Happy to discuss any of the above in more depth.

7. admin-updates template missing on IONOS shared sites

Same root cause as finding #2 (the update_07 guard-replay gap), different host. Sites upgraded via the VPS bulk script had Admin_Conductor_* options cleared preemptively — reseed ran, admin-updates landed in templates. Sites upgraded via the lighter IONOS shared SFTP path (updates cache cleared only) never triggered that reseed. Symptom: clicking “Check for Updates” on the admin home loads a blank page because there is no template to render the admin-updates content type.

Confirmed on 3 of 4 IONOS shared sites (epemail.elmspark.com, demo.elmspark.com, sun.elmspark.com). Fix: clear the Admin_Conductor_* options and PM_Seed, hit /admin/ to reseed. Going forward: always include the Admin_Conductor options wipe in the upgrade step for any site type — it is idempotent and safe.

PageMotor 0.8.1b Rollout — 2026-04-14

Following the 0.8 rollout, PageMotor 0.8.1b was released and deployed across the same ecosystem. Key pre-flight findings that shaped the run:

  • Migration guard is still update_08 — no new migration step in 0.8.1b. The upgrade is a pure file replacement with no DB changes required.
  • opcache_invalidate() is now guarded in 0.8.1b — finding #1 from the 0.8 session was fixed upstream. No patch needed before deploying to shared hosting.
  • Admin_Conductor wipe no longer required going 0.8 → 0.8.1b — all sites already have the admin-updates template from the 0.8 reseed. Only the updates cache is cleared.

Scope

EnvironmentSitesFromResult
Localhost dev10.80.8.1b ✓
Vultr VPS (nip.io)510.80.8.1b ✓
DigitalOcean VPS (nip.io)130.80.8.1b ✓
Linode VPS (nip.io)280.80.8.1b ✓
IONOS VPS (real domains)100.80.8.1b ✓
IONOS shared hosting (SFTP)40.80.8.1b ✓
South Africa VPS (domains.co.za)30.7.20.8.1b ✓
110 sites across 7 environments in 24 minutes. The SA VPS had 3 sites still on 0.7.2 — they migrated straight to 0.8.1b without issues.

EP Suite plugin registry

40 of 55 EP Suite plugins are now in the public update registry with SHA-256 checksums and download URLs. Any PM 0.8+ site will see update notifications in Admin → Updates automatically. The remaining 15 are works in progress not yet in the public registry.

Post-rollout issue surfaced (finding #7)

See finding #7 above — the admin-updates template was missing on IONOS shared sites that came through the lighter upgrade path. Fixed on all 4 affected sites after the 0.8.1b rollout completed.

Upgrade pattern for future releases

The proven per-site pattern for VPS hosts (stage once per host, loop per site):

# Stage once on the VPS
rsync -a PAGEMOTOR_SOURCE/ /root/pagemotor-new/

# Per-site loop:
# 1. Save plugins, keep only EP_Provisioning active
# 2. find SITE -name "._*" -delete  (macOS metadata cleanup)
# 3. rsync -a /root/pagemotor-new/ SITE/  (safe — no config.php or user-content in source)
# 4. chown -R $(stat -c %U:%G SITE/config.php) SITE
# 5. DELETE updates cache from options table
# 6. curl SITE/admin/  (warms cache; retry once if 500)
# 7. Restore full plugin list

For IONOS shared hosting (SFTP-only): ZIP the source, upload with a PHP unzipper, run a PHP fixer for the DB steps, hit /admin/, restore plugins, delete all scratch files.

EP Suite Plugin Registry

56 plugins in the EP Suite. 40 are in the public update registry. Sites on PM 0.8+ check updates.elmspark.com automatically — any plugin with an auto-update checksum will surface in Admin → Updates when a new version ships.

40 Published Plugins

PluginVersionStatusDownload
EP Affiliatev0.1.2RegisteredDownload
EP Analyticsv1.2.2Auto-update ✓Download
EP Audit Logv1.0RegisteredDownload
EP Bookingv1.0.29Auto-update ✓Download
EP Booking — Zoomv1.0.7RegisteredDownload
EP Breadcrumbsv1.2Possibly superseded by PM coreDownload
EP Bunny Fontsv1.0.4RegisteredDownload
EP Commentsv1.1.2Auto-update ✓Download
EP Connectv1.0.4Auto-update ✓Download
EP Diagnosticsv1.0.10Auto-update ✓Download
EP Ecommercev0.1.16Auto-update ✓Download
EP Ecommerce — PayPalv0.1.4RegisteredDownload
EP Ecommerce — Productsv0.1.8Auto-update ✓Download
EP Ecommerce — Stripev0.1.8Auto-update ✓Download
EP Ecommerce — Subscriptionsv0.2.5RegisteredDownload
EP Editorv0.1.3RegisteredDownload
EP Emailv1.9.29Auto-update ✓Download
EP Email — AI Replyv1.0.1RegisteredDownload
EP Email — Advanced Formsv1.0.1RegisteredDownload
EP Email — File Uploadsv1.0.12Possibly superseded by PM coreDownload
EP Email — Quizv1.0.4RegisteredDownload
EP FAQv1.0.1Auto-update ✓Download
EP GDPRv1.1.22Auto-update ✓Download
EP Galleryv1.0.7Auto-update ✓Download
EP Maintenancev1.0.2Auto-update ✓Download
EP Newsletterv1.1.22Auto-update ✓Download
EP Newsletter — SendGridv1.0.8RegisteredDownload
EP Passkeysv0.3Dev-only—
EP Password Resetv1.0.1Dev-only—
EP Redirectsv1.0.1Possibly superseded by PM coreDownload
EP Reviewsv1.0.5Auto-update ✓Download
EP SEOv1.1Possibly superseded by PM coreDownload
EP Scheduled Contentv1.1RegisteredDownload
EP Searchv1.0RegisteredDownload
EP Sitemapv1.0RegisteredDownload
EP Social Sharev1.0RegisteredDownload
EP Supportv1.1.15Auto-update ✓Download
EP Testimonialsv1.0.5Auto-update ✓Download
EP Trackingv0.1.1RegisteredDownload
EP Txt Filesv1.0.7Auto-update ✓Download

Auto-update ✓ — in registry with SHA-256 checksum; update will appear in Admin → Updates automatically.
Registered — in registry with download URL; checksum pending (admin notification not yet active for these).
Possibly superseded — functionality may now be handled by PageMotor core in 0.8+; under review.
Dev-only — internal/private; no public release.

16 Work in Progress (not yet in registry)

PluginVersionNotes
Discovery AIv0.3Central site / provisioning workflow
EP Assistantv1.0.0Web-based AI assistant
EP Business Plannerv0.1Early stage
EP Cardsv1.0.4Native cards + Focus Cards migration
EP Cards Importerv1.0.1Companion to EP Cards
EP Coursesv0.4.1Gateway integration planned for PM 0.8+
EP Email ElmsParkv1.0.0Internal/ElmsPark-specific
EP Email Inboxv1.0Early stage
EP MCP Bridgev0.17.2MCP integration layer
EP Membershipv0.4Gateway integration planned for PM 0.8+
EP Provisioningv1.1Internal provisioning; always active during upgrades
EP RSSv1.0Early stage
EP SendItv1.0.0Early stage
EP Sidebarv1.5.0PM built-in sidebar layout activator
EP Suite (base class)v1.0Shared trait; not a standalone plugin
EP Voice Messagesv1.0Early stage