# AuctionForge Changelog

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

---

## [3.6.2.8] — 2026-04-30

### Suppress alerts for transient retry conditions

#### Why
The "BidSpirit API returned 0 lots — catalog may not be ready yet" message is logged when the BidSpirit webhook fires before the auction catalog is fully published. The safety cron retries the job automatically and it usually succeeds on the next attempt — there's no operator action required, so a Telegram alert is just noise.

#### Fix
- New private helper `SyncLogger::isSuppressedFromAlerts($message, $context)` matches the message against a substring allowlist of known-transient errors. When matched, the row is still inserted into `af_sync_log` (so the in-app sync console still shows the retry chain) but the `auctionforge_sync_error_logged` fan-out action does NOT fire — wpdeploy and Telegram stay quiet.
- Default suppression substrings: `"BidSpirit API returned 0 lots"`, `"catalog may not be ready yet"`.
- Extendable via the new `auctionforge_sync_alert_suppress_patterns` filter — themes/MU plugins can add their own substrings.

#### Files Changed
- **Modified:** `auctionforge.php`, `inc/sync/SyncLogger.php`

---

## [3.6.2.7] — 2026-04-30

### Typo fix
- Settings → Connections tab: the placeholder/help text under the **Server** field showed `https://my-accaunt.bidspirit.com/` (extra "a"). Now reads `https://my-account.bidspirit.com/`.

#### Files Changed
- **Modified:** `auctionforge.php`, `inc/controllers/Settings.php`

---

## [3.6.2.6] — 2026-04-30

### Fix stale `plugin_version` in reports

#### Why
Sites were reporting their old version to wpdeploy long after they'd been upgraded — e.g. Lonsdale Auctioneers ran v3.6.1.1 but kept telling wpdeploy it was on v2.9.6. Root cause: the constant `AUCTIONFORGE_VERSION` is read from the plugin file's `Version:` header *once at plugin load*, then frozen into `$this->current_version` on the auto-updater instance. After a plugin upgrade replaces the file on disk, existing PHP-FPM workers keep using the cached constant value until they're recycled, so every report from those workers carries the old version.

#### Fix
- New helper `AuctionForge_Auto_Updater::get_live_version()` reads the version directly from the plugin file's header on each call (via `get_file_data()` — uncached).
- `report_health`, `push_sync_error_immediate`, and `retry_sync_errors` now send `get_live_version()` instead of the cached constant.
- The pre-existing iteration through active plugins inside `report_health` already used `get_plugin_data()` per plugin, so secondary plugin entries were already accurate; only the top-level `plugin_version` field was stale.

#### Files Changed
- **Modified:** `auctionforge.php`, `inc/class-auto-updater.php`

---

## [3.6.2.5] — 2026-04-30

### Real diagnostics for plugin self-updates

#### Why
The wpdeploy `update_queue` table had a 46% historical failure rate, but every single failure row stored the literal string `"Installation failed"` — collapsing every failure mode into one opaque error with no diagnostic value. Root cause: the `install_update()` method in both clients (AF auto-updater and wpd-client) treated `Plugin_Upgrader::upgrade()` as a boolean, throwing away `WP_Error` objects and skin-emitted error messages.

#### Fixes (`inc/class-auto-updater.php` `install_update()`)
- **Refreshes the `update_plugins` transient before invoking the upgrader.** The upgrader reads from this transient to know what version to download; if WP hasn't checked recently, the upgrader silently returns false (looking like a failure even though there's a real update queued at wpdeploy).
- **Distinguishes `WP_Error`, `false`/`null`, and success.** `WP_Error` was previously treated as truthy → reported as "completed" even on failure. Now extracts the error code and message.
- **Reads `Automatic_Upgrader_Skin::get_errors()`** to surface the real upgrader-emitted error messages.
- **Reads `$upgrader->result`** which holds the final `WP_Error` for some failure paths.
- **Captures PHP errors/warnings emitted during the upgrade** (file-move issues, etc.) via a temporary `set_error_handler`.
- **Detects "no update in transient"** as a distinct failure cause and labels it explicitly so we know it's a transient-cache problem, not an actual install failure.
- Truncates the combined diagnostic to 1000 chars before sending.

The same fix is applied to `wpd-client.php` (the WPD client plugin shipped from `client-plugin/wpd-client.php` on the wpdeploy server).

#### Files Changed
- **Modified:** `auctionforge.php`, `inc/class-auto-updater.php`

---

## [3.6.2.4] — 2026-04-30

### Bulk-sync worker dispatch fix

#### Fixes
- The HTTP-loopback worker (`auctionforge_rest_sync_worker` in `auctionforge.php`) had its own switch statement for job-type dispatch, distinct from the one in `crontasks/syncWorker.php`. The new `full_sync_upcoming` and `full_sync_past` job types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types to `BulkSyncWorker::process`.
- `inc/coreIncludes.php` now `require_once`s `BulkSyncWorker.php` so the class is available wherever sync code runs.

#### Files Changed
- **Modified:** `auctionforge.php`, `inc/coreIncludes.php`

---

## [3.6.2.3] — 2026-04-30

### Fixes for end-to-end Telegram round-trip

#### Fixes
- **`verify_api_key` reads the option fresh on every call** (`hash_equals` against `get_option('auctionforge_update_api_key')`) instead of relying on the cached `$this->api_key` from constructor time. Different FPM workers can hold different cached values when the option drifts (e.g. via auto-heal collisions), which previously produced intermittent 401s on REST endpoints.
- **New REST endpoint `POST /wp-json/auctionforge/v1/sync/version`** — wraps the existing `trigger-install` flow with a JSON response (always HTTP 200 with `success` flag). The original endpoint returned `WP_Error('no_update', …, status: 404)` when the plugin was already on the latest version, which nginx's `error_page 404` directive then clobbered with a static HTML 404 page on hosts using aaPanel/BTWAF. Keeping the URL under `/sync/*` matches the pattern that's already proven to pass through host WAFs.

#### Files Changed
- **Modified:** `auctionforge.php`, `inc/class-auto-updater.php`

---

## [3.6.2.2] — 2026-04-29

### Catalog-sync kind disambiguation

#### Fix
- `catalog_sync` jobs target a single auction that may be either active or archived. Until now their alerts were always classified as `kind=upcoming`. The classifier now resolves the auction's `auction_state` term meta and routes:
  - `ARCHIVED` / `ENDED` / `POST_AUCTION` → `kind=past` → **Kill + Full Past** buttons
  - anything else (or unresolvable) → `kind=upcoming` → **Kill + Full Upcoming** buttons

#### Files Changed
- **Modified:** `auctionforge.php`, `inc/class-auto-updater.php`

---

## [3.6.2.1] — 2026-04-29

### Telegram Sync Monitoring follow-ups

#### Features
- **Context-aware alert buttons.** Each Telegram alert now shows the action set most relevant to the failing sync: upcoming-sync errors get **Kill + Full Upcoming**, past-sync errors get **Kill + Full Past**, everything else gets **Kill** only. Button choice is driven by a new `kind` field classified at log-write time from the queue's `job_type`.
- **Plugin version in alerts + Update button.** Each push now includes the live `plugin_version`. wpdeploy compares it to the latest `is_stable=1` row in its `versions` table and only renders an **Update plugin** button when the site is behind. Tapping Update calls the existing `POST /wp-json/auctionforge/v1/trigger-install` endpoint to force the upgrade.

#### Architecture
- New private helper `AuctionForge_Auto_Updater::classify_error_kind($jobId)` looks up the job's `job_type` and maps it to `'upcoming'` / `'past'` / `'other'`. Used by both the immediate push and the 1-min retry cron.
- Outbound payloads now include `plugin_version` at the top level; wpdeploy refreshes `sites.plugin_version` on every `/sync-errors` POST.

#### Files Changed
- **Modified:** `auctionforge.php`, `inc/class-auto-updater.php`

---

## [3.6.2.0] — 2026-04-29

### Telegram Sync Monitoring & Remote Control

#### Features
- **Instant Telegram alerts on sync errors.** Any `level='error'` row written to `af_sync_log` is pushed (fire-and-forget, non-blocking) to wpdeploy, which fans it out to a configured Telegram group with inline-keyboard action buttons (Kill / Full Upcoming / Full Past / Acknowledge).
- **Remote command surface.** Any group admin can tap a button to:
  - **Kill sync** — flips DB flags via `SyncQueue::cancelAll()`, then SIGTERMs every active worker; a 5-second watchdog escalates to SIGKILL for any survivor.
  - **Full sync upcoming** — full re-import of every active auction and its lots (queued, observable, killable). Replaces the `re-sync?do=importBidSpiritUpcoming` flow with a queued equivalent.
  - **Full sync past** — same, for archived auctions.
- **Safety-net retry cron** (`auctionforge_retry_sync_errors`, every minute) re-pushes recent error rows. wpdeploy dedupes via `UNIQUE(site_id, af_log_id)` so duplicates are silent.

#### Architecture
- **`SyncLogger::log()`** now fires `do_action('auctionforge_sync_error_logged', …)` on `level='error'` so any handler can react. The auto-updater listener does a 2 s non-blocking POST to `/api.php/sync-errors`.
- **New REST endpoint** `POST /wp-json/auctionforge/v1/sync/cancel-all` (X-API-Key auth) — runs `SyncQueue::cancelAll()` + SIGTERM + watchdog. Logs every action through `SyncLogger`.
- **`POST /sync/start` extended** with a `mode` parameter (`upcoming` default, `past` for archived). Bulk re-sync is now a queued job, not a synchronous block.
- **New job types** `full_sync_upcoming` and `full_sync_past` on `SyncQueue`, dispatched to a new `BulkSyncWorker` that wraps the existing `AuctionList::init()` + `LotList::init($upcoming)` pipeline.
- **`syncWorker.php`** installs a SIGTERM handler that flips the in-flight job to `failed` with a "Killed by admin (SIGTERM, cleanup OK)" marker and closes the DB cleanly before exiting. Watchdog escalates to SIGKILL after 5 s grace.
- **`LotList::auctionProcessing()`** now heartbeats the active bulk-sync job between auctions, preventing the safety cron from auto-failing long bulk runs as stale (the 600 s threshold is unchanged).

#### Files Changed
- **Modified:** `auctionforge.php`, `inc/sync/SyncLogger.php`, `inc/sync/SyncQueue.php`, `inc/class-auto-updater.php`, `crontasks/syncWorker.php`, `inc/import/LotList.php`
- **Created:** `inc/sync/BulkSyncWorker.php`

---

## [3.6.1.6] — 2026-04-29

### High-Value Lot Modes for `[bp_lot_list]`

#### Features
- **`high_value_past` mode** — Displays past auction lots ordered by their catalog estimate (`estimatedPriceInt`), highest first. Useful for "Top Lots" or "Highlights" sections.
- **`high_value_sold` mode** — Displays past sold lots ordered by their actual sold price (`soldBid`), highest first. Only includes genuinely sold lots (`soldBid > 0`) and reuses the same false-positive guard as `random_past` to skip lots where `soldBid` was carried over from `startPrice` without real bidding activity.
- **Admin documentation updated** — The Shortcodes reference page in WP Admin now lists both new modes in the `mode` attribute description and includes example shortcode usage.

#### Architecture
- New constants `MODE_HIGH_VALUE_PAST` and `MODE_HIGH_VALUE_SOLD` on `LotsWidget`.
- New method `LotsWidget::getLotListHighValue($mode, $limit)` builds the query, applies the past-auction tax filter, sets `bp_orderby` to the relevant meta field with `DESC` order, and (for the sold variant) filters out unsold-but-stale lots in PHP.
- New filter hook `bp_lot_list_high_value_args_filter` for downstream customization of the WP_Query args.

#### Files Changed
- **Modified:** `inc/widgets/LotsWidget.php`, `inc/controllers/ShortcodesPage.php`

---

## [3.6.0.18] — 2026-04-25

### Brevo SMS Notifications Integration

#### Features
- **Transactional SMS via Brevo API** — The plugin now sends text message alerts through the Brevo transactional SMS service, running alongside existing Firebase push notifications.
- **Four Notification Types:**
  - **New Auction Published** — Broadcast SMS to all opted-in subscribers when an auction is published.
  - **Auction Starting Soon** — Targeted SMS to users registered for an auction when it is about to start.
  - **Favorited Item Live Soon** — Targeted SMS to users who favorited a lot when it is approaching in the auction.
  - **Outbid Notice** — SMS sent when a user's leading bid is surpassed, with a 5-minute per-item cooldown to prevent spam.
- **SMS Subscriber Database** — New `af_sms_subscribers` table stores user phone numbers, country codes, and per-notification-type opt-in preferences (GDPR-ready explicit consent).
- **Admin Settings Tab** — New "SMS Notifications" tab in Settings with:
  - Brevo API Key input
  - SMS Sender Name (11 char max)
  - Master enable/disable toggle
  - Per-notification-type on/off switches
  - Test SMS widget with phone input and send button
  - Live subscriber count display
- **Profile SMS Preferences** — Users can opt into SMS notifications directly from their profile page with phone number entry and granular notification type selection.

#### Architecture
- **BrevoSMS Service** (`inc/services/BrevoSMS.php`) — cURL-based API wrapper with broadcast, targeted, and single-send methods, message builders with 160-char limit awareness, rate limiting, and comprehensive error logging.
- **SMSSubscriber Model** (`inc/model/SMSSubscriber.php`) — CRUD operations, email-to-phone resolution, opt-in filtering, and upsert logic.
- **Webhook Integration** — SMS dispatch hooks added to `AuctionsAPI.php` methods: `leading_bid_surpassed()`, `item_live_soon()`, `auction_starts_soon()`, and `auctionPublishedPush()`.

#### AJAX Handlers
- `auctionforge_sms_save` — Save SMS preferences from profile page
- `auctionforge_sms_load` — Load SMS preferences for profile page
- `auctionforge_sms_test` — Admin-only test SMS endpoint

#### Database
- New table: `af_sms_subscribers` — Phone numbers, country codes, per-notification opt-ins, timestamps

#### Files Changed
- **Modified:** `auctionforge.php`, `inc/coreIncludes.php`, `inc/controllers/AuctionsAPI.php`, `inc/controllers/Settings.php`, `admin/view/settings.php`, `templates/parts/profile-templates.php`
- **Created:** `inc/services/BrevoSMS.php`, `inc/model/SMSSubscriber.php`

---

## [3.6.0.7] — 2026-04-14

### Hotfix

#### Fixes
- **Namespace resolution** — Fixed fatal error in `wp_footer` commission label hook caused by unqualified `SettingsHelper` and `Settings` class references. Now uses fully qualified namespaces.

#### Files Changed
- **Modified:** `auctionforge.php`

---

## [3.6.0.6] — 2026-04-14

### Commission Label Override Fix

#### Fixes
- **Theme template override compatibility** — Moved the commission label JS override from the `single-lot.php` template into `auctionforge.php` via `wp_footer` hook (priority 99). Sites with theme-level template overrides (e.g. `themes/*/auctionforge/single-lot.php`) were not picking up the inline script. The `wp_footer` approach works universally on all single lot pages.

#### Files Changed
- **Modified:** `auctionforge.php`, `templates/single-lot.php`

---

## [3.6.0.5] — 2026-04-14

### Commission Label Override

#### Features
- **Commission Label Setting** — New dropdown in Settings → Appearance allowing admins to choose between "Buyer's Premium" (default) and "Auction House Commission" as the label displayed on lot pages. This overrides the Bidspirit JS client's default label using a MutationObserver that catches asynchronously injected text.

#### Files Changed
- **Modified:** `inc/controllers/Settings.php`, `templates/single-lot.php`

---

## [3.6.0.4] — 2026-04-12

### Auction Approval Modal Redesign

#### Changes
- **Removed BidSpirit Terms of Use** — The generic BidSpirit "Terms of Use" section that appeared at the top of the non-credit-card approval modal has been removed. The modal now shows only the auction house's own legal agreements managed within the plugin.
- **Renamed Modal Title** — Changed "Terms of conditions" to "Terms & Conditions".
- **Larger Scroll Area** — The Terms & Conditions scroll box is now 350px tall (up from 200px) for easier reading.
- **Proportional Section Heights** — Refund & Return Policy and Chargeback Waiver sections use a shorter 260px scroll box to visually distinguish them from the primary Terms & Conditions.

#### Files Changed
- **Modified:** `templates/js-chunks/approval-info.php`, `assets/js/app.js`

---

## [3.6.0.2] — 2026-04-10

### Webhook Token Generator

#### Features
- **Token Generator Widget** — Added a 16-character random token generator directly below the "Webhook auth token" field on the Settings page. Includes a "Generate Token" button and a clipboard copy icon with visual feedback.

#### Files Changed
- **Modified:** `inc/controllers/Settings.php`

---

## [3.6.0.1] — 2026-04-10

### Patch

#### Changes
- Version bump.

---

## [3.6.0.0] — 2026-04-10

### Legal Agreements for Auction Approval

#### Features
- **Terms & Conditions** — A mandatory, scrollable Terms & Conditions document is now displayed during the auction approval process. Bidders must scroll to the bottom and check an agreement box before their approval request is submitted. Always enabled.
- **Refund & Return Policy** — An optional, scrollable Refund & Return Policy can be enabled in Settings. When enabled, bidders must read and accept it during auction approval.
- **Chargeback Waiver** — An optional, scrollable Chargeback Waiver can be enabled in Settings. When enabled, bidders must read and accept it during auction approval.
- **Admin Management** — New "Legal Agreements" page under the Bidspirit admin menu with tabbed TinyMCE editors for each agreement type. Includes automatic version numbering (incremented on content change) and full version history.
- **Consent Logging** — Every agreement acceptance is logged to a dedicated database table (`af_user_agreement_log`) with the agreement type, version, user email, IP address, user agent, auction ID, and timestamp.
- **User Tracking Integration** — Agreement events (`terms_agreement`, `refund_agreement`, `chargeback_waiver`) are also recorded in the existing IP tracking system alongside registration, login, and bid events.
- **Placeholder Content** — Default sample content is seeded on plugin activation for all three agreement types.

#### Settings Added
- **Enable Refund & Return Policy** — Toggle to require bidders to accept the refund policy during auction approval.
- **Enable Chargeback Waiver** — Toggle to require bidders to accept the chargeback waiver during auction approval.

#### Database
- New table: `af_legal_agreements` — Current active agreement content per type
- New table: `af_legal_agreement_versions` — Version history archive
- New table: `af_user_agreement_log` — User consent records

#### Files Changed
- **Modified:** `auctionforge.php`, `inc/controllers/Settings.php`, `inc/controllers/BidspiritViews.php`, `inc/ip-logger.php`, `templates/js-chunks/approval-info.php`, `assets/js/app.js`
- **Created:** `inc/legal/class-legal-agreements.php`, `admin/view/legal-agreements.php`

---

## [3.5.0.6] — 2026-04-08

### Bid Event Tracking

#### Features
- **Bid Tracking** — All successful bids (regular, catalog, pre-sale buyout, post-sale purchase) now fire `afLogIP("bid", bidspiritUser)` to capture the bidder's full identity in the user tracking table.
- **Whitelist Update** — Added `bid` to the allowed `event_type` values in both the local plugin and the central WPDeploy API.

---

## [3.5.0.5] — 2026-04-08

### Self-Healing Database Schema

#### Features
- **Auto-Migration** — The plugin now auto-detects and adds any missing tracking table columns on version upgrade, ensuring sites with older schemas seamlessly migrate without manual SQL.

---

## [3.5.0.4] — 2026-04-08

### API Key Auto-Healing

#### Features
- **Seamless Self-Registration** — The WordPress plugin now autonomously regenerates and saves a fresh API Key whenever WPDeploy rejects a payload with an `api_key_collision` error. This guarantees zero-downtime security handovers when developers clone instances to proxy/staging servers.

---

## [3.5.0.3] — 2026-04-08

### API Key Collision Defense

#### Features
- **Network-Level Quarantine** — Central API dynamically extracts and strictly validates the incoming payload domain against the registered API Key domain, forcefully blocking mismatched traffic (HTTP 403).
- **Local Critical Alert Banner** — Added a persistent crimson warning banner in the WordPress admin panel that intercepts `api_key_collision` errors, directly instructing developers to update cloned keys.

---

## [3.5.0.2] — 2026-04-08

### Modal Trigger Hotfix

#### Fixes
- Restructured internal UI rendering priority to gracefully deploy the inline Modal triggers inside custom IP log view without colliding with cached `z-index` stacks.

---

## [3.5.0.1] — 2026-04-08

### Hotfix

#### Fixes
- **Timed Auction Badge Visibility** — Fixed a strict type-matching bug in the main `auction_list.php` core file that incorrectly hid the "Timed Auction" badge overlay from valid timed/online auctions on the main upcoming/archive views. The badge now displays properly across all manual and Bidspirit-synced objects when toggled on.

#### Files Changed
- **Modified:** `templates/main/auction_list.php`

---

## [3.2.9.1] — 2026-03-30

### Timed Auction Display Enhancements

#### Features
- **Timed Auction Countdown Text Toggle** — Added a WP-Admin setting "Change text to 'Auction will close in'" that conditionally changes the countdown label for timed/online auctions on single lot pages.
- **Shortcode Text Toggle Support** — Extended the new countdown text logic to the `bp_upcoming_wide` shortcodes so "Live bidding starts in:" successfully transitions to "Auction will close in:" for timed auctions.

#### Files Changed
- **Modified:** `inc/controllers/Settings.php`, `templates/single-lot.php`, `templates/shortcodes/upcoming-auction-wide.php`, `templates/shortcodes/upcoming-auction-wide-manual.php`

---

## [3.2.9.0] — 2026-03-29

### Billing & Invoices Polish

#### Fixes
- **Profile Invoice ID Tracking** — Prevented internal auction codes (e.g. `002` or `ai4`) from breaking into the clean hyphenated titles inside the `profile-templates.php` view.
- **Dynamic Payment Statuses** — Replaced Bidspirit's default/cached `NOT PAID` HTML payloads by wiring the `dompdf` generator to directly respect the current `$b.status` string from the API (resolving instances where a paid invoice still printed "NOT PAID" atop the document).
- **Payment Instructions Conditional** — Automatically hide the "Payment Instructions" block from the PDF footer when an invoice is fully paid.
- **Improved Nomenclature** — Changed "Invoice No:" to "Bidder No:" on document output to prevent confusion with static invoice ledger IDs.

---

## [3.2.6.0] — 2026-03-28

### Multi-Day Auction Lot Filtering Fix

#### Fixes
- **Day 2+ showing lots from Day 1 in multi-day auctions** — The `itemIndex` column in `bp_meta_data` is `varchar(55)`, but the day-range filter in `BidspiritQuery::bp_query_where()` was comparing it as a string. With string comparison, lot numbers like `'11'`, `'101'`–`'145'` incorrectly fell between `'1001'` and `'1450'`, causing Day 1 lots to leak into Day 2 results. Fixed by using `CAST(bpMeta.itemIndex AS UNSIGNED)` for numeric comparison and `intval()` on the PHP-side boundary values. Letter lots (e.g. `14A`, `1025A`) are handled correctly — `CAST` strips the suffix, keeping them grouped with their base lot number.

#### Files Changed
- **Modified:** `inc/controllers/BidspiritQuery.php`

---

## [3.2.5.9] — 2026-03-27

### Catalog Page Fix

#### Fixes
- **Duplicate preview hours on catalog pages** — The `displayHours` field was rendered twice: once via the `bp_after_description_auction` hook in `BidspiritViews.php` and once inline in the auction template. Removed the hook-based rendering to eliminate the duplication.

#### Files Changed
- **Modified:** `inc/controllers/BidspiritViews.php`, `templates/main/auction.php`

---

## [3.2.5] — 2026-03-19

### Sync Reliability & Stuck Lot Prevention

#### Fixes
- **CATALOG_PUBLISHED webhook now triggers catalog sync** — Previously only sent push notifications without syncing lots. When the earlier AUCTION_CATALOG_UPDATED fired before the catalog was ready on BidSpirit, lots were never imported. CATALOG_PUBLISHED now calls `auctionCatalogUpdated()` to trigger a full catalog sync when the catalog is actually available.
- **0-lot API results now fail instead of silently succeeding** — When BidSpirit API returns 0 lots (catalog not yet ready), the sync job now fails with an explicit error message, allowing the next webhook to create a fresh retry job.
- **Expanded CDN allowlist in pre_http_request filter** — The HTTP filter during bulk post insertion now allows all `*.bidspirit.com` subdomains and BidSpirit image CDNs (`fastly.net`, `cloudfront.net`). Previously only `bidspirit.com` and `www.bidspirit.com` were allowed, blocking image downloads during sync and causing worker hangs.
- **InsertLotsBS timeout guard** — Stage 5 (InsertLotsBS processing loop) now has a 180-second timeout and logs progress every 2 batches. Prevents indefinite hangs that previously required manual force-kill.
- **fixResult() no longer marks lots as done when postId=0** — When `lotProcessing()` fails to create a WP post, the staging row now keeps `syncFinished=0` so the lot is retried on the next sync pass, instead of being permanently marked as complete with no actual post.
- **lotProcessing() logs error on missing auction term** — Previously returned 0 silently when `Auction::findAuctionById()` failed. Now logs an explicit error that feeds into `fixResult()` to keep the lot retryable.
- **Hourly orphan detection in safety cron** — New step in `syncSafety.php` detects lot posts that exist in `bp_meta_data` but are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them with `wp_set_object_terms()`.

#### Files Changed
- **Modified:** `inc/controllers/AuctionsAPI.php`, `inc/sync/CatalogSyncWorker.php`, `inc/abs/InsertLotsAbstract.php`, `inc/import/InsertLotsBS.php`, `crontasks/syncSafety.php`

---

## [3.2.4] — 2026-03-18

### Sync Worker Hang Fix

#### Fixes
- **Sync workers hanging at InsertLotsBS stage** — Third-party plugins (Smush Pro, SmartCrawl SEO, WPMU DEV Updates) hook into `wp_insert_post()` and make blocking outbound HTTPS calls for every lot processed. With 489+ lots per auction, this caused workers to hang indefinitely on socket reads to WPMU DEV servers (`32.193.94.25`, `32.193.93.106`). Added `WP_IMPORTING` constant at the REST worker entry point and a `pre_http_request` filter in `CatalogSyncWorker` that blocks all non-essential external HTTP during the batch post-insertion phase. Loopback (`127.0.0.1`, `localhost`) and BidSpirit API calls remain allowed.
- **Stuck staging rows after force-kill** — When workers were force-killed mid-sync, staging rows in `import_history` were left with `syncstart=1` and never unblocked. Subsequent sync runs could not see these rows (filtered out by `Lot::getRawLots()`), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (`SyncQueue::enqueue()` skips if a `processing` job exists), this created a deadlock where no new sync could proceed.

#### Files Changed
- **Modified:** `auctionforge.php`, `inc/sync/CatalogSyncWorker.php`

---

## [3.2.2] — 2026-03-17

### Full Sync Auction Discovery Fix

#### Fixes
- **Full Sync finds no auctions on fresh sites** — The v3 `full_sync` action only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. Added `AuctionList::init()` discovery step before querying the DB, so Full Sync now fetches all auctions from the BidSpirit API and creates taxonomy terms first, then enqueues catalog sync jobs for each.

#### Files Changed
- **Modified:** `auctionforge.php`

---

## [3.2.0] — 2026-03-14

### Webhook Pipeline Fixes, Archive Sync & Admin UI Overhaul

#### New Features
- **Archive Sync** — New "Archive Sync" button on Sync Status admin page. Enqueues catalog sync jobs for all ARCHIVED/ENDED auctions. Past auctions now appear in the auction dropdown with an optgroup separator and state label.
- **Sync Tasks v3** — Replaced old v2 task cards (importData, importBidSpirit, lotBidSpiritOld) with v3 queue-based tasks. "Sync Active Auctions" and "Sync Archived Auctions" now enqueue jobs via the queue system. Maintenance tasks (Clean Draft/Trash Lots, Clean Orphaned Meta) remain as direct-execution tasks.
- **How It Works guide rewritten** — Complete rewrite of the admin guide for v3 architecture. Documents webhook-first flow, all 5 job types, queue system mechanics, cron setup (external + WP-Cron), configuration constants, and troubleshooting.

#### Fixes
- **Dual-write removed** — Removed legacy handlers from `auctionCatalogUpdated()`, `auctionDayEnded()`, and `itemsUpdated()` webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows in `import_history` that were never processed. All three methods now exclusively use the v3 enqueue → spawn path.
- **ITEMS_UPDATED webhook** — Fixed `itemsUpdated()` failing to enqueue jobs. The `ITEMS_UPDATED` payload has `auctionId` nested in `itemsByLanguage.{lang}[0].item.auctionId`, not at the top level. Added extraction logic to resolve it.
- **ItemUpdateWorker incomplete** — `ItemUpdateWorker::process()` was only calling `Lot::bulkSave()` (staging table) without running `InsertLotsBS` to process staged data into WP posts and `bp_meta_data`. Added the InsertLotsBS processing step with autocommit restore. Item updates now flow: stage → InsertLotsBS → WP post + meta → cache clear.
- **Worker spawn reliability** — Changed `SyncQueue::spawnWorker()` from non-blocking HTTPS to blocking HTTP loopback (`http://127.0.0.1` with explicit `Host` header, 2s timeout). Fixes nginx 403/404 errors when HTTPS loopback hits the default vhost instead of the site's vhost.
- **API token constant** — Plugin expects `AUCTIONFORGE_API_TOKEN` in `wp-config.php` (renamed from `UCO_API_TOKEN` during the plugin rename). Documented in readme.

#### Database
- DB version: 22 → 23

#### Files Changed
- **Modified:** `inc/controllers/AuctionsAPI.php`, `inc/sync/ItemUpdateWorker.php`, `inc/sync/SyncQueue.php`, `auctionforge.php`, `admin/view/sync-status.php`, `readme.txt`

---

## [3.0.0] — 2025-03-14

### New Feature: Webhook-First Sync Architecture (Sync v3)

**Overview:** Complete redesign of the BidSpirit data synchronisation pipeline. Replaces the old two-phase polling system (importBidSpirit every hour + importData every minute) with a webhook-driven job queue that processes data in near real-time.

#### Architecture
- **Webhook-driven sync** — BidSpirit webhooks now enqueue jobs into a dedicated queue table (`af_sync_queue`) instead of relying solely on polling cron tasks. Data flows: webhook → queue → background worker → WordPress posts.
- **Dual-write mode** — During transition, webhooks enqueue jobs AND still run the legacy handlers. Once verified, legacy handlers can be removed cleanly.
- **Job deduplication** — Prevents duplicate work by checking for existing pending/processing jobs with the same auction_id + job_type.

#### New Components
- **Job Queue System** (`inc/sync/SyncQueue.php`) — Priority-based FIFO queue with atomic job claiming, status tracking (pending/processing/completed/failed/skipped), worker PID tracking, and automatic stuck job recovery.
- **Sync Logger** (`inc/sync/SyncLogger.php`) — Real-time log table for admin console polling with level filtering (info/success/warning/error) and incremental cursor-based retrieval.
- **Catalog Sync Worker** (`inc/sync/CatalogSyncWorker.php`) — Processes catalog_sync, full_sync, and day_ended jobs by reusing existing `LotList::singleRun()` logic.
- **Item Update Worker** (`inc/sync/ItemUpdateWorker.php`) — Processes individual item_update jobs with the same logic as the webhook `itemsUpdated` handler.
- **Background Worker** (`crontasks/syncWorker.php`) — Async CLI process spawned by webhooks via `exec()` or by the safety cron. Claims and processes queued jobs with a configurable timeout (default 240s).
- **Safety Net** (`crontasks/syncSafety.php`) — Hourly cron that resets stuck jobs (>10min), spawns workers for pending jobs, runs a 24-hour health check (auto-enqueues full sync if no webhooks received), and purges old data.

#### Admin UI
- **Queue Status Dashboard** — New card on Sync Status page showing pending/processing/completed/failed/skipped counts, recent jobs table with real-time refresh, worker status badge, and action buttons:
  - **Full Sync** — Enqueues catalog sync for all active auctions
  - **Sync Selected** — Enqueue a single auction from dropdown
  - **Spawn Worker** — Manually trigger a background worker process
- **Live Console** — Dark-themed terminal console on the Sync Status page. Polls `af_sync_log` every 1.5s showing timestamped, color-coded log entries from webhooks, workers, queue operations, and the safety cron. Supports pause, clear, and auto-scroll.

#### Cron Changes
- **Removed** `importData` (ran every minute — ~1,440 PHP executions/day)
- **Removed** `importBidSpirit` (ran hourly — ~24 PHP executions/day)
- **Added** `syncSafety` (runs hourly at :10 — 24 PHP executions/day)
- **Net effect:** ~62 PHP executions/hour → 1 PHP execution/hour
- **WP-Cron safety hook** (`auctionforge_sync_safety`) — Always-active hourly event, independent of external cron
- **setup.sh v3** — Updated task definitions, new `migrate` command for interactive v2→v3 transition

#### Database
- New table: `af_sync_queue` — Job queue with status, priority, source, attempts, worker PID, timing columns
- New table: `af_sync_log` — Real-time log entries with context, level, auction_id, job_id
- DB version: 20 → 22

#### Constants Added
| Constant | Default | Description |
|---|---|---|
| `AUCTIONFORGE_SYNC_WORKER_TIMEOUT` | 240 | Worker process timeout in seconds |
| `AUCTIONFORGE_SYNC_STALE_JOB` | 600 | Seconds before a processing job is considered stuck |
| `AUCTIONFORGE_SYNC_HEALTH_CHECK` | 86400 | Seconds without webhook before triggering full sync |
| `AUCTIONFORGE_SYNC_LOG_RETENTION` | 7 | Days to keep log entries |
| `AUCTIONFORGE_SYNC_QUEUE_RETENTION` | 30 | Days to keep completed queue jobs |
| `AUCTIONFORGE_CURL_TIMEOUT` | 30 | BidSpirit API cURL timeout in seconds |
| `AUCTIONFORGE_PHP_BINARY` | /www/server/php/83/bin/php | PHP CLI binary path for spawning workers |

#### AJAX Handlers Added
- `auctionforge_sync_log_poll` — Incremental log polling for live console
- `auctionforge_sync_queue_status` — Queue counts + recent jobs for dashboard
- `auctionforge_sync_enqueue` — Admin-triggered sync actions (full_sync, sync_auction, spawn_worker)

### Fixes
- **cURL timeout** — BidSpiritRequest now enforces a 30-second timeout (was unlimited/0), preventing hung connections from blocking the sync pipeline.

### Files Changed
- **Modified:** `auctionforge.php`, `inc/coreIncludes.php`, `inc/controllers/AuctionsAPI.php`, `inc/BidSpiritRequest.php`, `admin/view/sync-status.php`, `crontasks/setup.sh`
- **Created:** `inc/sync/SyncQueue.php`, `inc/sync/SyncLogger.php`, `inc/sync/CatalogSyncWorker.php`, `inc/sync/ItemUpdateWorker.php`, `crontasks/syncWorker.php`, `crontasks/syncSafety.php`

---

## [2.7.3.0]

### Fixes
- Various stability improvements and bug fixes.

## [2.7.1.2]

### Fixes
- Added missing `vatAndCommission()` function in `loadBid()` — the sales tax display on single lot pages was never being rendered because the code to call `loadTemplate` for `vat-and-commission-template` was missing from `app.js`.

## [2.7.1.1]

### Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via `showDecimals` flag. Start price, increments list, and other prices show whole numbers as before.

## [2.7.1.0]

### Fixes
- Fixed Buyer's Premium value not showing on single lot pages.

## [2.7.0.9]

### Fixes
- Fixed fatal error during plugin install/upgrade: `class-stats-database.php` require failure.

## [2.7.0.0]

### New Features: Advanced Lot Analytics
- Conversion Tracking, Peak Viewing Times, Pre-Sale vs Live Comparison, Category Performance, Share Analytics, Search → View Funnel, Click-Through Tracking, CSV Export, Hot Lot Badge.
- New DB table: `af_lot_shares`. New columns on `af_lot_analytics`. DB version bumped to 20.

## [2.6.0.0]

### New Feature: Lot Analytics
- Comprehensive lot view and favorite tracking system with admin dashboard.

## [2.5.0.0]

### New Features
- **Site Statistics System** — Full website analytics built into the plugin admin.
- **Recently Viewed Items** — Profile page tab showing last 20 viewed lots per user.
