# Changelog

All notable changes to the RankWizAI WordPress plugin are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and the plugin follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

This file is the source of truth for plugin release notes. The build script
(`bin/build.sh`) reads the latest version stanza here and synchronizes it into
`readme.txt` and the plugin header before producing a distributable zip.

## [1.0.9] — 2026-06-10

### Fixed
- **SREV-001 — CPT redirect now fires even when the original loser post still occupies the URL.** `maybe_fire_cpt_redirect()` now intercepts the request by querying `_rankwiz_redirect_from` meta against the raw `REQUEST_URI`, bypassing the `global $post` check that silently prevented 301s when a loser post stayed published.
- **SREV-004 — Redirect `to` target validated to same-site host.** Cross-host `to` URLs are now rejected in the CPT redirect fallback, matching the Redirection-plugin path.

## [1.0.8] — 2026-06-10

### Added
- **R6PROD-006 — Consolidation redirect endpoint.** New `POST /wp-json/rankwiz/v1/redirect` endpoint accepts a batch of `{"from": "/path/", "to": "https://winner.com/"}` pairs (max 20 per request) and creates 301 redirects. Uses the Redirection plugin (via `Red_Item::create`) when available; falls back to a custom `rankwiz_redirect` CPT otherwise. HMAC-authenticated.
- **R6PROD-006 — Noindex endpoint.** New `POST /wp-json/rankwiz/v1/noindex` endpoint accepts a list of WP post IDs (max 50) and applies `noindex,nofollow` via Yoast SEO (`_yoast_wpseo_meta-robots-noindex`) when present, or the `_rankwiz_robots` custom post meta fallback. Used by the consolidation publish job to tombstone losing pages. HMAC-authenticated.
- **R6PROD-006 — CPT redirect fallback.** `rankwiz_redirect` custom post type registered on `init`; `template_redirect` hook emits HTTP 301 for CPT-based redirects when the Redirection plugin is absent.
- **R6PROD-006 — `_rankwiz_robots` `wp_head` emission.** `wp_head` hook emits `<meta name="robots" content="…">` from `_rankwiz_robots` post meta when Yoast SEO is not managing the directive.

## [Unreleased] — wave-2 pack-5

### Added
- **API contract version field (WPINT-010).** The `/wp-json/rankwiz/v1/handshake` and `/wp-json/rankwiz/v1/status` response bodies now include `"api_version": "2"`. The SaaS uses this field to detect contract breaks (e.g., nonce-mandatory HMAC changes) and prompt the user to update the plugin instead of silently misbehaving. This field is additive and backward-compatible with older SaaS versions.
- **WP-002 — Durable webhook delivery.** Webhook sends now use cron-level re-delivery with exponential backoff (5 min / 30 min / 2 h) on 5xx/network failures. Direct-mode blocking timeout reduced from 30 s to 10 s; a failed direct-mode attempt immediately schedules a cron retry rather than looping in-process. A permanent failure (4xx except 429) is stored in `rankwiz_webhook_last_failure` for admin diagnostics. If `DISABLE_WP_CRON` is detected and a pending event is older than 10 minutes, an admin notice is shown urging the operator to configure an external cron daemon or set `RANKWIZ_WEBHOOK_MODE=direct`.
- **WP-007 — Self-hosted update channel.** Plugin version upgrades are now offered through WordPress' built-in update mechanism: a `pre_set_site_transient_update_plugins` filter fetches a manifest from the RankWizAI app every 12 hours and injects an update offer when a newer version is available. A `plugins_loaded` migration runner calls `rankwiz_run_upgrade_if_needed()` before class instantiation so version-specific option migrations can run in the future.
- **WP-010 — i18n text domain.** `load_plugin_textdomain()` is now hooked on `init` so custom `.mo` translation files in `languages/` are respected. A `languages/rankwiz-ai.pot` scaffold is included; `bin/build.sh` regenerates it via `wp i18n make-pot` when wp-cli is available.
- **WPINT-008 — Webhook truncation + reconciliation.** Incoming webhooks with >50 categories or >200 tags are now silently truncated before validation (instead of rejected with a permanent 422 that was never retried). A nightly `ReconcileWpInventoryJob` fetches the full published inventory from each connected WordPress site and tombstones (`post_status='not_found'`) any local `WpPost` row absent from the live set, closing the gap left by lost delete/trash webhooks.
- **WPUX-003 — Pre-flight reachability check on setup screen.** A "Check connectivity" button on the disconnected-state setup screen fires an unsigned GET to `/health` so admins can confirm outbound connectivity before pasting the setup token. Result shown inline; no shared secret required.
- **WPUX-007 — Publish activity log in wp-admin.** A capped ring buffer (20 entries, stored as `rankwiz_publish_activity_log`, autoload=false) is written on every successful publish or draft-save from the RankWizAI app. The connected-state admin page renders a "Recent activity from RankWizAI" table and a "Refresh Status" button.

### Added
- **R6PROD-006 — Consolidation redirect endpoint.** New `POST /wp-json/rankwiz/v1/redirect` endpoint accepts a batch of `{"from": "/path/", "to": "https://winner.com/"}` pairs (max 20 per request) and creates 301 redirects. Uses the Redirection plugin (via `Red_Item::create`) when available; falls back to a custom `rankwiz_redirect` CPT otherwise. HMAC-authenticated.
- **R6PROD-006 — Noindex endpoint.** New `POST /wp-json/rankwiz/v1/noindex` endpoint accepts a list of WP post IDs (max 50) and applies `noindex,nofollow` via Yoast SEO (`_yoast_wpseo_meta-robots-noindex`) when present, or the `_rankwiz_robots` custom post meta fallback. Used by the consolidation publish job to tombstone losing pages. HMAC-authenticated.

- **R21-WP-P2-02 — Publishing consent sentinel locked by tests.** Three PHPUnit cases added to `AdminPageSanitizeTest` to lock in the existing `allow_publishing` write-consent sentinel logic: sentinel present + unchecked → false; sentinel present + checked → true; token-only save (no sentinel) → existing consent preserved (security-critical case).

### Fixed
- **WP-CONSENT-GATE — Noindex/redirect endpoints now honor the publishing consent gate.** `POST /wp-json/rankwiz/v1/noindex` and `POST /wp-json/rankwiz/v1/redirect` previously mutated robots meta / created 301 redirects even when the site owner had set RankWizAI to analysis-only mode (`allow_publishing = false`). Both endpoints now return `403 publishing_disabled` before any post mutation, matching the guard already enforced on `/publish`.
- **R21-WP-P2-01 — Broken "Refresh Status" link.** The Refresh Status button href used `admin.php?page=rankwiz-ai-settings`, which has no WordPress routing entry (the settings page is registered under `options-general.php`). Operators landed on a "not allowed" page instead of refreshing diagnostics. Fixed to `admin_url('options-general.php?page=' . MENU_SLUG)`, consistent with all sibling URLs and the plugins-page Settings link.
- **R21-WP-P2-03 — Dead rotate-secret client code removed.** The `#rankwiz-rotate-secret` click handler block (32 lines in `admin.js`) and four corresponding localized strings (`rotateAction`, `confirmRotate`, `rotating`, `rotateSecret`) were removed. The rotate-secret button markup was intentionally removed in BUG-HUNT F-WP_PUBLISHING-002; retaining the handler and strings implied a feature that does not exist. The server-side 410 handler is retained as a safety net for cached browser assets.
- **WPUX-006 — Handshake failure feedback.** The setup screen now reads the most recent reject entry from the handshake debug log and renders an inline banner with plain-English guidance mapped to each reject code (`no_setup_token`, `invalid_token`, `rate_limited`, `already_connected`) instead of raw JSON.
- **WPINT-009 (plugin side) — Decrypt-failure banner.** If the shared secret is present but undecryptable (WordPress security keys rotated post-handshake), the setup screen shows an error-level admin notice explaining the cause and directing the admin to disconnect and reconnect.
- **WP-013 — Multisite uninstall.** `uninstall.php` now iterates every subsite via `switch_to_blog()` / `restore_current_blog()` and cleans per-site options, transients, user meta, and `_rankwiz_jsonld` post meta from each subsite independently. Scheduled hooks are unregistered network-wide after the per-site loop.

## [1.0.7] - 2026-06-05

### Fixed
- **Complete uninstall teardown (WP-3).** `uninstall.php` now removes all plugin-owned data on uninstall: `rankwiz_options` and `rankwiz_handshake_debug_log` options; six named transients (`rankwiz_last_request_at`, `rankwiz_last_dashboard_fetch`, `rankwiz_last_sync_error`, `rankwiz_last_sync_error_message`, `rankwiz_last_sync_success`, `rankwiz_webhook_failure_count`); per-user first-run dismissal meta across all users; `_rankwiz_jsonld` post meta from every post; and any pending `rankwiz_send_webhook` cron events. Yoast keys are explicitly not touched. Teardown is tested by a new `UninstallTest` suite.

## [1.0.6] - 2026-06-04

### Added
- Publishing consent / analysis-only mode. A new **"Publishing Settings"** card on the plugin admin page lets site owners toggle whether RankWizAI may write to their site. When "Allow RankWizAI to publish/update posts on this site" is unchecked, the publish endpoint returns `403 publishing_disabled` before any `wp_update_post()` call. The connected-view header now shows a status badge — "Publishing: enabled" or "Publishing: analysis-only" — so site owners can confirm the current state at a glance.
- Yoast-absent guard documented and locked by tests. `_yoast_wpseo_metadesc` is only written when `RankWiz_Yoast_Integration::is_available()` returns true. Tests now explicitly assert both the Yoast-active and Yoast-inactive paths (WP-2 / WP-4).

## [1.0.5] - 2026-06-01

### Added
- Publish endpoint now accepts a `schema` (JSON-LD) parameter. Schema is stored as `_rankwiz_jsonld` post meta and emitted via `wp_head` as `<script type="application/ld+json">`. When Yoast SEO is active, `meta_description` is written to Yoast's `_yoast_wpseo_metadesc`; when Yoast is absent, `meta_description` is folded into the post excerpt field.

### Fixed
- Disconnect button no longer freezes for up to 10 s when the RankWizAI app is slow or unreachable. The disconnect notification to RankWizAI is now sent fire-and-forget (non-blocking) so the AJAX response returns immediately regardless of app reachability.
- Conflict detection in the publish endpoint now accepts `wp_modified_at` timestamps in both `Y-m-d H:i:s` and ISO 8601 format and applies a ±1 s epoch tolerance to the comparison. Previously, format drift between WordPress and the Laravel side (trailing `Z`, microseconds) caused silent conflict-flip false positives.
- Processed webhook dedup index now uses a deterministic UUIDv5 `webhook_id` instead of allowing `NULL`. NULL-permissive unique indexes treated each NULL as distinct in both MySQL and SQLite, defeating the dedup constraint entirely.

### Security
- All outbound `wp_remote_*` calls (webhook sender, disconnect notify, test connection, dashboard data fetch) now validate the target URL via `RankWiz_Safe_Http::is_safe_outbound_url()`. This blocks SSRF via RFC 1918, loopback, link-local, AWS metadata endpoint, `.local`/`.internal` hostnames, IPv4-mapped IPv6, and DNS-rebinding vectors.
- Handshake endpoint now validates the supplied `rankwiz_url` against the configured `RANKWIZ_APP_URL` constant (exact host + port match) before accepting the connection. Prevents supply-chain hijack via attacker-supplied backend URL (BHUNT-AUTH-01).

### Changed
- Setup screen step 3 copy now explicitly names both actions: "Click 'Finish setup' below, then return to the RankWizAI dashboard and click the 'Verify connection' button to complete the handshake." Previously the copy implied the handshake happened automatically, causing users to miss the required dashboard step.

## [1.0.4] - 2026-05-12

### Added
- Content endpoint now returns `wp_post_status` in its response (BHUNT-DATA-04). RankWizAI uses this to map non-published statuses (private, pending, future) to a valid `publish_type` when rolling back a content snapshot. Previously the field was missing and rollbacks could send invalid `publish_type` values to the publish endpoint.
- Content endpoint accepts an opt-in `for_rollback=1` query param (BHUNT-DATA-04). When set, posts in any status are returned (private, pending, future, draft). Default behavior (without the flag) is unchanged — only `publish` status posts are exposed, preserving content-leak protection on normal sync flows.

### Changed
- Publish endpoint's idempotency cache key now mixes in the WP post ID via `hash('sha256', $post_id.':'.$idempotency_key)` (BHUNT-FORM-05). Previously the cache was keyed on the idempotency key alone, which would have allowed callers to see cross-post response collisions if they ever reused a key across different posts. The single-post replay behavior is unchanged (same key + same post → same cached response).

## [1.0.3] - 2026-05-09

### Fixed
- Fatal error on activation: `Class "RankWiz_Configure_Endpoint" not found` — added missing `require_once` for the configure endpoint class.
- WordPress connection status incorrectly showed "connected" for non-`connected` statuses — now only shows connected state when `status === 'connected'`.

## [1.0.2] - 2026-05-04

### Added
- Pre-prod debug instrumentation across the handshake flow (gated request/response logging) for easier troubleshooting of connection issues.

### Changed
- Redesigned the "Connect to RankWizAI" setup screen for a clearer first-run experience.

### Fixed
- Admin page contrast under browser-forced dark mode (Chrome "Auto Dark Mode for Web Contents") by opting the plugin card out via `color-scheme: light`. Cache-buster bumped so styles refresh on update.
- Scoped dark-mode overrides to `body.is-dark-theme` so the plugin card no longer inverts when only the OS is in dark mode while WP admin chrome stays light.
- Restored the notice dismiss button to its top-right position.

## [1.0.1] - 2026-05-02

fix: add missing require_once for RankWiz_Dashboard_Widget, RankWiz_Publish_Endpoint, RankWiz_Dashboard_Data_Endpoint — prevented plugin from loading (fatal class-not-found error on activation)

## [1.0.0] - 2026-04-28

### Added
- Initial release of the RankWizAI WordPress connector plugin.
- HMAC-SHA256 authentication with nonce replay protection.
- Bidirectional sync: inventory, content, publish, webhooks.
- Conflict detection on publish.
- Yoast SEO integration with graceful fallback when Yoast is absent.
- Dashboard widget showing pending recommendations and ROI summary.
- Admin settings page with Test Connection, Secret Rotation, and diagnostics.
- Contextual help tabs with troubleshooting guide.
