=== EZ Page Sync ===
Contributors: JONxBLAZE
Tags: sync, staging, deployment, migration, rollback
Requires at least: 6.9
Tested up to: 7.0
Stable tag: 1.1.37
Requires PHP: 8.1
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Push page edits from a staging WordPress site to production with full data fidelity, HMAC security, and automatic rollback.

== Description ==

EZ Page Sync pushes page and post edits from a staging WordPress install to a production install — safely, with cryptographic request signing, automatic pre-sync snapshots, and one-click rollback if anything goes wrong.

Install it on both your staging and production sites, connect them with a shared secret key, and push individual pages (or bulk-push many at once) without needing a database migration, a full site export, or manual copy/paste.

**What it syncs**

* Post content, title, slug, status, and dates
* All custom post meta (custom fields)
* ACF fields (if Advanced Custom Fields is active on both ends) — Premium
* Elementor page layouts, including images and Global Widget / Template references remapped to production IDs — Premium
* Astra theme meta (if Astra is active on the receiver) — Premium
* Taxonomy assignments across all registered taxonomies — Premium
* Featured images and inline media attachments — Premium
* Yoast SEO meta fields — Premium
* URL rewriting — staging URLs found in content and meta are rewritten to production URLs on arrival

**Sync options**

* Full Sync — syncs everything above
* Content Only — just the post body, title, and slug
* Dry Run — runs the full pipeline including validation without writing to the database, and returns a diff summary
* Preview — shows match confidence and any warnings (e.g. a title mismatch) before you commit
* Bulk sync — push multiple pages at once — Premium
* Force sync — explicitly override an ambiguous-match warning after reviewing it (always free, on every tier)

**Security**

* HMAC-SHA256 signed requests — every payload is cryptographically verified
* Replay protection via one-time nonces with a 10-minute TTL
* Timestamp validation (±5 minute window)
* IP whitelist on the receiver, with CIDR support — Premium to edit; an already-configured whitelist keeps being enforced even if a license lapses
* Rate limiting on the receiver (60 sync requests/hour/IP)
* HTTPS enforced — HTTP is rejected unless `WP_DEBUG` is on and the request is local
* API secret key stored encrypted (AES-256-GCM)

**Rollback**

* A snapshot is taken automatically before every sync
* One-click restore from the Rollback Manager tab — Premium
* Auto rollback on sync failure (configurable)

= Free vs. Premium =

The Free version covers unlimited basic content sync: a single destination, `page`/`post` types, draft-or-publish status, with no row caps or forced downgrades on anything it does support. Premium adds:

* Unlimited sync destinations (multi-destination / relay chains) instead of 1
* Running as both Sender and Receiver on the same site (multi-role)
* Syncing any custom post type, not just `page`/`post`
* ACF, custom meta, Yoast SEO, Astra, Elementor, and taxonomy sync
* Featured image and inline media sideloading
* Bulk sync
* Editing the IP whitelist
* Restricting plugin settings access to a specific role or user
* The Rollback Manager tab
* Extended/unlimited sync log retention and CSV export

A Free install never loses saved configuration or breaks if a Premium feature lapses — enforcement simply pauses until the license is active again.

= Third-Party Services =

EZ Page Sync uses [Freemius](https://freemius.com) to manage Premium licensing and (optionally) plugin update/usage analytics. This applies to the Premium upgrade path only — Freemius is not required for, and does not gate, the plugin's core free sync functionality between your own staging and production sites, which talk to each other directly via the HMAC-signed connection you configure, not through Freemius.

On first activation you'll see an opt-in screen where you can choose to share basic, non-sensitive diagnostic data (such as your site URL, WordPress/PHP version, and plugin version) to help improve the product, or skip this entirely — skipping does not affect any Free-tier functionality. If you purchase a Premium license, Freemius additionally processes the billing/account information necessary to issue and validate that license.

See Freemius's [Privacy Policy](https://freemius.com/privacy/) and [Terms of Service](https://freemius.com/terms/) for full details on what is collected and how it's used.

== Installation ==

1. Upload the `ez-page-sync` folder to `/wp-content/plugins/`, or install it through the **Plugins → Add New** screen.
2. Activate the plugin on **both** your staging and production sites.
3. On staging, go to **EZ Page Sync → Settings**, set the role to **Sender (Staging)**, enter your production site's URL, and copy the generated API Secret Key.
4. On production, go to **EZ Page Sync → Settings**, set the role to **Receiver (Production)**, and paste in the same API Secret Key.
5. Optionally restrict the receiver to known sender IPs, and configure Trusted Proxy IPs if you're behind a load balancer.
6. From the **Sync Pages** tab on staging, use Preview or Sync on any page.

== Frequently Asked Questions ==

= Does this support WordPress Multisite? =

Not currently. Single-site only.

= Does it work with Advanced Custom Fields? =

Yes (Premium). Both staging and production need ACF active; fields are matched by key first, with a name-based fallback.

= What happens if a sync fails? =

With Auto Rollback enabled (the default), the production post is automatically restored from the pre-sync snapshot, and a `sync_failed` log entry records what happened.

= Can I see what would change before actually syncing? =

Yes — use **Preview** for a match-confidence score and any warnings without writing anything, or **Dry Run** to run the entire pipeline (validation, media, meta) without a database write.

= Is the Free version a crippled trial? =

No. The Free version is not time-limited and does not stop working after a quota or trial period. It's a permanently usable single-destination `page`/`post` sync tool; Premium adds capacity and integrations (multi-destination, ACF, Elementor, media, etc.) on top of it, it doesn't unlock a disabled core.

= Does this send my content to a third party? =

No. Sync payloads travel directly between your own staging and production sites over an HMAC-signed HTTPS connection that you configure — never through EZ Page Sync's own servers or Freemius. See "Third-Party Services" above for what the optional licensing/analytics opt-in does cover.

= How do I regenerate the API key? =

Go to **EZ Page Sync → Settings** and click **Regenerate Key**. Update the other site immediately, since existing connections are invalidated the moment you do.

= I'm getting "Rate limit exceeded" errors. =

The receiver allows 60 authenticated sync requests per hour per sender IP (health-check pings don't count). Wait for the window shown in the error to reset, or reduce how often you run bulk syncs.

== Screenshots ==

1. Sync Pages tab — searchable page list with per-page sync/preview and bulk actions.
2. Sync Log tab — filterable, paginated audit log.
3. Settings tab — sender/receiver configuration in one place.
4. Rollback Manager — snapshot history with one-click restore (Premium).

== Changelog ==

= 1.1.37 - 2026-07-03 =
* **Fix**: The v1.1.35 Free-tier Plugin Role lock now also fires when a receiver has been contacted by an authenticated sender that hasn't yet completed a real content sync (e.g. Test Connection). Previous behavior only detected sync via the `_ez_ps_last_synced_site` post meta — which is written by `Receiver::handle_sync()` but NOT by test-connection pings — so a receiver that had auth-passed a sender but hadn't synced any content yet fell outside the lock's coverage. Now a lightweight `ez_ps_receiver_last_contacted_at` option is set on the first successful `Receiver::verify_request()` (any authenticated request from any sender), and `Admin::has_incoming_sync_history()` checks it as a fast path before falling back to the post-meta signal.
* **Fix**: `rest_reset_sync_origin()` now also clears the `ez_ps_receiver_last_contacted_at` option along with the ownership meta and the API key. Without this, a receiver that had ever been contacted would stay locked into Receiver role forever, and the admin couldn't repurpose the site as a Sender post-Reset. The next authenticated request re-sets the option and re-engages the lock.
* **Tests**: 2 new methods in `tests/AdminGatingTest.php` — the option-based fast path in `has_incoming_sync_history()` (fires even without post meta), and Reset's clearing of the option. 606 PHPUnit + 17 JS tests pass.

= 1.1.36 - 2026-07-03 =
* **Fix**: Reset Sync Origin now also regenerates the API secret key, truly severing the current staging site. Previously, clicking Reset Sync Origin only cleared post-ownership metadata — the current staging (which still held the old API key) could immediately re-sync and re-claim ownership, silently defeating the reset. Now the key is rotated alongside the meta cleanup, so the current staging's HMAC signatures are instantly invalid. The new key is returned to the admin UI in the same flow the Regenerate Key button uses, so it can be copied to whichever staging the admin wants to reconnect.
* **UI**: Reset Sync Origin description and confirmation dialog updated to explain both actions (ownership clear + key rotation). Rebuild instructions: copy the new key to the reconnecting staging site.
* **Tests**: New coverage in `tests/AdminGatingTest.php` verifies (a) the response includes a new plain-text key different from the old, (b) storage now holds the encrypted new key, (c) the three ownership meta keys are deleted from `wp_postmeta`, and (d) the success message references both actions. `EZ_PS_Test_WPDB` gained a `delete()` stub with a `delete_calls[]` trace to unlock this and future postmeta-cleanup tests. 604 PHPUnit + 17 JS tests pass.

= 1.1.35 - 2026-07-03 =
* **Fix (Free tier)**: The Plugin Role selector on the Settings tab is now locked when a Free-tier site is currently a Receiver AND has already received at least one sync from an upstream Sender. Previously a Free-tier receiver could flip to Sender via the checkbox, which would silently orphan the sender's outgoing flow. The lock is enforced both in the UI (checkboxes render disabled with a lock explanation) and in the server-side sanitize (any submitted role change is rejected and existing storage returned). Premium bypasses entirely — with `multi_role` entitled, running both roles simultaneously is safe.
* **Fix**: If disabled by the lock, the Receiver role is preserved on save via a hidden input (disabled checkboxes don't submit); the sanitize layer's early-return guard is defense-in-depth.
* **Tests**: New coverage in `tests/AdminGatingTest.php` for the sync-history probe, the composite `is_role_change_locked_on_free()` gate (Free-tier + receiver-only + has history), the sanitize early-return, and the template disabled-checkbox / lock-message rendering. 601 PHPUnit + 17 JS tests pass.

= 1.1.34 - 2026-07-03 =
* **Breaking (protocol)**: Completed the two-stage rebrand started in v1.1.20/v1.1.21 — renamed every internal identifier from the legacy `WPS_*` / `wps_*` / `WPS\` / `wps/v1` / `class-wps-*.php` / `wp_wps_logs` / `_wps_*` / `wpsAdmin` / `.wps-*` scheme to `EZ_PS_*` / `ez_ps_*` / `EzPageSync\` / `ez-page-sync/v1` / `class-ez-ps-*.php` / `wp_ez_ps_logs` / `_ez_ps_*` / `ezPageSync…` / `.ez-ps-*`. Sender and Receiver must upgrade in lockstep (REST namespace + HMAC payload fields changed).
* **Change**: 13 class files renamed via `git mv` (`class-wps-*.php` → `class-ez-ps-*.php`, plus the shared-utilities trait); PHP namespace `WPS\` → `EzPageSync\`; DB option prefix `wps_*` → `ez_ps_*`; post-meta prefix `_wps_*` → `_ez_ps_*`; log table `wp_wps_logs` → `wp_ez_ps_logs`; REST namespace `wps/v1` → `ez-page-sync/v1`; HMAC payload fields `x_wps_origin_chain`/`x_wps_domain_map`/`wps_version` → `x_ez_ps_*`; JS globals `wpsAdmin`/`wpsMetabox`/`wpsAdminColumns`/`wpsActiveTab`/`wpsElementorSyncAllowed`/`wpsMultiRoleAllowed` → `ezPageSync…`; CSS classes `.wps-*` → `.ez-ps-*`; cron hooks + filter/action hooks + capability + settings group + transient prefixes + object-cache group + debug log filename + CSV export filename all shifted in lockstep.
* **New**: `Uninstaller::cleanup_legacy_wps_data()` runs on every activation and idempotently wipes any leftover pre-rebrand data (`wps_*` options, `_wps_*` meta, `wp_wps_logs` table, legacy cron hooks). SQL LIKE patterns pipe-escape underscore + explicit `ESCAPE '|'` to prevent wildcard collision with unrelated plugin prefixes (e.g. WP Store Locator's `wpsl_*`, WP Shopping Cart's `wpsc_*`). Multisite cron cleanup runs inside the per-site loop since WP cron is site-scoped.
* **Deliberately unchanged**: Yoast SEO's `_yoast_wpseo_*` meta, the vendored Freemius SDK, historical `docs/` audit reports, plugin folder slug (`ez-page-sync/`), text domain (`ez-page-sync`), and Freemius product slug (already correct since v1.1.20).
* **Upgrade action**: After update, WordPress does NOT auto-deactivate this time (main file path unchanged since v1.1.21). All plugin state resets — old `wps_*` options/meta are cleaned on activation, so you'll re-run through Settings once. Sync history and rollback snapshots taken pre-v1.1.34 are cleared.
* **Tests**: 591 PHPUnit + 17 JS. Five new `UninstallerTest` methods cover the destructive legacy-cleanup path: SQL correctness, wildcard-escape regression guard, per-site cron clearing on multisite, single-site cron clearing, idempotency.

= 1.1.33 - 2026-07-02 =
* Security: Hardened the ACF field direct-write path — field name and label are now sanitized and length-capped at the write site (the field name could previously arrive unsanitized via the post_content blob fallback), and field settings merged from the incoming blob (type, instructions, default_value, conditional_logic, wrapper, etc.) are now type-checked against a whitelist before being re-serialized into the database.
* Docs: The deliberate kses bypass on the content write path (required for Gutenberg unicode-escape fidelity) is now documented at every call site and in the Receiver class docblock, including the content trust model it relies on (HMAC + timestamp + nonce + IP whitelist + rate limit).
* Docs: Marked the unreachable post_date/post_modified handling in the receiver as known-dead code with wiring instructions, so date sync isn't accidentally half-enabled.
* Tests: New regression locks — post_content passes through verbatim (kses bypass invariant, Gutenberg unicode-escape preservation, invalid-UTF-8 stripping) and ACF direct-write bounds/type enforcement.

= 1.1.32 - 2026-07-01 =
* Fix: Premium badge upgrade links no longer open in a new tab — `target="_blank"` and `rel="noopener"` removed from both the PHP `premium_badge()` helper and the JS-rendered badge in the sync list column.

= 1.1.31 - 2026-07-01 =
* Change: Distribution model consolidated to a single Premium-mode zip — wp.org distribution is no longer targeted. An unlicensed install still runs in Free mode (all Premium features remain gated at runtime by the Freemius license check), so upgrade/downgrade behavior for existing installs is unchanged.
* Build: Removed `build-free` and `build-premium` Makefile targets in favor of a single `make build`. `EZ_PS_FS_IS_PREMIUM` now defaults to `true` in source — no more sed-flip at package time. `php -l` lint is now part of the standard build (scoped to skip the vendored Freemius SDK).
* Freemius: `is_org_compliant` flipped to `false`; removed the `'pricing' => false` menu arg and the `admin_head` CSS hack that suppressed the SDK's "Pricing" submenu link — both existed only to satisfy wp.org Guideline 11.
* Housekeeping: Swept stale "Free/wp.org build" references from code and test comments.

= 1.1.30 - 2026-06-25 =
* Fix: Freemius checkout URL in the free (wp.org) build now includes all expected query params — plugin_id, public_key, plugin_version, plan_id, pricing_id, plan_name, user email/name, home_url (plugin settings page), billing_cycle, currency — built via `http_build_query()` for deterministic, reliable output.
* Fix: `admin_head` CSS that suppresses the Freemius sidebar "Upgrade ▶" link is now scoped to EZ Page Sync admin screens only, removing any sitewide CSS output and fully satisfying WP.org Guideline 11.

= 1.1.29 - 2026-06-25 =
* Fix: "Go Premium" button now appears in the free (wp.org) build where the Freemius SDK is not vendored. `get_upgrade_url()` falls back to the `EZ_PS_UPGRADE_URL` constant (direct Freemius checkout) when the SDK isn't present.

= 1.1.28 - 2026-06-25 =
* Add: "Go Premium" button in the admin header — visible on every tab for free users, links directly to the Freemius upgrade page.
* Add: "Unlock Premium Features" callout section at the bottom of the Help tab with a feature summary and upgrade CTA.
* Fix: Suppress Freemius "Upgrade ▶" sidebar link (which violated WP.org Guideline 11) via `'pricing' => false` config and a targeted CSS hide so the pricing page URL remains accessible for the Go Premium buttons.

= 1.1.27 - 2026-06-25 =
* Fix: Sync button no longer disappears after editing a post on staging sites whose WordPress timezone differs from the server's PHP timezone. `get_sync_status()` was comparing `post_modified` (WP local time, parsed by PHP's `date.timezone`) against `_ez_ps_last_synced` (UTC Unix timestamp from `time()`), producing a wrong offset on any host where the two timezones diverge. Now uses `post_modified_gmt` with explicit UTC parsing — the same approach WordPress core uses internally — so the comparison is always UTC vs UTC.

= 1.1.26 - 2026-06-25 =
* Fix: Admin notices (storage engine warning and grandfathered over-cap upgrade prompt) are now scoped to the plugin's own admin pages instead of firing on every WordPress admin screen — resolves WordPress.org Guideline 11 violation.
* Fix: Added missing `translators:` comment to `class-ez-ps-media-sync.php` sprintf call flagged during POT generation.
* Add: Generated `languages/ez-page-sync.pot` translation template to enable community translations via translate.wordpress.org.
* Add: `make pot` target to Makefile for regenerating the POT file after string changes.

= 1.1.25 - 2026-06-25 =
* Fix: Second round of WordPress.org plugin scan compliance — eliminated all remaining ERRORs (logger get_logs() ternary rewritten to if/else to stop the scanner tokenising ternary operators as raw SQL); suppressed advisory DirectQuery/NoCaching warnings across all files with custom-table and atomic-options queries; resolved InterpolatedNotPrepared warnings in auth nonce purge, rollback postmeta cleanup, and sender post-ID mapping query; fixed missing languages/ directory; removed deprecated load_plugin_textdomain() call; suppressed NonceVerification false-positives on read-only GET params in dashboard navigation.

= 1.1.24 - 2026-06-25 =
* Fix: WordPress.org plugin scan compliance — resolved all remaining PHPCS and PluginCheck errors: missing translators comment, unescaped integer in printf, logger SQL built into intermediate variables, fopen/fclose on php://temp stream, unlink replaced with wp_delete_file, debug log rerouted to error_log(), and LIKE wildcard in auth rate-limit query now passed as a parameterised placeholder.

= 1.1.23 - 2026-06-25 =
* Fix: Media file encoding now falls back to native PHP on FTP/SSH WordPress installs where WP_Filesystem cannot initialise without credentials; previously the sync silently omitted all media on those setups.
* Fix: Added a post-read size check after WP_Filesystem::get_contents() to close a TOCTOU gap where a file could grow between the filesize() guard and the actual read, potentially loading more than the configured limit into memory.
* Fix: RuntimeException message in the snapshot safety policy now uses __() rather than esc_html__(); the message flows to the DB audit log and email notifications, not HTML output, so HTML-escaping produced literal entities in those contexts.
* Fix: Removed a stale machine-generated PHPCS report artifact from the docs/ directory.
* Fix: WordPress i18n compliance — added translators comments to all sprintf() calls with placeholders, switched unordered %s/%s pairs to ordered %1$s/%2$s, and replaced parse_url() with wp_parse_url() throughout for consistent cross-PHP behaviour.
* Fix: premium_badge() output is now wrapped in wp_kses_post() at every echo site in templates and admin output, and the metabox sync button label is passed through esc_html().
* Fix: Replaced @unlink() with wp_delete_file() in the receiver's media sideload path.
* Build: Excluded includes/freemius from the build-free distribution zip and added a hidden-file cleanup pass to the build step.

= 1.1.22 - 2026-06-24 =
* Fix: Removed uninstall.php — Freemius can't report the uninstall event (including user feedback) while that file is present. Cleanup now runs via Freemius's own after_uninstall hook instead.
* Fix: Corrected the EZ Page Sync logo shown on the Freemius account/upgrade pages, which was previously falling back to a generic placeholder icon.
* Feature: Added a Help & FAQ tab to the admin dashboard with a setup guide and troubleshooting tips.

= 1.1.21 - 2026-06-24 =
* Breaking: Renamed the plugin's main file to finish the EZ Page Sync rebrand started in 1.1.20. WordPress will show the plugin as deactivated immediately after this update — please re-activate it from the Plugins page.
* Internal: Verified the Freemius account/licensing integration and the admin menu structure continue to work correctly under the renamed plugin identity; updated internal build tooling and documentation to match.

= 1.1.20 - 2026-06-23 =
**Elementor Free-Tier Restriction**
* Feature: Syncing or previewing an Elementor-built page on the Free tier is now blocked outright with a clear "requires Premium" message, instead of silently succeeding while the Elementor layout is dropped. Elementor pages render almost entirely from their layout data rather than the post body, so a partial sync could previously leave production showing a near-blank page while reporting success.
* UI: The Sync Pages tab, the post-list Sync column, and the post-edit metabox all show a Premium lock badge in place of their sync controls for Elementor pages on Free, consistent with every other Premium-gated control in the dashboard (including the same click-to-upgrade link).
* Fix: The Sync Pages tab's "Select All" checkbox could include Elementor-locked rows in a bulk sync despite their controls being disabled; bulk selection now correctly excludes them.
* Hardening: Consolidated the Elementor Free-tier check into a single shared gate used by both the sync and preview code paths, removing a duplicated condition that could have drifted out of sync.

= 1.1.19 - 2026-06-23 =
**Elementor Sync Hardening**
* Performance: Scoped the sender's Elementor image-attachment collection to the exact shapes the receiver actually resolves, instead of a blanket numeric scan that could bundle unrelated attachments on every sync of an Elementor page, on every tier.
* Correctness: Elementor's "Template" widget and Global Widget references — which embed another post by its staging post ID — are now remapped to their production counterpart, reusing the existing ACF relationship-field post-ID mapping. An unresolved reference is left unchanged and logged, never guessed.
* Packaging: Fixed the build process bundling a stray local debugging artifact into shipped zips.
* Tests: Added test coverage for the above.

= 1.1.18 - 2026-06-23 =
**Freemius Feature Gating Audit Fixes**
* Documentation and UI hardening fixes following an internal Premium feature-gating audit (Settings UI badges, force-sync documentation, gating-seam hardening notes).

= 1.1.17 - 2026-06-22 =
* Fixed Elementor image rendering for the newer nested attachment-ID JSON layout format.
* Reordered the receiver sync pipeline so Elementor and Astra meta sync after media uploads, so attachment IDs resolve correctly on the first sync.

= 1.1.16 - 2026-06-22 =
* Fixed a bug where syncing an existing post could silently reset `post_parent` and `menu_order`.
* Hardened several Premium entitlement checks on the receiver.
* Fixed multi-destination sync overwriting legacy fallback meta keys, and corrected the "Last Synced" column to show the most recent sync across destinations.
* Fixed timezone-offset errors in relative sync timestamps.

= 1.1.15 - 2026-06-21 =
* Force Sync is no longer gated as a Premium capability — it's a free data-safety acknowledgment on every tier.
* Removed a server-side restriction that blocked legitimate single-role switching on a grandfathered Free-tier site.

= 1.1.14 - 2026-06-21 =
* Added test coverage for capabilities, media sync, admin gating, rollback snapshots, and multi-destination regressions.
* Fixed Premium feature gating gaps and reinforced entitlement checks.

= 1.1.13 - 2026-06-20 =
* Introduced the Freemius-based Premium tier, with all gating routed through a single seam. The Free build never fatals or loses configuration when a feature isn't entitled.
* Added Premium lock badges with explanatory tooltips next to gated settings.
* Closed several capped-destination-bypass paths and added write-time entitlement checks to the Settings save handler.

= 1.1.12 - 2026-06-18 =
* Fixed unnecessary ACF field group re-imports that falsely flagged ACF Local JSON as out of sync.

= 1.1.11 - 2026-06-18 =
* Fixed a WebP/AVIF content-mismatch regression where genuinely valid images were rejected.

= 1.1.10 - 2026-06-18 =
* Fixed WebP/AVIF uploads being wrongly rejected due to image-parser decode gaps; verified via container signature bytes instead.

= 1.1.9 - 2026-06-18 =
* Closed a nonce-reuse race condition and a media-deduplication race condition.
* Added content-sniffing on top of extension-based MIME validation for media sync.
* Tightened several admin endpoint permission checks.

= 1.1.8 - 2026-06-11 =
* Fixed PHP 8.5 deprecation warnings in CSV export.

= 1.1.7 - 2026-06-10 =
* Fixed broken image synchronization and improved handling of local production modifications to prevent silent overwrites.
* Added WordPress Multisite uninstall support.

= 1.1.6 - 2026-06-06 =
* Hardened production post ownership verification to prevent referential ID hijacking.
* Migrated rollback snapshots from PHP serialization to JSON to mitigate object injection.
* Fixed a rate limiter race condition.

= 1.1.5 - 2026-06-04 =
* Added sorting and filtering for syncable pages in the admin dashboard.

= 1.1.4 - 2026-05-12 =
* Fixed Astra Pro "Custom Layouts" sync and added taxonomy term description/hierarchy sync.
* Fixed Yoast SEO meta fields being skipped.

= 1.1.3 - 2026-05-11 =
* Hardened ACF field deletion sync and fixed a payload key mismatch that stripped field group definitions.

= 1.1.2 - 2026-05-10 =
* Added support for ACF field value deletion sync.

= 1.1.1 - 2026-05-10 =
* Fixed ACF field deletion sync for full data parity.

= 1.1.0 - 2026-05-10 =
* Upgraded secret key storage to AES-256-GCM authenticated encryption.
* Fixed signature verification ordering issues.
* Reduced build size by 90% by excluding development-only directories.

= 1.0.2 - 2026-05-09 =
* Added multi-destination support and multi-role (Sender + Receiver) support.
* Added an automated migration system for legacy single-destination settings.

= 1.0.0 - 2026-04-14 =
* Initial public-facing release: Rollback Manager, Preview, force-sync guard, Trusted Proxy IP support, CSV log export, and core HMAC-secured sync.

== Upgrade Notice ==

= 1.1.20 =
Elementor pages on the Free tier now show a clear "requires Premium" lock instead of silently syncing without their layout. Recommended for all Free-tier users with Elementor pages.

= 1.1.19 =
Elementor sync hardening: fixes a sender-side performance issue and adds correct ID remapping for Elementor Template/Global Widget references. Recommended for all Elementor users.

= 1.1.13 =
Introduces the Premium tier via Freemius. Existing Free installs are unaffected — no functionality is removed, and over-cap configuration (e.g. a second destination) is preserved, not deleted.

= 1.1.0 =
Encryption upgrade for the stored API secret key. No action needed; re-saving your settings after updating both sites is recommended.
