Version History
182 releases-
v3.7.1.22 Stable Jul 25, 2026Download v3.7.1.22📦 9.42 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.1.22] — 2026-07-25
Fixed —
Lot::countLots()loaded the entire lot catalogue to produce one integerinc/model/Lot.phpbuilt its query with'numberposts' => -1and then returnedcount(get_posts($args)). That materialises every matching lot as a fullWP_Postobject — priming the post-meta and term caches for all of them — purely to arrive at
a row count.
The caller makes it worse:
ajax/LotListAjax.phpreturns only 5 lots per requestand calls
countLots()alongside them just to render a total. So a "give me 5 lots"AJAX request could load the whole catalogue.
It has been dormant rather than harmless. The query also filters on the
featuredmeta flag (
Lot::VIEW_HOMEPAGE_BS), which currently matches only 0–11 lots on everysite measured, so the cost never materialised. It would have appeared the moment
anyone bulk-flagged lots as featured on a site holding thousands of them. Measured on
two live sites with the flag filter removed to expose the worst case:
| lots | before | after |
|---|---|---|
| 18,718 | 113.5 MB, 0.78 s | 0.0 MB, 0.01 s |
| 12,850 | 76.5 MB, 0.60 s | 0.0 MB, 0.01 s |
Now uses
WP_Querywithposts_per_page => 1,fields => 'ids'andupdate_post_meta_cache/update_post_term_cachedisabled, returningfound_posts.get_posts()cannot be used for this: it forcesno_found_rows => true, whichdiscards the very total required. This is the pattern
templates/page_auction.phpalready used (
$countLots = $wp_query->found_posts;), so the two call sites now agree.The filter arguments are byte-identical — the
BP_QUERY/bp_fieldmeta filter,suppress_filters => falseand the Polylang branch are untouched — so the returnednumber cannot change. Verified on three live sites: old and new implementations and
the live method all returned the same count (11, 7 and 0 respectively).
-
v3.7.1.21 Stable Jul 25, 2026Download v3.7.1.21📦 9.42 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.1.21] — 2026-07-25
Fixed — Webhook item updates never purged the page cache
inc/sync/ItemUpdateWorker.phpstage 5 ("Clearing cache") called onlyHummingbirdSupport::clearCacheByUrl()andAuction::doClearCacheForAuctionPages(). Both are Hummingbird-only, so on any site without that plugin — which is all of them on the current hosting stack — the entire stage was a no-op: Cloudflare, nginx FastCGI and LiteSpeed were never told about the change. Item updates arriving by BidSpirit webhook therefore left stale cached pages behind.It also passed the wrong id.
Lot::getId()returns the BidSpirit lot id (it is loaded straight off the API payload), not the WP post id, soget_permalink()was resolving a nonexistent post and the log line reported a boguspost #17471for what was actually post13620. Stage 5 now resolves the real post id throughbp_meta_data(the authoritative lot↔post join, matched on lot + auction + language and widening if needed) and routes the purge throughCachePurge::purgeLot(), so every configured cache layer is invalidated. The log line reports the resolved post, or says plainly that it could not be resolved.Fixed — A cache dir that cannot be reached now reports itself instead of failing silently
inc/controllers/NginxCacheSupport.phpskipped the file-deletion purge whenever the configured cache directory was unreachable, and said nothing. BecauseisConfigured()only tests thataf_nginx_cache_diris non-empty — never that it is reachable —CachePurgewould call in, the purge would delete nothing, and the caller could not tell success from failure.This is not hypothetical: aaPanel writes
open_basedir=/www/wwwroot/<site>/:/tmp/into each site's.user.ini, while the shared FastCGI cache lives at/www/server/fastcgi_cache— outside it. Every purge silently no-op'd and pages stayed stale untilfastcgi_cache_validexpired (24h by default). It went unnoticed for days because.user.iniisPHP_INI_PERDIRand is read only by the FPM/CGI SAPI — WP-CLI ignores it, sowp evalpurge tests passed while the real web and webhook paths failed.Now every rejection path (outside
open_basedir, missing, or not writable) logs one clearly-worded line per request via the newnoteCacheDirFault(), records the fault in theaf_nginx_purge_erroroption, and raises anotice-warningin wp-admin telling the site owner exactly which directory is unreachable and why. The flag clears itself the moment a purge succeeds. A new publicNginxCacheSupport::purgeHealth()returns the same state for support tooling without attempting a purge.purgeByFile()also checks reachability up front, so a blocked path no longer sprays rawopen_basedir restriction in effectwarnings into the error log.No schema, option-format or config-version change; existing
af_nginx_*settings are untouched. -
v3.7.1.20 Stable Jul 24, 2026Download v3.7.1.20📦 9.41 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.1.20] — 2026-07-24
Fixed —
auto_commit_disabledleft autocommit off, so sync jobs never committed 'completed'On sites with the Auto commit mysql disabled option ON,
inc/import/InsertLotsBS.php::init()issuedSET autocommit = 0before importing lots but never restoredautocommit = 1after its finalCOMMIT. The database connection was left mid-transaction, so the sync worker's subsequentSyncQueue::complete()status UPDATE was never committed — it rolled back when the request ended. The lot data persisted (it is committed in per-batch chunks inside the import), but everycatalog_sync/item_updatejob stayedprocessinguntil the stuck-job watchdog marked it failed, so the Sync Status dashboard showed unbroken failures even though the catalog was mostly current.Observed in the wild on a site where
catalog_syncwas 0-for-326 and no job of any type had completed in ~4 weeks; the lots were present (560/560) but no job ever reachedcompleted.init()now runsSET autocommit = 1immediately after its finalCOMMIT, so the caller's writes — the job-status update in particular — commit normally. (Settingautocommit = 1also implicitly commits any still-open transaction, so this is doubly safe.) Sites with the option off (the default) never entered this path and are unaffected. No schema or config-version change. -
v3.7.1.19 Stable Jul 24, 2026Download v3.7.1.19📦 9.41 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.1.19] — 2026-07-24
Fixed — Category-filtered listings emitted invalid SQL and returned zero lots
inc/controllers/BidspiritQuery.php::bp_query_where()builds thebpMetaWHERE clause for AuctionForge listing queries. Many callers passbp_indexpurely as a truthy JOIN marker —array(1)(seeinc/shortcodes/LotsByCategory.php,inc/model/Lot.php,inc/helpers/BS_API.php) — meaning "join thebp_meta_datatable so I can filter bybp_field/term_id", with noitemIndexconstraint. The reader tried to interpret that positionalarray(1)as an associative["compare"]/["value"]clause, always resolvingcompareto""andvalueto0, and emitted:```
CAST(bpMeta.
itemIndexAS UNSIGNED) 0```
— a comparison with no operator, which MariaDB rejects as a syntax error. Because this ran inside
posts_whereon the main listing query, the *entire* query failed: category-filtered catalog pages and thebp_lots_by_categorywidget returned no lots, and every render loggedWordPress database error You have an error in your SQL syntax … near '0 ) AND ( bpMeta.\categoryId\= … )'(66,000+ entries on the dev site since 2026-03-26).The
itemIndexbranch now only emits aCAST(...) {compare} {value}clause when a comparison operator is actually present; whenbp_indexis just a JOIN marker it emits a1=1no-op so the wrappingAND ( … )stays valid, leaving the real filtering toterm_id/bp_field. Thebetweenrange branch (used by day-range queries with explicit>=/<=) and thebp_idbranch are unchanged. Verified on dev: the previously-failingbp_index=array(1)+categoryIdquery now produces valid SQL (AND ( 1=1 ) AND ( bpMeta.\categoryId\= 717 )) and returns matching lots with no DB error. No schema or config-version change.*(Latent parallel: the
bp_idbranch dereferences["compare"]without an emptiness guard too; not currently triggered in the wild, noted for a future hardening pass.)* -
v3.7.1.17 Stable Jul 24, 2026Download v3.7.1.17📦 9.41 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.1.17] — 2026-07-24
Fixed — Catalog sync
auction-publishedpush fataled with "Cannot use stdClass as array"inc/controllers/AuctionsAPI.php::auctionPublishedPush()read the localized auction name with$obj->auctionsInfoByLang[$langCode]— an array subscript on a value that arrives from the BidSpirit webhook as a stdClass (the payload is decoded as objects, exactly as the surrounding sync code already treatsitemsByLanguage). Under PHP 8 this threw an uncaughtError: Cannot use object of type stdClass as array, returning HTTP 500 toPOST /wp-json/bidspirit/v1/syncso the auction-published push — and the sync request carrying it — failed. It is now read with a null-safe object access (isset($obj->auctionsInfoByLang->{$langCode})), falling back to the auction number when the localized info is absent, and degrades quietly whether the field is an object, an array, or missing. No schema or config-version change.Fixed — nginx FastCGI cache purge logged open_basedir Warnings and silently failed under restricted sites
inc/controllers/NginxCacheSupport.php::safeCacheDir()calledis_dir()/is_writable()directly on the configured cache directory. On aaPanel-hosted sites whoseopen_basediris restricted to their own docroot +/tmp— whileaf_nginx_cache_dirpoints at the shared/www/server/fastcgi_cache— every auction/lot purge emittedPHP Warning: … open_basedir restriction in effectand the direct-file-deletion purge silently did nothing.safeCacheDir()now checks the target against the activeopen_basedirfirst (newdirWithinOpenBasedir()helper) and returns quietly when it is unreachable, suppressing any residual stat warning. Sites with no restriction (or whose cache dir is insideopen_basedir) are unchanged. Sites that cannot reach the cache dir directly should use thengx_cache_purgeURL method (AF_NGINX_PURGE_URL) or add the cache path toopen_basedir. No schema or config-version change.---
-
v3.7.1.16 Stable Jul 24, 2026Download v3.7.1.16📦 9.41 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.1.16] — 2026-07-24
Fixed — Auction pages white-screened (HTTP 500) when an auction had no
auction_daystemplates/page_auction.phpread theauction_daysterm meta into$auctionDaysand immediately calledcount($auctionDays)(and iterated it withforeach). For any auction whoseauction_daysmeta was unset,getTermOption()returnsnull, socount(null)threw an uncaughtTypeError: count(): Argument #1 ($value) must be of type Countable|array, null givenunder PHP 8 — returning a 500 white-screen to visitors and search-engine crawlers hitting that auction page.$auctionDaysis now normalised to an array (if (!is_array($auctionDays)) { $auctionDays = []; }) immediately after it is read, so thecount(), theforeach, and the day-index lookups below are all safe when no schedule is present. Auctions with valid single- or multi-day schedules are unchanged. No schema or config-version change.---
-
v3.7.1.15 Stable Jul 24, 2026Download v3.7.1.15📦 9.41 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.1.12] — 2026-07-20
Fixed — Open-invoices banner ignored the "Enable My Invoices tab" setting
The site-wide "⚠ This account has open invoices / Pay Now" banner rendered for logged-in users with unpaid BidSpirit bills even on sites where the My Invoices profile tab is disabled — and its "Pay Now" button linked to
#bp-bills, a tab that does not exist on those sites, so it led nowhere.checkUnpaidBillsBanner()had no gate on the setting.BidspiritViews::bp_footer()now emitswindow.afBillingTabEnabled, computed from the sameenable_billing_tabexpression that gates the tab markup inprofile-templates.php(so banner and tab can never disagree), andcheckUnpaidBillsBanner()returns immediately when it isfalse— no banner, and nobills/getUnpaidBills/ unpaid-status AJAX traffic on such sites. Sites with the tab enabled are unchanged. No schema or config-version change.Fixed — US auction houses resolved to the UK "Catalogue" dialect in Auto mode (regression)
Restores the address-signature detection that was lost in an earlier merge (originally fixed 2026-06-26, silently reverted by a later version sync). US house addresses typically name no country and end in "City, ST 12345", so
Dialect::captureRegionFromAuctions()took the "ST 12345" tail as an unknown country name and resolved UK.countryToDialect()now takes the full address and recognises a US state code + 5-digit ZIP tail (constUS_STATE_CODES, uppercase-only match), plus a bare trailing ZIP with a$currency as a secondary signal. Conservative by design: named countries still resolve UK — Canadian ("ON M5V 3L9") and Australian ("WA 6000", 4-digit postcode) tails cannot match the 5-digit requirement.Fixed — LiteSpeed cache purges during sync stacked "Purge url" admin notices
CachePurgepurged per-lot URLs via the publiclitespeed_purge_urlaction, which callsPurge::purge_url()with$quite=falseand enqueues a dismissible admin notice per URL — a full-auction sync stacked hundreds of "Purge url …" notices on the next wp-admin page and looked like a fault to clients. Purges now call\LiteSpeed\Purge::cls()->purge_url($u, false, true)directly (identical purge, no notice), falling back to the action when the class is unavailable. Manual purges elsewhere keep their confirmations.Fixed — Sync log wrote one row per skipped auction
LotList::updateData()logged " Skipping auction N (excluded or not matched)" for every excluded auction on every worker run — the dominant source ofaf_sync_logtable growth on multi-house sites (observed in the GBs fleet-wide). It now counts the skips and writes a single "Skipped N/M auctions (excluded or not matched)" summary line per run. The per-auction detail remains available viaDebug::logData().Housekeeping
Removed stray
.bakworking files (auctionforge.php.bak-*,readme.txt.bak-verbump,app.js.prephonebypass-*.bak, CachePurge/BidspiritViews backups) that had been shipping inside the plugin tree since 3.7.1.4.[3.7.1.11] — 2026-07-20
Added — "Auction is live" popup on single-lot pages
When BidSpirit reports an auction as live, single-lot pages now show the "Auction is live — Enter live auction" popup (linking to the BidSpirit bidding console), exactly as auction catalogue pages already did. Previously a one-lot auction had no live indication anywhere on the site, because its catalogue URL redirects straight to the lot page and only the catalogue code path surfaced the live state. (Entry backfilled — 3.7.1.11 shipped with this note in readme.txt only. 3.7.1.10 was an internal bump with no separate changes.)
[3.7.1.9] — 2026-07-14
Fixed — Catalog pagination stayed broken for returning visitors (stale session)
Follow-up to 3.7.1.8. That fix stopped *new* sessions from getting a bad per-page baseline, but a returning visitor whose session already held the old baseline (the session cookie lives 24h) kept getting page 2+ reset to page 1 at any non-default items-per-page — only a full "clear site data" (which drops the session cookie) fixed it for them.
The server no longer resets pagination from session state at all. Per-page changes already return to page 1 on the client (the items-per-page dropdown navigates to the page-1 URL), so the session-based reset in
BidspiritLots::search_filter()andbidspirit_lot_per_page()was both redundant and the source of the stale-baseline problem — it has been removed. Pagination is now correct for every visitor (new or returning, any session state) on the next page load, with no cache/cookie clearing required. No schema or config-version change.[3.7.1.8] — 2026-07-14
Fixed — Catalog pagination broke at any non-default "Items per page"
On auction catalog pages, setting Items per page to any value other than the site's WordPress default (
posts_per_page) made page 2 and beyond redirect straight back to page 1 — so it appeared to "show the same lots" — while the value matching the default worked.BidspiritLots::bp_session_starter()seeded the per-page change-detection baseline fromget_option('posts_per_page'); because auction pages are served from full-page cache / behind a CDN that drops the PHP session cookie, that baseline reset to the default on every request, so any non-default size looked "just changed" on every page load and was force-redirected to page 1 bymaybe_redirect_to_first_page().The baseline is now seeded to
null("unknown previous value"), matching the existing category/search baselines, so a page reset only fires when a genuinely-known prior per-page value differs (isset && !== null && !=). The items-per-page dropdown additionally returns to page 1 client-side, so "change size → page 1" no longer depends on the session. No schema or config-version change.[3.7.1.7] — 2026-07-13
Added — Per-house toggle to bypass phone verification (UK houses auto-bypass)
BidSpirit's
users/phone/sendPhoneVerificationCodereturns a genericUNKNOWN_ERRORfor UK houses (e.g. rosanreeves, edwards) even for valid+44mobiles, because SMS/phone-verification is not provisioned on those BidSpirit accounts. As the requirement is enforced only plugin-side, this left UK bidders unable to bid.A new per-house setting
require_phone_verification(in thebidspirit_settingsoption) makes the client-side bid gate conditional:auto(default) — enforce EXCEPT on UK houses (Dialect::isUk()), which are bypassed while their BidSpirit accounts lack SMS provisioning.off— never enforce (per-house override, e.g.wp option patch update bidspirit_settings require_phone_verification off).on— always enforce.
BidspiritViews::bp_footer()emitswindow.afRequirePhoneVerification; both bid gates (submitBid,submitBidCatalog) and the profile “Verify phone” CTA honour it. Fails closed (enforces) when the flag is absent. No schema or config-version change.---
[3.7.1.2] — 2026-06-30
Fixed — Background sync worker spawn timing out ("Worker spawn HTTPS failed: cURL error 28")
On busy or memory-pressured hosts the background sync worker frequently failed to start, logging
Worker spawn HTTPS failed: cURL error 28: Operation timed out after 5002 milliseconds with 0 bytes received(observed ~29×/day on one site).SyncQueue::spawnWorker()triggers the worker with a blocking loopback HTTP request to the site's own REST endpoint; the timeout was hard-coded to 5 seconds, so whenever the site's own backend took longer than 5s to respond (DB/FPM contention) the spawn aborted *as a hard error* and raised a sync alert — even though the site was up and serving.Three changes make the spawn resilient:
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
AUCTIONFORGE_WORKER_SPAWN_TIMEOUTconstant or theauctionforge_worker_spawn_timeoutfilter. - Scheme-matched loopback. Strategy 1 now issues the
127.0.0.1loopback using the site's own scheme (https when the site is https) instead of always http. Previously an http→https redirect (301) made the loopback look like a failure and forced the slower public-URL fallback; the POST now reaches the worker endpoint directly. - Non-blocking best-effort fallback. If both blocking attempts can't confirm a start within the timeout, the worker is now triggered with a final non-blocking (fire-and-forget) request, logged at warning instead of error. A merely-slow backend still starts its worker, the hourly
SafetySynccron re-spawns if it genuinely didn't, and the condition no longer raises a hard sync alert.
No schema, DB, or config-version change. Behaviour is unchanged on healthy hosts (the loopback still returns 2xx immediately); the new paths engage only when the backend is slow. The underlying slow backend was a server-side issue (MariaDB query-cache lock contention + memory pressure) fixed at the infrastructure level; this release hardens the plugin so a transiently slow backend can no longer turn into a failed sync.
---
[3.7.1.1] — 2026-06-29
Added — Active uptime monitoring via Kuma keyword health marker
The keepalive mu-plugin now emits an invisible
<!-- af-health:ok -->marker into every rendered front-end page (hooked onwp_headandwp_footer, emitted once per request, pure echo — no DB/IO, lands inside cached HTML). Uptime Kuma monitors each site by pulling the homepage and asserting the marker is present, which measures real user-facing availability (DNS + TLS + nginx + PHP + WordPress + DB + render) instead of the old push-heartbeat model that only measured whether WP-Cron was firing — and routinely false-flagged perfectly healthy sites as down (the worst offenders flapped 50–60×/day while serving HTTP 200 the entire time).Because the marker lives in the mu-plugin (which WordPress can't deactivate and AuctionForge re-asserts on every load), it reflects "WordPress is rendering pages" independently of AuctionForge's own activation state, and it survives failed or in-progress plugin updates. The bundled keepalive template is bumped to 1.3.0, so the installer rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpon update. The legacy push heartbeat is retained in this release for a safe transition to active monitoring.---
[3.7.0.43] — 2026-06-22
Fixed — Submission "Need help?" contact was hardcoded (one tenant's details on every site)
The vehicle-submission help panel (
templates/js-chunks/submissions.php) rendered a single hardcoded email and phone, so every AuctionForge site displayed the same contact regardless of which auction house it was — e.g. one tenant'sinfo@absoluteclearance.co.uk/01536 674804showed on unrelated sites.The panel now resolves the contact dynamically, in priority order:
apply_filters('auctionforge_help_email', '')/apply_filters('auctionforge_help_phone', '')— explicit per-site override (any source, e.g. a theme/global/Elementor setting);- otherwise the SEO & AEO Optimizer LocalBusiness details stored in the
seo_aeo_optionsoption (local_email/local_phone); - if neither yields a value the corresponding row is omitted, and the whole "Need help?" block is hidden when no contact is available — so no placeholder or wrong-tenant detail is ever shown.
Internal
- Modified:
templates/js-chunks/submissions.php— replaced the two hardcoded<p>contact values with filtered,esc_html-escaped values resolved fromseo_aeo_options, each row guarded by anif. - Modified:
auctionforge.php— plugin header → 3.7.0.43. - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.42] — 2026-06-22
Fixed — Submission templates emitted extra
<h1>s and placeholder content site-wideThe vehicle-submission view (
templates/js-chunks/submissions.php) is rendered onwp_footeron every page, so its<script type="text/template">chunks shipped into the source of all pages. Two of those chunks used<h1>headings ("Submit your vehicle for auction" and "Detail received successfully"), meaning every page's HTML source contained three<h1>elements. While these live inside inert<script>templates (so a DOM-rendering crawler like Googlebot still resolves a single true H1), raw-source SEO tooling and some AI crawlers flag the duplicates, and the templates also shipped vendor placeholder text.- Both template headings demoted from
<h1>to<h2>— a single H1 per page in source. - Replaced Lorem-ipsum placeholder copy in the intro and progress panels with real submission guidance.
- Replaced the dummy "Need help?" contact details (
support@domain.com/+3265464325) with the live business contact (info@absoluteclearance.co.uk/01536 674804).
No functional/JS changes — template IDs and structure are unchanged.
---
[3.7.0.41] — 2026-06-19
Fixed — Duplicate auction terms (auction shown twice, catalogue split)
A newly published auction could be created twice in WordPress as two identical
auction_numberterms, so the auction rendered twice on the listing and its catalogue was split across the two copies (e.g. 145 lots stored as 144 + 1). The lot posts themselves stayed unique — this was a duplicate *auction*, not duplicate *lots*.Root cause: auction terms are only created in
Auction::save()viawp_insert_category, which inserts whenever the caller's lookup returned no term. That lookup (findAuctionByIdWithLang()→get_terms()meta_query) can return nothing for a brand-new auction when two syncs run concurrently — for example afull_sync_*bulk run (viaBulkSyncWorker/AuctionList) overlapping a catalogue webhook. Both see "no term" and both insert one. The only fallback,findAuctionByNumber(), looks up by the auction *number* slug and never matched the real term slug, so it could not catch the miss. This path is not covered by the existingCatalogSyncWorkerper-auction lock, which serialises catalogue syncs to prevent duplicate *lot posts* but does not guard *term* creation.Auction::save()now, before inserting a term:- acquires a per-auction MySQL advisory lock (
GET_LOCK('<prefix>af_term_<auctionId>', 10)) so concurrent creators serialise — the loser then finds the just-created term and updates it instead of inserting a second one; and - performs an authoritative, cache-bypassing lookup by
auction_id(directtermmeta⋈term_taxonomyquery) and switches to update-in-place when a term already exists.
Polylang-aware: under full multilingual support it only reuses a term in the requested language, so legitimate per-language terms are still created.
Internal
- Modified:
inc/model/Auction.php—save()gains a pre-insert dedup guard; new private helpersfindTermIdByAuctionId(),acquireTermLock(),releaseTermLock().InstanceDBwraps the global$wpdb, so the advisory lock and thewp_insert_categoryinsert share a single DB session (the same property that makes the existingCatalogSyncWorkerlock effective). - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.40] — 2026-06-14
Fixed — Lot "begin to close" countdown now shows minutes (and seconds in the final minute)
The lot-page countdown (
#auctionStartTimer, the *"Auction will begin to close in …"* line) only ever rendered whole days and whole hours, and computed once on page load. With less than an hour to go it therefore read "0 days and 0 hours" — e.g. a lot closing at 20:00 looked like a dead/expired timer from 19:00 onwards, even though ~46 minutes remained. The time maths was correct; the widget simply had no sub-hour granularity and never ticked.It now updates every second and shows the largest meaningful units, dropping leading zero units:
44 minutes•3 hours and 20 minutes•1 day, 2 hours and 5 minutes- In the final minute it counts the seconds down (
40 seconds→1 second) so it never sits on a flat "0". - Once the close time passes (
result < 0) the timer hides itself, as before.
getCountdownText()now wraps the suffix in a.bp-start-countdownspan so the script can rewrite the whole phrase; if that wrapper is ever absent (e.g. a stale page-cached markup),getLeftDate()falls back to the original days+hours behaviour rather than erroring — important becauseapp.jsalso carries the live bidding logic.Internal
- Modified:
inc/controllers/Settings.php—getCountdownText()suffix wrapped in.bp-start-countdown. - Modified:
assets/js/app.js—getLeftDate()rewritten: 1s ticking viasetInterval, days/hours/minutes/seconds, zero-unit omission, pluralisation, and a wrapper-missing fallback. This is the file actually served (front-end$jsFileis hardcoded to.js). - Modified:
src/js/app.js— samegetLeftDate()change, to keep the gulp source in sync.app.min.jsis intentionally not regenerated (unused — the unminifiedapp.jsis enqueued). - Modified:
auctionforge.php— plugin header → 3.7.0.40. (3.7.0.39 was present in the header with no changelog entry; this release supersedes it.)
---
[3.7.0.38] — 2026-06-02
Added — Spelling Dialect "Auto" mode (match the auction house region)
The Spelling Dialect setting (Settings → Appearance) gains a third option, "Auto — match the auction house region", which becomes the new default for sites that haven't chosen one:
- During the existing auction sync (
getAllAuctions),Dialect::captureRegionFromAuctions()reads the auction house's country from the auctionaddress(trailing segment;currencyas a fallback) and stores the resolved dialect in thespelling_dialect_autooption. No API calls happen on page loads — it's a fixed, site-wide value refreshed each sync, so it's fully page-cache-safe. - Mapping: US → Catalog; every other country → Catalogue (American "Catalog" is US-only; UK/IE/AU/NZ/CA/etc. use "Catalogue").
- The admin field shows what was detected, e.g. *"Detected region: United Kingdom → Catalogue."* United States and United Kingdom remain manual hard-overrides.
Default change: new/unconfigured sites now default to Auto (was UK). Until a site's first sync resolves the region, Auto falls back to UK, so behaviour is unchanged until detection kicks in — then it self-corrects (a US house starts showing "Catalog" automatically).
Internal
- Modified:
inc/helpers/Dialect.php—automode,FALLBACK_DIALECT,countryToDialect(),captureRegionFromAuctions();DEFAULT_DIALECT→auto. - Modified:
inc/import/AuctionList.php— capture the house region during the auctions sync. - Modified:
inc/controllers/Settings.php— Auto dropdown option + detected-region note. - Modified:
auctionforge.php— plugin header → 3.7.0.38.
---
[3.7.0.37] — 2026-06-02
Fixed — Per-lot "full price VAT" now drives the VAT statement
The standalone VAT line on lot pages (
vat-and-commission→.vat-field) was keyed off the auction-house VAT rule, so it always read *"VAT: X% on item price and commission"* regardless of a lot's full-price-VAT toggle — and toggling a lot off didn't change it. It now branches on the live per-lot flag (itemInfo.item.fullPriceVat), matching the price-breakdown calc ingetFullPriceInfo():- Full-price VAT on → *"VAT: X% on item price and commission"* (VAT applied to hammer + commission)
- off (default) → *"VAT: X% on commission only"*
This only changes the wording to match the amount
getFullPriceInfo()already computes per the toggle; the VAT figure itself was already correct. The separaterenderVatField()"no VAT on commission" path is unchanged.Internal
- Modified:
templates/js-chunks/vat-and-commission.php— VAT statement branches on the per-lotfullPriceVatinstead ofhouseCommissionVatRule. - Modified:
auctionforge.php— plugin header → 3.7.0.37.
---
[3.7.0.36] — 2026-06-02
A Site Statistics release: day-of-week analysis, an opt-in weekly email report, lot-focused "top pages", and several dashboard refinements.
Added — Day-of-week tabs on the hourly charts
The Hourly Distribution (Site Statistics → Overview) and Peak Viewing Times (Lot Analytics) charts now have a day tab strip — All days · Mon · Tue … Sun — each showing that weekday's total. Selecting a day redraws the 24-hour chart consolidated across every matching date in the range, so you can see *which days and times* visitors are active. The lot-analytics data already carried
day_of_week; it was previously flattened to 24 hours and is now surfaced.Added — Scheduled weekly email report (opt-in, off by default)
A new report under Site Statistics → Settings emails a polished weekly summary: headline KPI cards (Visitors / Pageviews / Sessions with this-week-vs-last-week change), a visitor journey funnel (Visitors → Viewed a lot → Left a bid), busiest hour for each weekday, and the week's most-viewed lots. Driven by a daily cron that sends once on a configurable weekday. Supports one or several recipients (comma/semicolon/space/newline separated, validated and de-duped) and a "Send test now" button that sends to whatever is currently in the Recipient(s) box. Built table-based with inline CSS for Gmail/Outlook/Apple Mail compatibility.
Added — New vs Returning visitors, auction markers, engagement funnel
- New vs Returning visitor KPI cards on the Overview (new = authoritative
af_stats_visitors.first_seenwithin the period, joined so it can never exceed total visitors). - Auction-date markers drawn on the *Traffic Over Time* chart (dashed lines + a captioned list) so traffic can be read against sale dates.
- Engagement funnel panel: Visitors → Viewed a lot → Viewed 5+ lots → Bidders (bidders from synced
af_bid_events).
Changed — "Top pages" is now lot-focused ("Top Lots" / "Most viewed lots")
Both the dashboard Overview panel and the email now list the most-viewed individual lot pages only (
/lot-…), excluding home, catalogue listing/pagination and profile pages as low-signal. Email page titles are shown in full (no mid-word truncation) with the site-name suffix stripped; the dedicated Pages tab remains the unfiltered drill-down.Changed — Lot Analytics "Peak Viewing Times" honours the global date range
Previously hard-coded to the last 30 days; it now respects the dashboard's range picker. The Lot Analytics tab also auto-loads on first click (previously required a manual refresh).
Changed — Chart.js bundled locally
The Statistics and plugin dashboards loaded Chart.js from a CDN at two different versions (
@4and@4.4.1). Chart.js 4.4.1 is now bundled atadmin/assets/js/chart.umd.min.jsand loaded from the plugin (version-busted), so charts work offline / behind strict CSP and the version is pinned.Internal
- Added:
inc/stats/class-stats-report.php—AF_Stats_Report(weekly cron, recipient parsing, HTML builder, "send test" AJAX).admin/assets/js/chart.umd.min.js— bundled Chart.js 4.4.1. - Modified:
inc/stats/class-stats-ajax.php— hourly-by-DOW query;new_visitors,auction_events,funnelin the dashboard payload; lot-only top pages; global date range passed to peak times; weekly-email settings saved. - Modified:
inc/model/LotAnalytics.php—getPeakViewingTimes()accepts an explicit$from/$torange. - Modified:
inc/stats/class-stats-init.php— require + registerAF_Stats_Report. - Modified:
admin/view/stats/dashboard.php— day-of-week tabs, KPI cards, auction-marker plugin, funnel, Top Lots, weekly-email settings UI + test button, local Chart.js. - Modified:
admin/view/dashboard.php— local Chart.js. - Modified:
auctionforge.php— plugin header → 3.7.0.36.
---
[3.7.0.35] — 2026-06-02
Added — Regional spelling dialect ("Catalog" US / "Catalogue" UK)
A new Spelling Dialect dropdown at the top of Settings → Appearance lets each site choose how the word "catalog" is presented on the public front end:
- United States — Catalog — US spelling (the plugin's original authored wording).
- United Kingdom — Catalogue — UK spelling.
When UK is selected, every front-end occurrence is rewritten — *"Catalogue will be available soon"*, *"View catalogue"*, *"Catalogue Coming Soon"*, *"Download catalogue"*, *"Back to the catalogue"*, the lot-page Catalogue tab, and the "catalogue published" app notification.
How it works. Every user-facing catalog string already passes through the
auctionforgetext domain (__()), so the swap is implemented as a singlegettext/gettext_with_contextfilter rather than by editing template files.Dialect::toUk()rewrites whole words only, preserving case and pluralisation (Catalog→Catalogue,View catalog→View catalogue,CATALOG→CATALOGUE,Catalogs→Catalogues), and is idempotent. The filter is front-end only (is_admin()-guarded, with awp_doing_ajax()exception so front-end AJAX is still covered) and is scoped to theauctionforgedomain, so it never touches WordPress core, the theme, or other plugins. Code identifiers, option keys (catalog_layout,bids_in_catalog), CSS classes (af-catalog-sidebar,catalog-section,bp-back-to-catalog-button) and JS variables are all left intact. On US sites the filter is not registered at all, so there is zero added overhead.Internal
- Added:
inc/helpers/Dialect.php—Dialecthelper:get()/isUk()(reads thespelling_dialectoption, default UK),word()for non-translated strings, the case/plural-preservingtoUk()transform, thegettextfilter callbacks, andregister()(no-op on US sites). - Modified:
inc/coreIncludes.php— requireDialect.phpand callDialect::register()at plugin load. - Modified:
inc/controllers/Settings.php—usetheDialecthelper; add the "Regional Spelling" sub-heading and Spelling Dialect field at the top of the Appearance section, plus thefill_bidspirit_options_spelling_dialect()renderer. The value persists through the existingbidspirit_settingsoption and sanitize callback. - Modified:
auctionforge.php— plugin header → 3.7.0.35.
Note for existing sites. The default dialect is UK ("Catalogue"). Any site that upgrades without an explicit
spelling_dialectvalue will therefore display "Catalogue" on the front end. Sites that should keep US spelling must set Settings → Appearance → Spelling Dialect to United States — Catalog (stored asspelling_dialect = us). Admin labels are unaffected in either dialect.---
[3.7.0.34] — 2026-05-30
Fixed — Duplicate lot posts created by concurrent catalog syncs for the same auction
When two catalog-sync jobs for the same auction ran at the same time, every lot was imported twice and the front-end showed each lot as a pair (the second post taking a
-2slug). Observed on a live sale: 157 lots became 313 publishedlotposts.Root cause. Two BidSpirit webhooks arriving in the same second (e.g.
CATALOG_PUBLISHED+AUCTION_CATALOG_UPDATED) each enqueue acatalog_sync.SyncQueue::enqueue()deduplicates with a non-atomic *SELECT-then-INSERT* (no DB unique constraint), so two simultaneous calls both pass the "already pending?" check and both jobs land in the queue. Job *claiming* is atomic per-job, but nothing serialised two different jobs for the same auction, so two workers ran concurrently. The matcherLot::findByBSMeta()only sees committedpublish/draftposts, so during the overlapping insert windows each worker saw "no existing post" for every lot and inserted a fresh full set. Worse,findByBSMetadoesGROUP BY lotId, so the resulting duplicate pair is collapsed to a single row in every later sync — the importer never detects or repairs it, and thecount($datum) > 1"Double" guard inInsertLotsBS::init()is effectively dead code.Fix.
CatalogSyncWorker::process()andprocessDayEnded()now take a per-auction MySQL advisory lock (GET_LOCK('<prefix>af_sync_<auctionId>', 0)) around the API-fetch + post-insertion work. If another worker already holds the lock for that auction, the duplicate job logsconcurrent-lock busyand returns cleanly — marked completed, since the in-flight sync already covers the work — instead of racing. The lock is held on the worker's own DB connection and auto-releases if the process dies, so it can never deadlock the queue. This serialises all catalog and day-ended processing per auction no matter how many duplicate jobs reach the queue.Internal
- Modified:
inc/sync/CatalogSyncWorker.php— newacquireAuctionLock()/releaseAuctionLock()helpers;process()andprocessDayEnded()acquire the lock up front (skip-on-busy) and release it in afinally. - Modified:
auctionforge.php— plugin header → 3.7.0.34.
Note for existing sites. This prevents *new* duplicates; it does not remove ones already created. To find them, look for published
lotposts where two share one BidSpirit lot id (bp_meta_data.id). Keep the most-recently-synced post per lot id and set the rest topost_status = 'trash'— thelotpost type has trash disabled, so do it via SQL, and do not usedraft(whichfindByBSMetastill matches). Then recount theauction_numberterm and clear caches.---
[3.7.0.33] — 2026-05-30
Fixed —
auctionforge_sync_lotscron never scheduled (Lots push to wpdeploy silently stopped)AF_Lots_Syncuses the custombp_ten_minutelyschedule. The sync crons were registered fromAF_Stats_Init::init(), which runs at plugin-load time — *before*auctionforge.phpaddsbp_ten_minutelyto thecron_schedulesfilter further down the same file. Sowp_schedule_event(time(), 'bp_ten_minutely', 'auctionforge_sync_lots')ran against an unknown schedule, failed silently, and left the hook permanently unscheduled. The other four sync hooks use core'shourlyand were unaffected — which is why only the Lots push to wpdeploy went stale (observed: 3 days with no push while every other stream ran on time). Fixed by deferring the syncregister()calls to theinithook, by which point every custom schedule is registered. The hook self-arms on the next request after upgrade.Fixed — Safety Sync admin run reported a false "Request failed (HTTP 200)"
auctionforge_ajax_run_cron_taskreturns its result as JSON, but a stray PHP notice/deprecation emitted by a task (e.g. the Safety Sync BidSpirit delta-check on PHP 8.1) printed into the response body ahead of the JSON, so the browser couldn't parse it and the modal showed a misleading "Request failed (HTTP 200)" even though the task succeeded. The handler now buffers task output, discards it before emitting JSON, and logs any suppressed output viaSyncLogger(contextadmin) so the offending notice can still be found and fixed at the root.Added — "Clear duplicate cron events" button on Sync Status
The one-shot
AF_Cron_Cleanupmigration only runs once per site, so per-hostname duplicate events created by an older build *after* the migration had already marked itself done were never swept. NewAF_Cron_Cleanup::force_sweep()runs the same sweep on demand, exposed via a Clear Cron Dupes button in the Sync Queue toolbar (AJAXauctionforge_clear_cron_dupes). It removes legacy-args duplicates of the four per-hostname hooks plus the two orphan hooks, keeping one clean event each.Internal
- Modified:
inc/stats/class-stats-init.php— syncregister()calls wrapped inadd_action('init', …, 20). - Modified:
inc/maintenance/class-cron-cleanup.php— new publicforce_sweep()reusing the existing sweep helpers. - Modified:
auctionforge.php—auctionforge_ajax_run_cron_task()output-buffered against stray output; newauctionforge_ajax_clear_cron_dupes()handler + registration; plugin header → 3.7.0.33. - Modified:
admin/view/sync-status.php— added the "Clear Cron Dupes" button and handler.
Note: with all scheduling now arg-free (3.7.0.28+), the duplicates do not regenerate once swept; a canonical-hostname redirect (www ↔ apex) at the web-server level remains good hygiene but is no longer required to keep the cron table clean.
---
[3.7.0.32] — 2026-05-30
Fixed — V3 sync worker never spawned on some hosts (silent import stall)
On some sites (confirmed on a LiteSpeed/cPanel install) the V3 queue worker never actually started, so catalog lots were staged into
import_historybut never converted to WordPress posts —postId IS NULL,syncFinished = 0,syncTimes = 0, and no errors logged. Clearing the queue and reprocessing re-stalled immediately.Root cause.
SyncQueue::spawnWorker()starts the worker by POSTing the REST route/wp-json/auctionforge/v1/sync-worker. It tried an HTTP loopback tohttp://127.0.0.1/with the site'sHost:header first, and the success test wasif (!is_wp_error($result))— it never inspected the HTTP status code. On hosts where127.0.0.1+Hostheader resolves to the wrong vhost (or the route is blocked), that request returns 404/403, which is not aWP_Error. The old code therefore treated the 404 as a successful spawn, loggedSpawned … (HTTP 404), returnedtrue, and never reached the HTTPS fallback. The worker never ran; nothing drained the queue or the staging table. Sites whose127.0.0.1loopback resolves correctly returned 200 and were unaffected — which is why this was host-specific.Fix.
spawnWorker()now reads the response code and only treats 2xx as a successful spawn. On any non-2xx (or aWP_Error) from the loopback it logs a warning and falls through to therest_url()HTTPS path, which is likewise status-checked. A new constant short-circuits the doomed attempt on known-bad hosts:```php
define('AUCTIONFORGE_WORKER_SKIP_LOOPBACK', true); // wp-config.php — skip 127.0.0.1, go straight to HTTPS
```
Added — Automatic staging recovery on the safety cron
The hourly
auctionforge_sync_safetycron previously only spawned a worker when the queue had pending jobs. Lots that were staged intoimport_historybut left unconverted (e.g. after a worker died, or while the spawn bug above was in effect) had no queue job attached, so nothing ever picked them up. The safety cron now also checksSyncQueue::stagingBacklogCount()and, when it is non-zero, kicks the staging processor (non-blocking) to convert the backlog into posts — making the pipeline self-healing without manual intervention.Added — "Process Staging" button on Sync Status
A new Process Staging button in the Sync Queue toolbar (Lots → Sync Status) runs
InsertLotsBSover the staging table on demand — for when lots are staged but not appearing on the site. It fires non-blocking and switches the Live Console into fast-poll so the operator can watch posts being created in batches of 50.Internal
- Modified:
inc/sync/SyncQueue.php—spawnWorker()rewritten with HTTP-status-aware fallback +AUCTIONFORGE_WORKER_SKIP_LOOPBACKsupport;triggerStagingProcessor()gained a$blockingparameter (non-blocking fire-and-forget mode for cron/admin); newstagingBacklogCount()helper (countsimport_historyrows withsyncFinished = 0). - Modified:
auctionforge.php—auctionforge_sync_safety_action()now drains orphaned staging via the non-blocking processor when a backlog exists; newprocess_stagingcase in theauctionforge_sync_enqueueAJAX handler; plugin header bumped to 3.7.0.32. - Modified:
admin/view/sync-status.php— added the "Process Staging" button and its click handler.
---
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.7.1.14 Stable Jul 23, 2026Download v3.7.1.14📦 9.41 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.1.12] — 2026-07-20
Fixed — Open-invoices banner ignored the "Enable My Invoices tab" setting
The site-wide "⚠ This account has open invoices / Pay Now" banner rendered for logged-in users with unpaid BidSpirit bills even on sites where the My Invoices profile tab is disabled — and its "Pay Now" button linked to
#bp-bills, a tab that does not exist on those sites, so it led nowhere.checkUnpaidBillsBanner()had no gate on the setting.BidspiritViews::bp_footer()now emitswindow.afBillingTabEnabled, computed from the sameenable_billing_tabexpression that gates the tab markup inprofile-templates.php(so banner and tab can never disagree), andcheckUnpaidBillsBanner()returns immediately when it isfalse— no banner, and nobills/getUnpaidBills/ unpaid-status AJAX traffic on such sites. Sites with the tab enabled are unchanged. No schema or config-version change.Fixed — US auction houses resolved to the UK "Catalogue" dialect in Auto mode (regression)
Restores the address-signature detection that was lost in an earlier merge (originally fixed 2026-06-26, silently reverted by a later version sync). US house addresses typically name no country and end in "City, ST 12345", so
Dialect::captureRegionFromAuctions()took the "ST 12345" tail as an unknown country name and resolved UK.countryToDialect()now takes the full address and recognises a US state code + 5-digit ZIP tail (constUS_STATE_CODES, uppercase-only match), plus a bare trailing ZIP with a$currency as a secondary signal. Conservative by design: named countries still resolve UK — Canadian ("ON M5V 3L9") and Australian ("WA 6000", 4-digit postcode) tails cannot match the 5-digit requirement.Fixed — LiteSpeed cache purges during sync stacked "Purge url" admin notices
CachePurgepurged per-lot URLs via the publiclitespeed_purge_urlaction, which callsPurge::purge_url()with$quite=falseand enqueues a dismissible admin notice per URL — a full-auction sync stacked hundreds of "Purge url …" notices on the next wp-admin page and looked like a fault to clients. Purges now call\LiteSpeed\Purge::cls()->purge_url($u, false, true)directly (identical purge, no notice), falling back to the action when the class is unavailable. Manual purges elsewhere keep their confirmations.Fixed — Sync log wrote one row per skipped auction
LotList::updateData()logged " Skipping auction N (excluded or not matched)" for every excluded auction on every worker run — the dominant source ofaf_sync_logtable growth on multi-house sites (observed in the GBs fleet-wide). It now counts the skips and writes a single "Skipped N/M auctions (excluded or not matched)" summary line per run. The per-auction detail remains available viaDebug::logData().Housekeeping
Removed stray
.bakworking files (auctionforge.php.bak-*,readme.txt.bak-verbump,app.js.prephonebypass-*.bak, CachePurge/BidspiritViews backups) that had been shipping inside the plugin tree since 3.7.1.4.[3.7.1.11] — 2026-07-20
Added — "Auction is live" popup on single-lot pages
When BidSpirit reports an auction as live, single-lot pages now show the "Auction is live — Enter live auction" popup (linking to the BidSpirit bidding console), exactly as auction catalogue pages already did. Previously a one-lot auction had no live indication anywhere on the site, because its catalogue URL redirects straight to the lot page and only the catalogue code path surfaced the live state. (Entry backfilled — 3.7.1.11 shipped with this note in readme.txt only. 3.7.1.10 was an internal bump with no separate changes.)
[3.7.1.9] — 2026-07-14
Fixed — Catalog pagination stayed broken for returning visitors (stale session)
Follow-up to 3.7.1.8. That fix stopped *new* sessions from getting a bad per-page baseline, but a returning visitor whose session already held the old baseline (the session cookie lives 24h) kept getting page 2+ reset to page 1 at any non-default items-per-page — only a full "clear site data" (which drops the session cookie) fixed it for them.
The server no longer resets pagination from session state at all. Per-page changes already return to page 1 on the client (the items-per-page dropdown navigates to the page-1 URL), so the session-based reset in
BidspiritLots::search_filter()andbidspirit_lot_per_page()was both redundant and the source of the stale-baseline problem — it has been removed. Pagination is now correct for every visitor (new or returning, any session state) on the next page load, with no cache/cookie clearing required. No schema or config-version change.[3.7.1.8] — 2026-07-14
Fixed — Catalog pagination broke at any non-default "Items per page"
On auction catalog pages, setting Items per page to any value other than the site's WordPress default (
posts_per_page) made page 2 and beyond redirect straight back to page 1 — so it appeared to "show the same lots" — while the value matching the default worked.BidspiritLots::bp_session_starter()seeded the per-page change-detection baseline fromget_option('posts_per_page'); because auction pages are served from full-page cache / behind a CDN that drops the PHP session cookie, that baseline reset to the default on every request, so any non-default size looked "just changed" on every page load and was force-redirected to page 1 bymaybe_redirect_to_first_page().The baseline is now seeded to
null("unknown previous value"), matching the existing category/search baselines, so a page reset only fires when a genuinely-known prior per-page value differs (isset && !== null && !=). The items-per-page dropdown additionally returns to page 1 client-side, so "change size → page 1" no longer depends on the session. No schema or config-version change.[3.7.1.7] — 2026-07-13
Added — Per-house toggle to bypass phone verification (UK houses auto-bypass)
BidSpirit's
users/phone/sendPhoneVerificationCodereturns a genericUNKNOWN_ERRORfor UK houses (e.g. rosanreeves, edwards) even for valid+44mobiles, because SMS/phone-verification is not provisioned on those BidSpirit accounts. As the requirement is enforced only plugin-side, this left UK bidders unable to bid.A new per-house setting
require_phone_verification(in thebidspirit_settingsoption) makes the client-side bid gate conditional:auto(default) — enforce EXCEPT on UK houses (Dialect::isUk()), which are bypassed while their BidSpirit accounts lack SMS provisioning.off— never enforce (per-house override, e.g.wp option patch update bidspirit_settings require_phone_verification off).on— always enforce.
BidspiritViews::bp_footer()emitswindow.afRequirePhoneVerification; both bid gates (submitBid,submitBidCatalog) and the profile “Verify phone” CTA honour it. Fails closed (enforces) when the flag is absent. No schema or config-version change.---
[3.7.1.2] — 2026-06-30
Fixed — Background sync worker spawn timing out ("Worker spawn HTTPS failed: cURL error 28")
On busy or memory-pressured hosts the background sync worker frequently failed to start, logging
Worker spawn HTTPS failed: cURL error 28: Operation timed out after 5002 milliseconds with 0 bytes received(observed ~29×/day on one site).SyncQueue::spawnWorker()triggers the worker with a blocking loopback HTTP request to the site's own REST endpoint; the timeout was hard-coded to 5 seconds, so whenever the site's own backend took longer than 5s to respond (DB/FPM contention) the spawn aborted *as a hard error* and raised a sync alert — even though the site was up and serving.Three changes make the spawn resilient:
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
AUCTIONFORGE_WORKER_SPAWN_TIMEOUTconstant or theauctionforge_worker_spawn_timeoutfilter. - Scheme-matched loopback. Strategy 1 now issues the
127.0.0.1loopback using the site's own scheme (https when the site is https) instead of always http. Previously an http→https redirect (301) made the loopback look like a failure and forced the slower public-URL fallback; the POST now reaches the worker endpoint directly. - Non-blocking best-effort fallback. If both blocking attempts can't confirm a start within the timeout, the worker is now triggered with a final non-blocking (fire-and-forget) request, logged at warning instead of error. A merely-slow backend still starts its worker, the hourly
SafetySynccron re-spawns if it genuinely didn't, and the condition no longer raises a hard sync alert.
No schema, DB, or config-version change. Behaviour is unchanged on healthy hosts (the loopback still returns 2xx immediately); the new paths engage only when the backend is slow. The underlying slow backend was a server-side issue (MariaDB query-cache lock contention + memory pressure) fixed at the infrastructure level; this release hardens the plugin so a transiently slow backend can no longer turn into a failed sync.
---
[3.7.1.1] — 2026-06-29
Added — Active uptime monitoring via Kuma keyword health marker
The keepalive mu-plugin now emits an invisible
<!-- af-health:ok -->marker into every rendered front-end page (hooked onwp_headandwp_footer, emitted once per request, pure echo — no DB/IO, lands inside cached HTML). Uptime Kuma monitors each site by pulling the homepage and asserting the marker is present, which measures real user-facing availability (DNS + TLS + nginx + PHP + WordPress + DB + render) instead of the old push-heartbeat model that only measured whether WP-Cron was firing — and routinely false-flagged perfectly healthy sites as down (the worst offenders flapped 50–60×/day while serving HTTP 200 the entire time).Because the marker lives in the mu-plugin (which WordPress can't deactivate and AuctionForge re-asserts on every load), it reflects "WordPress is rendering pages" independently of AuctionForge's own activation state, and it survives failed or in-progress plugin updates. The bundled keepalive template is bumped to 1.3.0, so the installer rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpon update. The legacy push heartbeat is retained in this release for a safe transition to active monitoring.---
[3.7.0.43] — 2026-06-22
Fixed — Submission "Need help?" contact was hardcoded (one tenant's details on every site)
The vehicle-submission help panel (
templates/js-chunks/submissions.php) rendered a single hardcoded email and phone, so every AuctionForge site displayed the same contact regardless of which auction house it was — e.g. one tenant'sinfo@absoluteclearance.co.uk/01536 674804showed on unrelated sites.The panel now resolves the contact dynamically, in priority order:
apply_filters('auctionforge_help_email', '')/apply_filters('auctionforge_help_phone', '')— explicit per-site override (any source, e.g. a theme/global/Elementor setting);- otherwise the SEO & AEO Optimizer LocalBusiness details stored in the
seo_aeo_optionsoption (local_email/local_phone); - if neither yields a value the corresponding row is omitted, and the whole "Need help?" block is hidden when no contact is available — so no placeholder or wrong-tenant detail is ever shown.
Internal
- Modified:
templates/js-chunks/submissions.php— replaced the two hardcoded<p>contact values with filtered,esc_html-escaped values resolved fromseo_aeo_options, each row guarded by anif. - Modified:
auctionforge.php— plugin header → 3.7.0.43. - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.42] — 2026-06-22
Fixed — Submission templates emitted extra
<h1>s and placeholder content site-wideThe vehicle-submission view (
templates/js-chunks/submissions.php) is rendered onwp_footeron every page, so its<script type="text/template">chunks shipped into the source of all pages. Two of those chunks used<h1>headings ("Submit your vehicle for auction" and "Detail received successfully"), meaning every page's HTML source contained three<h1>elements. While these live inside inert<script>templates (so a DOM-rendering crawler like Googlebot still resolves a single true H1), raw-source SEO tooling and some AI crawlers flag the duplicates, and the templates also shipped vendor placeholder text.- Both template headings demoted from
<h1>to<h2>— a single H1 per page in source. - Replaced Lorem-ipsum placeholder copy in the intro and progress panels with real submission guidance.
- Replaced the dummy "Need help?" contact details (
support@domain.com/+3265464325) with the live business contact (info@absoluteclearance.co.uk/01536 674804).
No functional/JS changes — template IDs and structure are unchanged.
---
[3.7.0.41] — 2026-06-19
Fixed — Duplicate auction terms (auction shown twice, catalogue split)
A newly published auction could be created twice in WordPress as two identical
auction_numberterms, so the auction rendered twice on the listing and its catalogue was split across the two copies (e.g. 145 lots stored as 144 + 1). The lot posts themselves stayed unique — this was a duplicate *auction*, not duplicate *lots*.Root cause: auction terms are only created in
Auction::save()viawp_insert_category, which inserts whenever the caller's lookup returned no term. That lookup (findAuctionByIdWithLang()→get_terms()meta_query) can return nothing for a brand-new auction when two syncs run concurrently — for example afull_sync_*bulk run (viaBulkSyncWorker/AuctionList) overlapping a catalogue webhook. Both see "no term" and both insert one. The only fallback,findAuctionByNumber(), looks up by the auction *number* slug and never matched the real term slug, so it could not catch the miss. This path is not covered by the existingCatalogSyncWorkerper-auction lock, which serialises catalogue syncs to prevent duplicate *lot posts* but does not guard *term* creation.Auction::save()now, before inserting a term:- acquires a per-auction MySQL advisory lock (
GET_LOCK('<prefix>af_term_<auctionId>', 10)) so concurrent creators serialise — the loser then finds the just-created term and updates it instead of inserting a second one; and - performs an authoritative, cache-bypassing lookup by
auction_id(directtermmeta⋈term_taxonomyquery) and switches to update-in-place when a term already exists.
Polylang-aware: under full multilingual support it only reuses a term in the requested language, so legitimate per-language terms are still created.
Internal
- Modified:
inc/model/Auction.php—save()gains a pre-insert dedup guard; new private helpersfindTermIdByAuctionId(),acquireTermLock(),releaseTermLock().InstanceDBwraps the global$wpdb, so the advisory lock and thewp_insert_categoryinsert share a single DB session (the same property that makes the existingCatalogSyncWorkerlock effective). - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.40] — 2026-06-14
Fixed — Lot "begin to close" countdown now shows minutes (and seconds in the final minute)
The lot-page countdown (
#auctionStartTimer, the *"Auction will begin to close in …"* line) only ever rendered whole days and whole hours, and computed once on page load. With less than an hour to go it therefore read "0 days and 0 hours" — e.g. a lot closing at 20:00 looked like a dead/expired timer from 19:00 onwards, even though ~46 minutes remained. The time maths was correct; the widget simply had no sub-hour granularity and never ticked.It now updates every second and shows the largest meaningful units, dropping leading zero units:
44 minutes•3 hours and 20 minutes•1 day, 2 hours and 5 minutes- In the final minute it counts the seconds down (
40 seconds→1 second) so it never sits on a flat "0". - Once the close time passes (
result < 0) the timer hides itself, as before.
getCountdownText()now wraps the suffix in a.bp-start-countdownspan so the script can rewrite the whole phrase; if that wrapper is ever absent (e.g. a stale page-cached markup),getLeftDate()falls back to the original days+hours behaviour rather than erroring — important becauseapp.jsalso carries the live bidding logic.Internal
- Modified:
inc/controllers/Settings.php—getCountdownText()suffix wrapped in.bp-start-countdown. - Modified:
assets/js/app.js—getLeftDate()rewritten: 1s ticking viasetInterval, days/hours/minutes/seconds, zero-unit omission, pluralisation, and a wrapper-missing fallback. This is the file actually served (front-end$jsFileis hardcoded to.js). - Modified:
src/js/app.js— samegetLeftDate()change, to keep the gulp source in sync.app.min.jsis intentionally not regenerated (unused — the unminifiedapp.jsis enqueued). - Modified:
auctionforge.php— plugin header → 3.7.0.40. (3.7.0.39 was present in the header with no changelog entry; this release supersedes it.)
---
[3.7.0.38] — 2026-06-02
Added — Spelling Dialect "Auto" mode (match the auction house region)
The Spelling Dialect setting (Settings → Appearance) gains a third option, "Auto — match the auction house region", which becomes the new default for sites that haven't chosen one:
- During the existing auction sync (
getAllAuctions),Dialect::captureRegionFromAuctions()reads the auction house's country from the auctionaddress(trailing segment;currencyas a fallback) and stores the resolved dialect in thespelling_dialect_autooption. No API calls happen on page loads — it's a fixed, site-wide value refreshed each sync, so it's fully page-cache-safe. - Mapping: US → Catalog; every other country → Catalogue (American "Catalog" is US-only; UK/IE/AU/NZ/CA/etc. use "Catalogue").
- The admin field shows what was detected, e.g. *"Detected region: United Kingdom → Catalogue."* United States and United Kingdom remain manual hard-overrides.
Default change: new/unconfigured sites now default to Auto (was UK). Until a site's first sync resolves the region, Auto falls back to UK, so behaviour is unchanged until detection kicks in — then it self-corrects (a US house starts showing "Catalog" automatically).
Internal
- Modified:
inc/helpers/Dialect.php—automode,FALLBACK_DIALECT,countryToDialect(),captureRegionFromAuctions();DEFAULT_DIALECT→auto. - Modified:
inc/import/AuctionList.php— capture the house region during the auctions sync. - Modified:
inc/controllers/Settings.php— Auto dropdown option + detected-region note. - Modified:
auctionforge.php— plugin header → 3.7.0.38.
---
[3.7.0.37] — 2026-06-02
Fixed — Per-lot "full price VAT" now drives the VAT statement
The standalone VAT line on lot pages (
vat-and-commission→.vat-field) was keyed off the auction-house VAT rule, so it always read *"VAT: X% on item price and commission"* regardless of a lot's full-price-VAT toggle — and toggling a lot off didn't change it. It now branches on the live per-lot flag (itemInfo.item.fullPriceVat), matching the price-breakdown calc ingetFullPriceInfo():- Full-price VAT on → *"VAT: X% on item price and commission"* (VAT applied to hammer + commission)
- off (default) → *"VAT: X% on commission only"*
This only changes the wording to match the amount
getFullPriceInfo()already computes per the toggle; the VAT figure itself was already correct. The separaterenderVatField()"no VAT on commission" path is unchanged.Internal
- Modified:
templates/js-chunks/vat-and-commission.php— VAT statement branches on the per-lotfullPriceVatinstead ofhouseCommissionVatRule. - Modified:
auctionforge.php— plugin header → 3.7.0.37.
---
[3.7.0.36] — 2026-06-02
A Site Statistics release: day-of-week analysis, an opt-in weekly email report, lot-focused "top pages", and several dashboard refinements.
Added — Day-of-week tabs on the hourly charts
The Hourly Distribution (Site Statistics → Overview) and Peak Viewing Times (Lot Analytics) charts now have a day tab strip — All days · Mon · Tue … Sun — each showing that weekday's total. Selecting a day redraws the 24-hour chart consolidated across every matching date in the range, so you can see *which days and times* visitors are active. The lot-analytics data already carried
day_of_week; it was previously flattened to 24 hours and is now surfaced.Added — Scheduled weekly email report (opt-in, off by default)
A new report under Site Statistics → Settings emails a polished weekly summary: headline KPI cards (Visitors / Pageviews / Sessions with this-week-vs-last-week change), a visitor journey funnel (Visitors → Viewed a lot → Left a bid), busiest hour for each weekday, and the week's most-viewed lots. Driven by a daily cron that sends once on a configurable weekday. Supports one or several recipients (comma/semicolon/space/newline separated, validated and de-duped) and a "Send test now" button that sends to whatever is currently in the Recipient(s) box. Built table-based with inline CSS for Gmail/Outlook/Apple Mail compatibility.
Added — New vs Returning visitors, auction markers, engagement funnel
- New vs Returning visitor KPI cards on the Overview (new = authoritative
af_stats_visitors.first_seenwithin the period, joined so it can never exceed total visitors). - Auction-date markers drawn on the *Traffic Over Time* chart (dashed lines + a captioned list) so traffic can be read against sale dates.
- Engagement funnel panel: Visitors → Viewed a lot → Viewed 5+ lots → Bidders (bidders from synced
af_bid_events).
Changed — "Top pages" is now lot-focused ("Top Lots" / "Most viewed lots")
Both the dashboard Overview panel and the email now list the most-viewed individual lot pages only (
/lot-…), excluding home, catalogue listing/pagination and profile pages as low-signal. Email page titles are shown in full (no mid-word truncation) with the site-name suffix stripped; the dedicated Pages tab remains the unfiltered drill-down.Changed — Lot Analytics "Peak Viewing Times" honours the global date range
Previously hard-coded to the last 30 days; it now respects the dashboard's range picker. The Lot Analytics tab also auto-loads on first click (previously required a manual refresh).
Changed — Chart.js bundled locally
The Statistics and plugin dashboards loaded Chart.js from a CDN at two different versions (
@4and@4.4.1). Chart.js 4.4.1 is now bundled atadmin/assets/js/chart.umd.min.jsand loaded from the plugin (version-busted), so charts work offline / behind strict CSP and the version is pinned.Internal
- Added:
inc/stats/class-stats-report.php—AF_Stats_Report(weekly cron, recipient parsing, HTML builder, "send test" AJAX).admin/assets/js/chart.umd.min.js— bundled Chart.js 4.4.1. - Modified:
inc/stats/class-stats-ajax.php— hourly-by-DOW query;new_visitors,auction_events,funnelin the dashboard payload; lot-only top pages; global date range passed to peak times; weekly-email settings saved. - Modified:
inc/model/LotAnalytics.php—getPeakViewingTimes()accepts an explicit$from/$torange. - Modified:
inc/stats/class-stats-init.php— require + registerAF_Stats_Report. - Modified:
admin/view/stats/dashboard.php— day-of-week tabs, KPI cards, auction-marker plugin, funnel, Top Lots, weekly-email settings UI + test button, local Chart.js. - Modified:
admin/view/dashboard.php— local Chart.js. - Modified:
auctionforge.php— plugin header → 3.7.0.36.
---
[3.7.0.35] — 2026-06-02
Added — Regional spelling dialect ("Catalog" US / "Catalogue" UK)
A new Spelling Dialect dropdown at the top of Settings → Appearance lets each site choose how the word "catalog" is presented on the public front end:
- United States — Catalog — US spelling (the plugin's original authored wording).
- United Kingdom — Catalogue — UK spelling.
When UK is selected, every front-end occurrence is rewritten — *"Catalogue will be available soon"*, *"View catalogue"*, *"Catalogue Coming Soon"*, *"Download catalogue"*, *"Back to the catalogue"*, the lot-page Catalogue tab, and the "catalogue published" app notification.
How it works. Every user-facing catalog string already passes through the
auctionforgetext domain (__()), so the swap is implemented as a singlegettext/gettext_with_contextfilter rather than by editing template files.Dialect::toUk()rewrites whole words only, preserving case and pluralisation (Catalog→Catalogue,View catalog→View catalogue,CATALOG→CATALOGUE,Catalogs→Catalogues), and is idempotent. The filter is front-end only (is_admin()-guarded, with awp_doing_ajax()exception so front-end AJAX is still covered) and is scoped to theauctionforgedomain, so it never touches WordPress core, the theme, or other plugins. Code identifiers, option keys (catalog_layout,bids_in_catalog), CSS classes (af-catalog-sidebar,catalog-section,bp-back-to-catalog-button) and JS variables are all left intact. On US sites the filter is not registered at all, so there is zero added overhead.Internal
- Added:
inc/helpers/Dialect.php—Dialecthelper:get()/isUk()(reads thespelling_dialectoption, default UK),word()for non-translated strings, the case/plural-preservingtoUk()transform, thegettextfilter callbacks, andregister()(no-op on US sites). - Modified:
inc/coreIncludes.php— requireDialect.phpand callDialect::register()at plugin load. - Modified:
inc/controllers/Settings.php—usetheDialecthelper; add the "Regional Spelling" sub-heading and Spelling Dialect field at the top of the Appearance section, plus thefill_bidspirit_options_spelling_dialect()renderer. The value persists through the existingbidspirit_settingsoption and sanitize callback. - Modified:
auctionforge.php— plugin header → 3.7.0.35.
Note for existing sites. The default dialect is UK ("Catalogue"). Any site that upgrades without an explicit
spelling_dialectvalue will therefore display "Catalogue" on the front end. Sites that should keep US spelling must set Settings → Appearance → Spelling Dialect to United States — Catalog (stored asspelling_dialect = us). Admin labels are unaffected in either dialect.---
[3.7.0.34] — 2026-05-30
Fixed — Duplicate lot posts created by concurrent catalog syncs for the same auction
When two catalog-sync jobs for the same auction ran at the same time, every lot was imported twice and the front-end showed each lot as a pair (the second post taking a
-2slug). Observed on a live sale: 157 lots became 313 publishedlotposts.Root cause. Two BidSpirit webhooks arriving in the same second (e.g.
CATALOG_PUBLISHED+AUCTION_CATALOG_UPDATED) each enqueue acatalog_sync.SyncQueue::enqueue()deduplicates with a non-atomic *SELECT-then-INSERT* (no DB unique constraint), so two simultaneous calls both pass the "already pending?" check and both jobs land in the queue. Job *claiming* is atomic per-job, but nothing serialised two different jobs for the same auction, so two workers ran concurrently. The matcherLot::findByBSMeta()only sees committedpublish/draftposts, so during the overlapping insert windows each worker saw "no existing post" for every lot and inserted a fresh full set. Worse,findByBSMetadoesGROUP BY lotId, so the resulting duplicate pair is collapsed to a single row in every later sync — the importer never detects or repairs it, and thecount($datum) > 1"Double" guard inInsertLotsBS::init()is effectively dead code.Fix.
CatalogSyncWorker::process()andprocessDayEnded()now take a per-auction MySQL advisory lock (GET_LOCK('<prefix>af_sync_<auctionId>', 0)) around the API-fetch + post-insertion work. If another worker already holds the lock for that auction, the duplicate job logsconcurrent-lock busyand returns cleanly — marked completed, since the in-flight sync already covers the work — instead of racing. The lock is held on the worker's own DB connection and auto-releases if the process dies, so it can never deadlock the queue. This serialises all catalog and day-ended processing per auction no matter how many duplicate jobs reach the queue.Internal
- Modified:
inc/sync/CatalogSyncWorker.php— newacquireAuctionLock()/releaseAuctionLock()helpers;process()andprocessDayEnded()acquire the lock up front (skip-on-busy) and release it in afinally. - Modified:
auctionforge.php— plugin header → 3.7.0.34.
Note for existing sites. This prevents *new* duplicates; it does not remove ones already created. To find them, look for published
lotposts where two share one BidSpirit lot id (bp_meta_data.id). Keep the most-recently-synced post per lot id and set the rest topost_status = 'trash'— thelotpost type has trash disabled, so do it via SQL, and do not usedraft(whichfindByBSMetastill matches). Then recount theauction_numberterm and clear caches.---
[3.7.0.33] — 2026-05-30
Fixed —
auctionforge_sync_lotscron never scheduled (Lots push to wpdeploy silently stopped)AF_Lots_Syncuses the custombp_ten_minutelyschedule. The sync crons were registered fromAF_Stats_Init::init(), which runs at plugin-load time — *before*auctionforge.phpaddsbp_ten_minutelyto thecron_schedulesfilter further down the same file. Sowp_schedule_event(time(), 'bp_ten_minutely', 'auctionforge_sync_lots')ran against an unknown schedule, failed silently, and left the hook permanently unscheduled. The other four sync hooks use core'shourlyand were unaffected — which is why only the Lots push to wpdeploy went stale (observed: 3 days with no push while every other stream ran on time). Fixed by deferring the syncregister()calls to theinithook, by which point every custom schedule is registered. The hook self-arms on the next request after upgrade.Fixed — Safety Sync admin run reported a false "Request failed (HTTP 200)"
auctionforge_ajax_run_cron_taskreturns its result as JSON, but a stray PHP notice/deprecation emitted by a task (e.g. the Safety Sync BidSpirit delta-check on PHP 8.1) printed into the response body ahead of the JSON, so the browser couldn't parse it and the modal showed a misleading "Request failed (HTTP 200)" even though the task succeeded. The handler now buffers task output, discards it before emitting JSON, and logs any suppressed output viaSyncLogger(contextadmin) so the offending notice can still be found and fixed at the root.Added — "Clear duplicate cron events" button on Sync Status
The one-shot
AF_Cron_Cleanupmigration only runs once per site, so per-hostname duplicate events created by an older build *after* the migration had already marked itself done were never swept. NewAF_Cron_Cleanup::force_sweep()runs the same sweep on demand, exposed via a Clear Cron Dupes button in the Sync Queue toolbar (AJAXauctionforge_clear_cron_dupes). It removes legacy-args duplicates of the four per-hostname hooks plus the two orphan hooks, keeping one clean event each.Internal
- Modified:
inc/stats/class-stats-init.php— syncregister()calls wrapped inadd_action('init', …, 20). - Modified:
inc/maintenance/class-cron-cleanup.php— new publicforce_sweep()reusing the existing sweep helpers. - Modified:
auctionforge.php—auctionforge_ajax_run_cron_task()output-buffered against stray output; newauctionforge_ajax_clear_cron_dupes()handler + registration; plugin header → 3.7.0.33. - Modified:
admin/view/sync-status.php— added the "Clear Cron Dupes" button and handler.
Note: with all scheduling now arg-free (3.7.0.28+), the duplicates do not regenerate once swept; a canonical-hostname redirect (www ↔ apex) at the web-server level remains good hygiene but is no longer required to keep the cron table clean.
---
[3.7.0.32] — 2026-05-30
Fixed — V3 sync worker never spawned on some hosts (silent import stall)
On some sites (confirmed on a LiteSpeed/cPanel install) the V3 queue worker never actually started, so catalog lots were staged into
import_historybut never converted to WordPress posts —postId IS NULL,syncFinished = 0,syncTimes = 0, and no errors logged. Clearing the queue and reprocessing re-stalled immediately.Root cause.
SyncQueue::spawnWorker()starts the worker by POSTing the REST route/wp-json/auctionforge/v1/sync-worker. It tried an HTTP loopback tohttp://127.0.0.1/with the site'sHost:header first, and the success test wasif (!is_wp_error($result))— it never inspected the HTTP status code. On hosts where127.0.0.1+Hostheader resolves to the wrong vhost (or the route is blocked), that request returns 404/403, which is not aWP_Error. The old code therefore treated the 404 as a successful spawn, loggedSpawned … (HTTP 404), returnedtrue, and never reached the HTTPS fallback. The worker never ran; nothing drained the queue or the staging table. Sites whose127.0.0.1loopback resolves correctly returned 200 and were unaffected — which is why this was host-specific.Fix.
spawnWorker()now reads the response code and only treats 2xx as a successful spawn. On any non-2xx (or aWP_Error) from the loopback it logs a warning and falls through to therest_url()HTTPS path, which is likewise status-checked. A new constant short-circuits the doomed attempt on known-bad hosts:```php
define('AUCTIONFORGE_WORKER_SKIP_LOOPBACK', true); // wp-config.php — skip 127.0.0.1, go straight to HTTPS
```
Added — Automatic staging recovery on the safety cron
The hourly
auctionforge_sync_safetycron previously only spawned a worker when the queue had pending jobs. Lots that were staged intoimport_historybut left unconverted (e.g. after a worker died, or while the spawn bug above was in effect) had no queue job attached, so nothing ever picked them up. The safety cron now also checksSyncQueue::stagingBacklogCount()and, when it is non-zero, kicks the staging processor (non-blocking) to convert the backlog into posts — making the pipeline self-healing without manual intervention.Added — "Process Staging" button on Sync Status
A new Process Staging button in the Sync Queue toolbar (Lots → Sync Status) runs
InsertLotsBSover the staging table on demand — for when lots are staged but not appearing on the site. It fires non-blocking and switches the Live Console into fast-poll so the operator can watch posts being created in batches of 50.Internal
- Modified:
inc/sync/SyncQueue.php—spawnWorker()rewritten with HTTP-status-aware fallback +AUCTIONFORGE_WORKER_SKIP_LOOPBACKsupport;triggerStagingProcessor()gained a$blockingparameter (non-blocking fire-and-forget mode for cron/admin); newstagingBacklogCount()helper (countsimport_historyrows withsyncFinished = 0). - Modified:
auctionforge.php—auctionforge_sync_safety_action()now drains orphaned staging via the non-blocking processor when a backlog exists; newprocess_stagingcase in theauctionforge_sync_enqueueAJAX handler; plugin header bumped to 3.7.0.32. - Modified:
admin/view/sync-status.php— added the "Process Staging" button and its click handler.
---
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.7.1.13 Stable Jul 23, 2026Download v3.7.1.13📦 9.41 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.1.12] — 2026-07-20
Fixed — Open-invoices banner ignored the "Enable My Invoices tab" setting
The site-wide "⚠ This account has open invoices / Pay Now" banner rendered for logged-in users with unpaid BidSpirit bills even on sites where the My Invoices profile tab is disabled — and its "Pay Now" button linked to
#bp-bills, a tab that does not exist on those sites, so it led nowhere.checkUnpaidBillsBanner()had no gate on the setting.BidspiritViews::bp_footer()now emitswindow.afBillingTabEnabled, computed from the sameenable_billing_tabexpression that gates the tab markup inprofile-templates.php(so banner and tab can never disagree), andcheckUnpaidBillsBanner()returns immediately when it isfalse— no banner, and nobills/getUnpaidBills/ unpaid-status AJAX traffic on such sites. Sites with the tab enabled are unchanged. No schema or config-version change.Fixed — US auction houses resolved to the UK "Catalogue" dialect in Auto mode (regression)
Restores the address-signature detection that was lost in an earlier merge (originally fixed 2026-06-26, silently reverted by a later version sync). US house addresses typically name no country and end in "City, ST 12345", so
Dialect::captureRegionFromAuctions()took the "ST 12345" tail as an unknown country name and resolved UK.countryToDialect()now takes the full address and recognises a US state code + 5-digit ZIP tail (constUS_STATE_CODES, uppercase-only match), plus a bare trailing ZIP with a$currency as a secondary signal. Conservative by design: named countries still resolve UK — Canadian ("ON M5V 3L9") and Australian ("WA 6000", 4-digit postcode) tails cannot match the 5-digit requirement.Fixed — LiteSpeed cache purges during sync stacked "Purge url" admin notices
CachePurgepurged per-lot URLs via the publiclitespeed_purge_urlaction, which callsPurge::purge_url()with$quite=falseand enqueues a dismissible admin notice per URL — a full-auction sync stacked hundreds of "Purge url …" notices on the next wp-admin page and looked like a fault to clients. Purges now call\LiteSpeed\Purge::cls()->purge_url($u, false, true)directly (identical purge, no notice), falling back to the action when the class is unavailable. Manual purges elsewhere keep their confirmations.Fixed — Sync log wrote one row per skipped auction
LotList::updateData()logged " Skipping auction N (excluded or not matched)" for every excluded auction on every worker run — the dominant source ofaf_sync_logtable growth on multi-house sites (observed in the GBs fleet-wide). It now counts the skips and writes a single "Skipped N/M auctions (excluded or not matched)" summary line per run. The per-auction detail remains available viaDebug::logData().Housekeeping
Removed stray
.bakworking files (auctionforge.php.bak-*,readme.txt.bak-verbump,app.js.prephonebypass-*.bak, CachePurge/BidspiritViews backups) that had been shipping inside the plugin tree since 3.7.1.4.[3.7.1.11] — 2026-07-20
Added — "Auction is live" popup on single-lot pages
When BidSpirit reports an auction as live, single-lot pages now show the "Auction is live — Enter live auction" popup (linking to the BidSpirit bidding console), exactly as auction catalogue pages already did. Previously a one-lot auction had no live indication anywhere on the site, because its catalogue URL redirects straight to the lot page and only the catalogue code path surfaced the live state. (Entry backfilled — 3.7.1.11 shipped with this note in readme.txt only. 3.7.1.10 was an internal bump with no separate changes.)
[3.7.1.9] — 2026-07-14
Fixed — Catalog pagination stayed broken for returning visitors (stale session)
Follow-up to 3.7.1.8. That fix stopped *new* sessions from getting a bad per-page baseline, but a returning visitor whose session already held the old baseline (the session cookie lives 24h) kept getting page 2+ reset to page 1 at any non-default items-per-page — only a full "clear site data" (which drops the session cookie) fixed it for them.
The server no longer resets pagination from session state at all. Per-page changes already return to page 1 on the client (the items-per-page dropdown navigates to the page-1 URL), so the session-based reset in
BidspiritLots::search_filter()andbidspirit_lot_per_page()was both redundant and the source of the stale-baseline problem — it has been removed. Pagination is now correct for every visitor (new or returning, any session state) on the next page load, with no cache/cookie clearing required. No schema or config-version change.[3.7.1.8] — 2026-07-14
Fixed — Catalog pagination broke at any non-default "Items per page"
On auction catalog pages, setting Items per page to any value other than the site's WordPress default (
posts_per_page) made page 2 and beyond redirect straight back to page 1 — so it appeared to "show the same lots" — while the value matching the default worked.BidspiritLots::bp_session_starter()seeded the per-page change-detection baseline fromget_option('posts_per_page'); because auction pages are served from full-page cache / behind a CDN that drops the PHP session cookie, that baseline reset to the default on every request, so any non-default size looked "just changed" on every page load and was force-redirected to page 1 bymaybe_redirect_to_first_page().The baseline is now seeded to
null("unknown previous value"), matching the existing category/search baselines, so a page reset only fires when a genuinely-known prior per-page value differs (isset && !== null && !=). The items-per-page dropdown additionally returns to page 1 client-side, so "change size → page 1" no longer depends on the session. No schema or config-version change.[3.7.1.7] — 2026-07-13
Added — Per-house toggle to bypass phone verification (UK houses auto-bypass)
BidSpirit's
users/phone/sendPhoneVerificationCodereturns a genericUNKNOWN_ERRORfor UK houses (e.g. rosanreeves, edwards) even for valid+44mobiles, because SMS/phone-verification is not provisioned on those BidSpirit accounts. As the requirement is enforced only plugin-side, this left UK bidders unable to bid.A new per-house setting
require_phone_verification(in thebidspirit_settingsoption) makes the client-side bid gate conditional:auto(default) — enforce EXCEPT on UK houses (Dialect::isUk()), which are bypassed while their BidSpirit accounts lack SMS provisioning.off— never enforce (per-house override, e.g.wp option patch update bidspirit_settings require_phone_verification off).on— always enforce.
BidspiritViews::bp_footer()emitswindow.afRequirePhoneVerification; both bid gates (submitBid,submitBidCatalog) and the profile “Verify phone” CTA honour it. Fails closed (enforces) when the flag is absent. No schema or config-version change.---
[3.7.1.2] — 2026-06-30
Fixed — Background sync worker spawn timing out ("Worker spawn HTTPS failed: cURL error 28")
On busy or memory-pressured hosts the background sync worker frequently failed to start, logging
Worker spawn HTTPS failed: cURL error 28: Operation timed out after 5002 milliseconds with 0 bytes received(observed ~29×/day on one site).SyncQueue::spawnWorker()triggers the worker with a blocking loopback HTTP request to the site's own REST endpoint; the timeout was hard-coded to 5 seconds, so whenever the site's own backend took longer than 5s to respond (DB/FPM contention) the spawn aborted *as a hard error* and raised a sync alert — even though the site was up and serving.Three changes make the spawn resilient:
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
AUCTIONFORGE_WORKER_SPAWN_TIMEOUTconstant or theauctionforge_worker_spawn_timeoutfilter. - Scheme-matched loopback. Strategy 1 now issues the
127.0.0.1loopback using the site's own scheme (https when the site is https) instead of always http. Previously an http→https redirect (301) made the loopback look like a failure and forced the slower public-URL fallback; the POST now reaches the worker endpoint directly. - Non-blocking best-effort fallback. If both blocking attempts can't confirm a start within the timeout, the worker is now triggered with a final non-blocking (fire-and-forget) request, logged at warning instead of error. A merely-slow backend still starts its worker, the hourly
SafetySynccron re-spawns if it genuinely didn't, and the condition no longer raises a hard sync alert.
No schema, DB, or config-version change. Behaviour is unchanged on healthy hosts (the loopback still returns 2xx immediately); the new paths engage only when the backend is slow. The underlying slow backend was a server-side issue (MariaDB query-cache lock contention + memory pressure) fixed at the infrastructure level; this release hardens the plugin so a transiently slow backend can no longer turn into a failed sync.
---
[3.7.1.1] — 2026-06-29
Added — Active uptime monitoring via Kuma keyword health marker
The keepalive mu-plugin now emits an invisible
<!-- af-health:ok -->marker into every rendered front-end page (hooked onwp_headandwp_footer, emitted once per request, pure echo — no DB/IO, lands inside cached HTML). Uptime Kuma monitors each site by pulling the homepage and asserting the marker is present, which measures real user-facing availability (DNS + TLS + nginx + PHP + WordPress + DB + render) instead of the old push-heartbeat model that only measured whether WP-Cron was firing — and routinely false-flagged perfectly healthy sites as down (the worst offenders flapped 50–60×/day while serving HTTP 200 the entire time).Because the marker lives in the mu-plugin (which WordPress can't deactivate and AuctionForge re-asserts on every load), it reflects "WordPress is rendering pages" independently of AuctionForge's own activation state, and it survives failed or in-progress plugin updates. The bundled keepalive template is bumped to 1.3.0, so the installer rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpon update. The legacy push heartbeat is retained in this release for a safe transition to active monitoring.---
[3.7.0.43] — 2026-06-22
Fixed — Submission "Need help?" contact was hardcoded (one tenant's details on every site)
The vehicle-submission help panel (
templates/js-chunks/submissions.php) rendered a single hardcoded email and phone, so every AuctionForge site displayed the same contact regardless of which auction house it was — e.g. one tenant'sinfo@absoluteclearance.co.uk/01536 674804showed on unrelated sites.The panel now resolves the contact dynamically, in priority order:
apply_filters('auctionforge_help_email', '')/apply_filters('auctionforge_help_phone', '')— explicit per-site override (any source, e.g. a theme/global/Elementor setting);- otherwise the SEO & AEO Optimizer LocalBusiness details stored in the
seo_aeo_optionsoption (local_email/local_phone); - if neither yields a value the corresponding row is omitted, and the whole "Need help?" block is hidden when no contact is available — so no placeholder or wrong-tenant detail is ever shown.
Internal
- Modified:
templates/js-chunks/submissions.php— replaced the two hardcoded<p>contact values with filtered,esc_html-escaped values resolved fromseo_aeo_options, each row guarded by anif. - Modified:
auctionforge.php— plugin header → 3.7.0.43. - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.42] — 2026-06-22
Fixed — Submission templates emitted extra
<h1>s and placeholder content site-wideThe vehicle-submission view (
templates/js-chunks/submissions.php) is rendered onwp_footeron every page, so its<script type="text/template">chunks shipped into the source of all pages. Two of those chunks used<h1>headings ("Submit your vehicle for auction" and "Detail received successfully"), meaning every page's HTML source contained three<h1>elements. While these live inside inert<script>templates (so a DOM-rendering crawler like Googlebot still resolves a single true H1), raw-source SEO tooling and some AI crawlers flag the duplicates, and the templates also shipped vendor placeholder text.- Both template headings demoted from
<h1>to<h2>— a single H1 per page in source. - Replaced Lorem-ipsum placeholder copy in the intro and progress panels with real submission guidance.
- Replaced the dummy "Need help?" contact details (
support@domain.com/+3265464325) with the live business contact (info@absoluteclearance.co.uk/01536 674804).
No functional/JS changes — template IDs and structure are unchanged.
---
[3.7.0.41] — 2026-06-19
Fixed — Duplicate auction terms (auction shown twice, catalogue split)
A newly published auction could be created twice in WordPress as two identical
auction_numberterms, so the auction rendered twice on the listing and its catalogue was split across the two copies (e.g. 145 lots stored as 144 + 1). The lot posts themselves stayed unique — this was a duplicate *auction*, not duplicate *lots*.Root cause: auction terms are only created in
Auction::save()viawp_insert_category, which inserts whenever the caller's lookup returned no term. That lookup (findAuctionByIdWithLang()→get_terms()meta_query) can return nothing for a brand-new auction when two syncs run concurrently — for example afull_sync_*bulk run (viaBulkSyncWorker/AuctionList) overlapping a catalogue webhook. Both see "no term" and both insert one. The only fallback,findAuctionByNumber(), looks up by the auction *number* slug and never matched the real term slug, so it could not catch the miss. This path is not covered by the existingCatalogSyncWorkerper-auction lock, which serialises catalogue syncs to prevent duplicate *lot posts* but does not guard *term* creation.Auction::save()now, before inserting a term:- acquires a per-auction MySQL advisory lock (
GET_LOCK('<prefix>af_term_<auctionId>', 10)) so concurrent creators serialise — the loser then finds the just-created term and updates it instead of inserting a second one; and - performs an authoritative, cache-bypassing lookup by
auction_id(directtermmeta⋈term_taxonomyquery) and switches to update-in-place when a term already exists.
Polylang-aware: under full multilingual support it only reuses a term in the requested language, so legitimate per-language terms are still created.
Internal
- Modified:
inc/model/Auction.php—save()gains a pre-insert dedup guard; new private helpersfindTermIdByAuctionId(),acquireTermLock(),releaseTermLock().InstanceDBwraps the global$wpdb, so the advisory lock and thewp_insert_categoryinsert share a single DB session (the same property that makes the existingCatalogSyncWorkerlock effective). - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.40] — 2026-06-14
Fixed — Lot "begin to close" countdown now shows minutes (and seconds in the final minute)
The lot-page countdown (
#auctionStartTimer, the *"Auction will begin to close in …"* line) only ever rendered whole days and whole hours, and computed once on page load. With less than an hour to go it therefore read "0 days and 0 hours" — e.g. a lot closing at 20:00 looked like a dead/expired timer from 19:00 onwards, even though ~46 minutes remained. The time maths was correct; the widget simply had no sub-hour granularity and never ticked.It now updates every second and shows the largest meaningful units, dropping leading zero units:
44 minutes•3 hours and 20 minutes•1 day, 2 hours and 5 minutes- In the final minute it counts the seconds down (
40 seconds→1 second) so it never sits on a flat "0". - Once the close time passes (
result < 0) the timer hides itself, as before.
getCountdownText()now wraps the suffix in a.bp-start-countdownspan so the script can rewrite the whole phrase; if that wrapper is ever absent (e.g. a stale page-cached markup),getLeftDate()falls back to the original days+hours behaviour rather than erroring — important becauseapp.jsalso carries the live bidding logic.Internal
- Modified:
inc/controllers/Settings.php—getCountdownText()suffix wrapped in.bp-start-countdown. - Modified:
assets/js/app.js—getLeftDate()rewritten: 1s ticking viasetInterval, days/hours/minutes/seconds, zero-unit omission, pluralisation, and a wrapper-missing fallback. This is the file actually served (front-end$jsFileis hardcoded to.js). - Modified:
src/js/app.js— samegetLeftDate()change, to keep the gulp source in sync.app.min.jsis intentionally not regenerated (unused — the unminifiedapp.jsis enqueued). - Modified:
auctionforge.php— plugin header → 3.7.0.40. (3.7.0.39 was present in the header with no changelog entry; this release supersedes it.)
---
[3.7.0.38] — 2026-06-02
Added — Spelling Dialect "Auto" mode (match the auction house region)
The Spelling Dialect setting (Settings → Appearance) gains a third option, "Auto — match the auction house region", which becomes the new default for sites that haven't chosen one:
- During the existing auction sync (
getAllAuctions),Dialect::captureRegionFromAuctions()reads the auction house's country from the auctionaddress(trailing segment;currencyas a fallback) and stores the resolved dialect in thespelling_dialect_autooption. No API calls happen on page loads — it's a fixed, site-wide value refreshed each sync, so it's fully page-cache-safe. - Mapping: US → Catalog; every other country → Catalogue (American "Catalog" is US-only; UK/IE/AU/NZ/CA/etc. use "Catalogue").
- The admin field shows what was detected, e.g. *"Detected region: United Kingdom → Catalogue."* United States and United Kingdom remain manual hard-overrides.
Default change: new/unconfigured sites now default to Auto (was UK). Until a site's first sync resolves the region, Auto falls back to UK, so behaviour is unchanged until detection kicks in — then it self-corrects (a US house starts showing "Catalog" automatically).
Internal
- Modified:
inc/helpers/Dialect.php—automode,FALLBACK_DIALECT,countryToDialect(),captureRegionFromAuctions();DEFAULT_DIALECT→auto. - Modified:
inc/import/AuctionList.php— capture the house region during the auctions sync. - Modified:
inc/controllers/Settings.php— Auto dropdown option + detected-region note. - Modified:
auctionforge.php— plugin header → 3.7.0.38.
---
[3.7.0.37] — 2026-06-02
Fixed — Per-lot "full price VAT" now drives the VAT statement
The standalone VAT line on lot pages (
vat-and-commission→.vat-field) was keyed off the auction-house VAT rule, so it always read *"VAT: X% on item price and commission"* regardless of a lot's full-price-VAT toggle — and toggling a lot off didn't change it. It now branches on the live per-lot flag (itemInfo.item.fullPriceVat), matching the price-breakdown calc ingetFullPriceInfo():- Full-price VAT on → *"VAT: X% on item price and commission"* (VAT applied to hammer + commission)
- off (default) → *"VAT: X% on commission only"*
This only changes the wording to match the amount
getFullPriceInfo()already computes per the toggle; the VAT figure itself was already correct. The separaterenderVatField()"no VAT on commission" path is unchanged.Internal
- Modified:
templates/js-chunks/vat-and-commission.php— VAT statement branches on the per-lotfullPriceVatinstead ofhouseCommissionVatRule. - Modified:
auctionforge.php— plugin header → 3.7.0.37.
---
[3.7.0.36] — 2026-06-02
A Site Statistics release: day-of-week analysis, an opt-in weekly email report, lot-focused "top pages", and several dashboard refinements.
Added — Day-of-week tabs on the hourly charts
The Hourly Distribution (Site Statistics → Overview) and Peak Viewing Times (Lot Analytics) charts now have a day tab strip — All days · Mon · Tue … Sun — each showing that weekday's total. Selecting a day redraws the 24-hour chart consolidated across every matching date in the range, so you can see *which days and times* visitors are active. The lot-analytics data already carried
day_of_week; it was previously flattened to 24 hours and is now surfaced.Added — Scheduled weekly email report (opt-in, off by default)
A new report under Site Statistics → Settings emails a polished weekly summary: headline KPI cards (Visitors / Pageviews / Sessions with this-week-vs-last-week change), a visitor journey funnel (Visitors → Viewed a lot → Left a bid), busiest hour for each weekday, and the week's most-viewed lots. Driven by a daily cron that sends once on a configurable weekday. Supports one or several recipients (comma/semicolon/space/newline separated, validated and de-duped) and a "Send test now" button that sends to whatever is currently in the Recipient(s) box. Built table-based with inline CSS for Gmail/Outlook/Apple Mail compatibility.
Added — New vs Returning visitors, auction markers, engagement funnel
- New vs Returning visitor KPI cards on the Overview (new = authoritative
af_stats_visitors.first_seenwithin the period, joined so it can never exceed total visitors). - Auction-date markers drawn on the *Traffic Over Time* chart (dashed lines + a captioned list) so traffic can be read against sale dates.
- Engagement funnel panel: Visitors → Viewed a lot → Viewed 5+ lots → Bidders (bidders from synced
af_bid_events).
Changed — "Top pages" is now lot-focused ("Top Lots" / "Most viewed lots")
Both the dashboard Overview panel and the email now list the most-viewed individual lot pages only (
/lot-…), excluding home, catalogue listing/pagination and profile pages as low-signal. Email page titles are shown in full (no mid-word truncation) with the site-name suffix stripped; the dedicated Pages tab remains the unfiltered drill-down.Changed — Lot Analytics "Peak Viewing Times" honours the global date range
Previously hard-coded to the last 30 days; it now respects the dashboard's range picker. The Lot Analytics tab also auto-loads on first click (previously required a manual refresh).
Changed — Chart.js bundled locally
The Statistics and plugin dashboards loaded Chart.js from a CDN at two different versions (
@4and@4.4.1). Chart.js 4.4.1 is now bundled atadmin/assets/js/chart.umd.min.jsand loaded from the plugin (version-busted), so charts work offline / behind strict CSP and the version is pinned.Internal
- Added:
inc/stats/class-stats-report.php—AF_Stats_Report(weekly cron, recipient parsing, HTML builder, "send test" AJAX).admin/assets/js/chart.umd.min.js— bundled Chart.js 4.4.1. - Modified:
inc/stats/class-stats-ajax.php— hourly-by-DOW query;new_visitors,auction_events,funnelin the dashboard payload; lot-only top pages; global date range passed to peak times; weekly-email settings saved. - Modified:
inc/model/LotAnalytics.php—getPeakViewingTimes()accepts an explicit$from/$torange. - Modified:
inc/stats/class-stats-init.php— require + registerAF_Stats_Report. - Modified:
admin/view/stats/dashboard.php— day-of-week tabs, KPI cards, auction-marker plugin, funnel, Top Lots, weekly-email settings UI + test button, local Chart.js. - Modified:
admin/view/dashboard.php— local Chart.js. - Modified:
auctionforge.php— plugin header → 3.7.0.36.
---
[3.7.0.35] — 2026-06-02
Added — Regional spelling dialect ("Catalog" US / "Catalogue" UK)
A new Spelling Dialect dropdown at the top of Settings → Appearance lets each site choose how the word "catalog" is presented on the public front end:
- United States — Catalog — US spelling (the plugin's original authored wording).
- United Kingdom — Catalogue — UK spelling.
When UK is selected, every front-end occurrence is rewritten — *"Catalogue will be available soon"*, *"View catalogue"*, *"Catalogue Coming Soon"*, *"Download catalogue"*, *"Back to the catalogue"*, the lot-page Catalogue tab, and the "catalogue published" app notification.
How it works. Every user-facing catalog string already passes through the
auctionforgetext domain (__()), so the swap is implemented as a singlegettext/gettext_with_contextfilter rather than by editing template files.Dialect::toUk()rewrites whole words only, preserving case and pluralisation (Catalog→Catalogue,View catalog→View catalogue,CATALOG→CATALOGUE,Catalogs→Catalogues), and is idempotent. The filter is front-end only (is_admin()-guarded, with awp_doing_ajax()exception so front-end AJAX is still covered) and is scoped to theauctionforgedomain, so it never touches WordPress core, the theme, or other plugins. Code identifiers, option keys (catalog_layout,bids_in_catalog), CSS classes (af-catalog-sidebar,catalog-section,bp-back-to-catalog-button) and JS variables are all left intact. On US sites the filter is not registered at all, so there is zero added overhead.Internal
- Added:
inc/helpers/Dialect.php—Dialecthelper:get()/isUk()(reads thespelling_dialectoption, default UK),word()for non-translated strings, the case/plural-preservingtoUk()transform, thegettextfilter callbacks, andregister()(no-op on US sites). - Modified:
inc/coreIncludes.php— requireDialect.phpand callDialect::register()at plugin load. - Modified:
inc/controllers/Settings.php—usetheDialecthelper; add the "Regional Spelling" sub-heading and Spelling Dialect field at the top of the Appearance section, plus thefill_bidspirit_options_spelling_dialect()renderer. The value persists through the existingbidspirit_settingsoption and sanitize callback. - Modified:
auctionforge.php— plugin header → 3.7.0.35.
Note for existing sites. The default dialect is UK ("Catalogue"). Any site that upgrades without an explicit
spelling_dialectvalue will therefore display "Catalogue" on the front end. Sites that should keep US spelling must set Settings → Appearance → Spelling Dialect to United States — Catalog (stored asspelling_dialect = us). Admin labels are unaffected in either dialect.---
[3.7.0.34] — 2026-05-30
Fixed — Duplicate lot posts created by concurrent catalog syncs for the same auction
When two catalog-sync jobs for the same auction ran at the same time, every lot was imported twice and the front-end showed each lot as a pair (the second post taking a
-2slug). Observed on a live sale: 157 lots became 313 publishedlotposts.Root cause. Two BidSpirit webhooks arriving in the same second (e.g.
CATALOG_PUBLISHED+AUCTION_CATALOG_UPDATED) each enqueue acatalog_sync.SyncQueue::enqueue()deduplicates with a non-atomic *SELECT-then-INSERT* (no DB unique constraint), so two simultaneous calls both pass the "already pending?" check and both jobs land in the queue. Job *claiming* is atomic per-job, but nothing serialised two different jobs for the same auction, so two workers ran concurrently. The matcherLot::findByBSMeta()only sees committedpublish/draftposts, so during the overlapping insert windows each worker saw "no existing post" for every lot and inserted a fresh full set. Worse,findByBSMetadoesGROUP BY lotId, so the resulting duplicate pair is collapsed to a single row in every later sync — the importer never detects or repairs it, and thecount($datum) > 1"Double" guard inInsertLotsBS::init()is effectively dead code.Fix.
CatalogSyncWorker::process()andprocessDayEnded()now take a per-auction MySQL advisory lock (GET_LOCK('<prefix>af_sync_<auctionId>', 0)) around the API-fetch + post-insertion work. If another worker already holds the lock for that auction, the duplicate job logsconcurrent-lock busyand returns cleanly — marked completed, since the in-flight sync already covers the work — instead of racing. The lock is held on the worker's own DB connection and auto-releases if the process dies, so it can never deadlock the queue. This serialises all catalog and day-ended processing per auction no matter how many duplicate jobs reach the queue.Internal
- Modified:
inc/sync/CatalogSyncWorker.php— newacquireAuctionLock()/releaseAuctionLock()helpers;process()andprocessDayEnded()acquire the lock up front (skip-on-busy) and release it in afinally. - Modified:
auctionforge.php— plugin header → 3.7.0.34.
Note for existing sites. This prevents *new* duplicates; it does not remove ones already created. To find them, look for published
lotposts where two share one BidSpirit lot id (bp_meta_data.id). Keep the most-recently-synced post per lot id and set the rest topost_status = 'trash'— thelotpost type has trash disabled, so do it via SQL, and do not usedraft(whichfindByBSMetastill matches). Then recount theauction_numberterm and clear caches.---
[3.7.0.33] — 2026-05-30
Fixed —
auctionforge_sync_lotscron never scheduled (Lots push to wpdeploy silently stopped)AF_Lots_Syncuses the custombp_ten_minutelyschedule. The sync crons were registered fromAF_Stats_Init::init(), which runs at plugin-load time — *before*auctionforge.phpaddsbp_ten_minutelyto thecron_schedulesfilter further down the same file. Sowp_schedule_event(time(), 'bp_ten_minutely', 'auctionforge_sync_lots')ran against an unknown schedule, failed silently, and left the hook permanently unscheduled. The other four sync hooks use core'shourlyand were unaffected — which is why only the Lots push to wpdeploy went stale (observed: 3 days with no push while every other stream ran on time). Fixed by deferring the syncregister()calls to theinithook, by which point every custom schedule is registered. The hook self-arms on the next request after upgrade.Fixed — Safety Sync admin run reported a false "Request failed (HTTP 200)"
auctionforge_ajax_run_cron_taskreturns its result as JSON, but a stray PHP notice/deprecation emitted by a task (e.g. the Safety Sync BidSpirit delta-check on PHP 8.1) printed into the response body ahead of the JSON, so the browser couldn't parse it and the modal showed a misleading "Request failed (HTTP 200)" even though the task succeeded. The handler now buffers task output, discards it before emitting JSON, and logs any suppressed output viaSyncLogger(contextadmin) so the offending notice can still be found and fixed at the root.Added — "Clear duplicate cron events" button on Sync Status
The one-shot
AF_Cron_Cleanupmigration only runs once per site, so per-hostname duplicate events created by an older build *after* the migration had already marked itself done were never swept. NewAF_Cron_Cleanup::force_sweep()runs the same sweep on demand, exposed via a Clear Cron Dupes button in the Sync Queue toolbar (AJAXauctionforge_clear_cron_dupes). It removes legacy-args duplicates of the four per-hostname hooks plus the two orphan hooks, keeping one clean event each.Internal
- Modified:
inc/stats/class-stats-init.php— syncregister()calls wrapped inadd_action('init', …, 20). - Modified:
inc/maintenance/class-cron-cleanup.php— new publicforce_sweep()reusing the existing sweep helpers. - Modified:
auctionforge.php—auctionforge_ajax_run_cron_task()output-buffered against stray output; newauctionforge_ajax_clear_cron_dupes()handler + registration; plugin header → 3.7.0.33. - Modified:
admin/view/sync-status.php— added the "Clear Cron Dupes" button and handler.
Note: with all scheduling now arg-free (3.7.0.28+), the duplicates do not regenerate once swept; a canonical-hostname redirect (www ↔ apex) at the web-server level remains good hygiene but is no longer required to keep the cron table clean.
---
[3.7.0.32] — 2026-05-30
Fixed — V3 sync worker never spawned on some hosts (silent import stall)
On some sites (confirmed on a LiteSpeed/cPanel install) the V3 queue worker never actually started, so catalog lots were staged into
import_historybut never converted to WordPress posts —postId IS NULL,syncFinished = 0,syncTimes = 0, and no errors logged. Clearing the queue and reprocessing re-stalled immediately.Root cause.
SyncQueue::spawnWorker()starts the worker by POSTing the REST route/wp-json/auctionforge/v1/sync-worker. It tried an HTTP loopback tohttp://127.0.0.1/with the site'sHost:header first, and the success test wasif (!is_wp_error($result))— it never inspected the HTTP status code. On hosts where127.0.0.1+Hostheader resolves to the wrong vhost (or the route is blocked), that request returns 404/403, which is not aWP_Error. The old code therefore treated the 404 as a successful spawn, loggedSpawned … (HTTP 404), returnedtrue, and never reached the HTTPS fallback. The worker never ran; nothing drained the queue or the staging table. Sites whose127.0.0.1loopback resolves correctly returned 200 and were unaffected — which is why this was host-specific.Fix.
spawnWorker()now reads the response code and only treats 2xx as a successful spawn. On any non-2xx (or aWP_Error) from the loopback it logs a warning and falls through to therest_url()HTTPS path, which is likewise status-checked. A new constant short-circuits the doomed attempt on known-bad hosts:```php
define('AUCTIONFORGE_WORKER_SKIP_LOOPBACK', true); // wp-config.php — skip 127.0.0.1, go straight to HTTPS
```
Added — Automatic staging recovery on the safety cron
The hourly
auctionforge_sync_safetycron previously only spawned a worker when the queue had pending jobs. Lots that were staged intoimport_historybut left unconverted (e.g. after a worker died, or while the spawn bug above was in effect) had no queue job attached, so nothing ever picked them up. The safety cron now also checksSyncQueue::stagingBacklogCount()and, when it is non-zero, kicks the staging processor (non-blocking) to convert the backlog into posts — making the pipeline self-healing without manual intervention.Added — "Process Staging" button on Sync Status
A new Process Staging button in the Sync Queue toolbar (Lots → Sync Status) runs
InsertLotsBSover the staging table on demand — for when lots are staged but not appearing on the site. It fires non-blocking and switches the Live Console into fast-poll so the operator can watch posts being created in batches of 50.Internal
- Modified:
inc/sync/SyncQueue.php—spawnWorker()rewritten with HTTP-status-aware fallback +AUCTIONFORGE_WORKER_SKIP_LOOPBACKsupport;triggerStagingProcessor()gained a$blockingparameter (non-blocking fire-and-forget mode for cron/admin); newstagingBacklogCount()helper (countsimport_historyrows withsyncFinished = 0). - Modified:
auctionforge.php—auctionforge_sync_safety_action()now drains orphaned staging via the non-blocking processor when a backlog exists; newprocess_stagingcase in theauctionforge_sync_enqueueAJAX handler; plugin header bumped to 3.7.0.32. - Modified:
admin/view/sync-status.php— added the "Process Staging" button and its click handler.
---
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.7.1.12 Stable Jul 20, 2026Download v3.7.1.12📦 9.44 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.1.12] — 2026-07-20
Fixed — Open-invoices banner ignored the "Enable My Invoices tab" setting
The site-wide "⚠ This account has open invoices / Pay Now" banner rendered for logged-in users with unpaid BidSpirit bills even on sites where the My Invoices profile tab is disabled — and its "Pay Now" button linked to
#bp-bills, a tab that does not exist on those sites, so it led nowhere.checkUnpaidBillsBanner()had no gate on the setting.BidspiritViews::bp_footer()now emitswindow.afBillingTabEnabled, computed from the sameenable_billing_tabexpression that gates the tab markup inprofile-templates.php(so banner and tab can never disagree), andcheckUnpaidBillsBanner()returns immediately when it isfalse— no banner, and nobills/getUnpaidBills/ unpaid-status AJAX traffic on such sites. Sites with the tab enabled are unchanged. No schema or config-version change.Fixed — US auction houses resolved to the UK "Catalogue" dialect in Auto mode (regression)
Restores the address-signature detection that was lost in an earlier merge (originally fixed 2026-06-26, silently reverted by a later version sync). US house addresses typically name no country and end in "City, ST 12345", so
Dialect::captureRegionFromAuctions()took the "ST 12345" tail as an unknown country name and resolved UK.countryToDialect()now takes the full address and recognises a US state code + 5-digit ZIP tail (constUS_STATE_CODES, uppercase-only match), plus a bare trailing ZIP with a$currency as a secondary signal. Conservative by design: named countries still resolve UK — Canadian ("ON M5V 3L9") and Australian ("WA 6000", 4-digit postcode) tails cannot match the 5-digit requirement.Fixed — LiteSpeed cache purges during sync stacked "Purge url" admin notices
CachePurgepurged per-lot URLs via the publiclitespeed_purge_urlaction, which callsPurge::purge_url()with$quite=falseand enqueues a dismissible admin notice per URL — a full-auction sync stacked hundreds of "Purge url …" notices on the next wp-admin page and looked like a fault to clients. Purges now call\LiteSpeed\Purge::cls()->purge_url($u, false, true)directly (identical purge, no notice), falling back to the action when the class is unavailable. Manual purges elsewhere keep their confirmations.Fixed — Sync log wrote one row per skipped auction
LotList::updateData()logged " Skipping auction N (excluded or not matched)" for every excluded auction on every worker run — the dominant source ofaf_sync_logtable growth on multi-house sites (observed in the GBs fleet-wide). It now counts the skips and writes a single "Skipped N/M auctions (excluded or not matched)" summary line per run. The per-auction detail remains available viaDebug::logData().Housekeeping
Removed stray
.bakworking files (auctionforge.php.bak-*,readme.txt.bak-verbump,app.js.prephonebypass-*.bak, CachePurge/BidspiritViews backups) that had been shipping inside the plugin tree since 3.7.1.4. -
v3.7.1.11 Stable Jul 19, 2026Download v3.7.1.11📦 9.6 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.1.9] — 2026-07-14
Fixed — Catalog pagination stayed broken for returning visitors (stale session)
Follow-up to 3.7.1.8. That fix stopped *new* sessions from getting a bad per-page baseline, but a returning visitor whose session already held the old baseline (the session cookie lives 24h) kept getting page 2+ reset to page 1 at any non-default items-per-page — only a full "clear site data" (which drops the session cookie) fixed it for them.
The server no longer resets pagination from session state at all. Per-page changes already return to page 1 on the client (the items-per-page dropdown navigates to the page-1 URL), so the session-based reset in
BidspiritLots::search_filter()andbidspirit_lot_per_page()was both redundant and the source of the stale-baseline problem — it has been removed. Pagination is now correct for every visitor (new or returning, any session state) on the next page load, with no cache/cookie clearing required. No schema or config-version change.[3.7.1.8] — 2026-07-14
Fixed — Catalog pagination broke at any non-default "Items per page"
On auction catalog pages, setting Items per page to any value other than the site's WordPress default (
posts_per_page) made page 2 and beyond redirect straight back to page 1 — so it appeared to "show the same lots" — while the value matching the default worked.BidspiritLots::bp_session_starter()seeded the per-page change-detection baseline fromget_option('posts_per_page'); because auction pages are served from full-page cache / behind a CDN that drops the PHP session cookie, that baseline reset to the default on every request, so any non-default size looked "just changed" on every page load and was force-redirected to page 1 bymaybe_redirect_to_first_page().The baseline is now seeded to
null("unknown previous value"), matching the existing category/search baselines, so a page reset only fires when a genuinely-known prior per-page value differs (isset && !== null && !=). The items-per-page dropdown additionally returns to page 1 client-side, so "change size → page 1" no longer depends on the session. No schema or config-version change.[3.7.1.7] — 2026-07-13
Added — Per-house toggle to bypass phone verification (UK houses auto-bypass)
BidSpirit's
users/phone/sendPhoneVerificationCodereturns a genericUNKNOWN_ERRORfor UK houses (e.g. rosanreeves, edwards) even for valid+44mobiles, because SMS/phone-verification is not provisioned on those BidSpirit accounts. As the requirement is enforced only plugin-side, this left UK bidders unable to bid.A new per-house setting
require_phone_verification(in thebidspirit_settingsoption) makes the client-side bid gate conditional:auto(default) — enforce EXCEPT on UK houses (Dialect::isUk()), which are bypassed while their BidSpirit accounts lack SMS provisioning.off— never enforce (per-house override, e.g.wp option patch update bidspirit_settings require_phone_verification off).on— always enforce.
BidspiritViews::bp_footer()emitswindow.afRequirePhoneVerification; both bid gates (submitBid,submitBidCatalog) and the profile “Verify phone” CTA honour it. Fails closed (enforces) when the flag is absent. No schema or config-version change.---
[3.7.1.2] — 2026-06-30
Fixed — Background sync worker spawn timing out ("Worker spawn HTTPS failed: cURL error 28")
On busy or memory-pressured hosts the background sync worker frequently failed to start, logging
Worker spawn HTTPS failed: cURL error 28: Operation timed out after 5002 milliseconds with 0 bytes received(observed ~29×/day on one site).SyncQueue::spawnWorker()triggers the worker with a blocking loopback HTTP request to the site's own REST endpoint; the timeout was hard-coded to 5 seconds, so whenever the site's own backend took longer than 5s to respond (DB/FPM contention) the spawn aborted *as a hard error* and raised a sync alert — even though the site was up and serving.Three changes make the spawn resilient:
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
AUCTIONFORGE_WORKER_SPAWN_TIMEOUTconstant or theauctionforge_worker_spawn_timeoutfilter. - Scheme-matched loopback. Strategy 1 now issues the
127.0.0.1loopback using the site's own scheme (https when the site is https) instead of always http. Previously an http→https redirect (301) made the loopback look like a failure and forced the slower public-URL fallback; the POST now reaches the worker endpoint directly. - Non-blocking best-effort fallback. If both blocking attempts can't confirm a start within the timeout, the worker is now triggered with a final non-blocking (fire-and-forget) request, logged at warning instead of error. A merely-slow backend still starts its worker, the hourly
SafetySynccron re-spawns if it genuinely didn't, and the condition no longer raises a hard sync alert.
No schema, DB, or config-version change. Behaviour is unchanged on healthy hosts (the loopback still returns 2xx immediately); the new paths engage only when the backend is slow. The underlying slow backend was a server-side issue (MariaDB query-cache lock contention + memory pressure) fixed at the infrastructure level; this release hardens the plugin so a transiently slow backend can no longer turn into a failed sync.
---
[3.7.1.1] — 2026-06-29
Added — Active uptime monitoring via Kuma keyword health marker
The keepalive mu-plugin now emits an invisible
<!-- af-health:ok -->marker into every rendered front-end page (hooked onwp_headandwp_footer, emitted once per request, pure echo — no DB/IO, lands inside cached HTML). Uptime Kuma monitors each site by pulling the homepage and asserting the marker is present, which measures real user-facing availability (DNS + TLS + nginx + PHP + WordPress + DB + render) instead of the old push-heartbeat model that only measured whether WP-Cron was firing — and routinely false-flagged perfectly healthy sites as down (the worst offenders flapped 50–60×/day while serving HTTP 200 the entire time).Because the marker lives in the mu-plugin (which WordPress can't deactivate and AuctionForge re-asserts on every load), it reflects "WordPress is rendering pages" independently of AuctionForge's own activation state, and it survives failed or in-progress plugin updates. The bundled keepalive template is bumped to 1.3.0, so the installer rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpon update. The legacy push heartbeat is retained in this release for a safe transition to active monitoring.---
[3.7.0.43] — 2026-06-22
Fixed — Submission "Need help?" contact was hardcoded (one tenant's details on every site)
The vehicle-submission help panel (
templates/js-chunks/submissions.php) rendered a single hardcoded email and phone, so every AuctionForge site displayed the same contact regardless of which auction house it was — e.g. one tenant'sinfo@absoluteclearance.co.uk/01536 674804showed on unrelated sites.The panel now resolves the contact dynamically, in priority order:
apply_filters('auctionforge_help_email', '')/apply_filters('auctionforge_help_phone', '')— explicit per-site override (any source, e.g. a theme/global/Elementor setting);- otherwise the SEO & AEO Optimizer LocalBusiness details stored in the
seo_aeo_optionsoption (local_email/local_phone); - if neither yields a value the corresponding row is omitted, and the whole "Need help?" block is hidden when no contact is available — so no placeholder or wrong-tenant detail is ever shown.
Internal
- Modified:
templates/js-chunks/submissions.php— replaced the two hardcoded<p>contact values with filtered,esc_html-escaped values resolved fromseo_aeo_options, each row guarded by anif. - Modified:
auctionforge.php— plugin header → 3.7.0.43. - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.42] — 2026-06-22
Fixed — Submission templates emitted extra
<h1>s and placeholder content site-wideThe vehicle-submission view (
templates/js-chunks/submissions.php) is rendered onwp_footeron every page, so its<script type="text/template">chunks shipped into the source of all pages. Two of those chunks used<h1>headings ("Submit your vehicle for auction" and "Detail received successfully"), meaning every page's HTML source contained three<h1>elements. While these live inside inert<script>templates (so a DOM-rendering crawler like Googlebot still resolves a single true H1), raw-source SEO tooling and some AI crawlers flag the duplicates, and the templates also shipped vendor placeholder text.- Both template headings demoted from
<h1>to<h2>— a single H1 per page in source. - Replaced Lorem-ipsum placeholder copy in the intro and progress panels with real submission guidance.
- Replaced the dummy "Need help?" contact details (
support@domain.com/+3265464325) with the live business contact (info@absoluteclearance.co.uk/01536 674804).
No functional/JS changes — template IDs and structure are unchanged.
---
[3.7.0.41] — 2026-06-19
Fixed — Duplicate auction terms (auction shown twice, catalogue split)
A newly published auction could be created twice in WordPress as two identical
auction_numberterms, so the auction rendered twice on the listing and its catalogue was split across the two copies (e.g. 145 lots stored as 144 + 1). The lot posts themselves stayed unique — this was a duplicate *auction*, not duplicate *lots*.Root cause: auction terms are only created in
Auction::save()viawp_insert_category, which inserts whenever the caller's lookup returned no term. That lookup (findAuctionByIdWithLang()→get_terms()meta_query) can return nothing for a brand-new auction when two syncs run concurrently — for example afull_sync_*bulk run (viaBulkSyncWorker/AuctionList) overlapping a catalogue webhook. Both see "no term" and both insert one. The only fallback,findAuctionByNumber(), looks up by the auction *number* slug and never matched the real term slug, so it could not catch the miss. This path is not covered by the existingCatalogSyncWorkerper-auction lock, which serialises catalogue syncs to prevent duplicate *lot posts* but does not guard *term* creation.Auction::save()now, before inserting a term:- acquires a per-auction MySQL advisory lock (
GET_LOCK('<prefix>af_term_<auctionId>', 10)) so concurrent creators serialise — the loser then finds the just-created term and updates it instead of inserting a second one; and - performs an authoritative, cache-bypassing lookup by
auction_id(directtermmeta⋈term_taxonomyquery) and switches to update-in-place when a term already exists.
Polylang-aware: under full multilingual support it only reuses a term in the requested language, so legitimate per-language terms are still created.
Internal
- Modified:
inc/model/Auction.php—save()gains a pre-insert dedup guard; new private helpersfindTermIdByAuctionId(),acquireTermLock(),releaseTermLock().InstanceDBwraps the global$wpdb, so the advisory lock and thewp_insert_categoryinsert share a single DB session (the same property that makes the existingCatalogSyncWorkerlock effective). - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.40] — 2026-06-14
Fixed — Lot "begin to close" countdown now shows minutes (and seconds in the final minute)
The lot-page countdown (
#auctionStartTimer, the *"Auction will begin to close in …"* line) only ever rendered whole days and whole hours, and computed once on page load. With less than an hour to go it therefore read "0 days and 0 hours" — e.g. a lot closing at 20:00 looked like a dead/expired timer from 19:00 onwards, even though ~46 minutes remained. The time maths was correct; the widget simply had no sub-hour granularity and never ticked.It now updates every second and shows the largest meaningful units, dropping leading zero units:
44 minutes•3 hours and 20 minutes•1 day, 2 hours and 5 minutes- In the final minute it counts the seconds down (
40 seconds→1 second) so it never sits on a flat "0". - Once the close time passes (
result < 0) the timer hides itself, as before.
getCountdownText()now wraps the suffix in a.bp-start-countdownspan so the script can rewrite the whole phrase; if that wrapper is ever absent (e.g. a stale page-cached markup),getLeftDate()falls back to the original days+hours behaviour rather than erroring — important becauseapp.jsalso carries the live bidding logic.Internal
- Modified:
inc/controllers/Settings.php—getCountdownText()suffix wrapped in.bp-start-countdown. - Modified:
assets/js/app.js—getLeftDate()rewritten: 1s ticking viasetInterval, days/hours/minutes/seconds, zero-unit omission, pluralisation, and a wrapper-missing fallback. This is the file actually served (front-end$jsFileis hardcoded to.js). - Modified:
src/js/app.js— samegetLeftDate()change, to keep the gulp source in sync.app.min.jsis intentionally not regenerated (unused — the unminifiedapp.jsis enqueued). - Modified:
auctionforge.php— plugin header → 3.7.0.40. (3.7.0.39 was present in the header with no changelog entry; this release supersedes it.)
---
[3.7.0.38] — 2026-06-02
Added — Spelling Dialect "Auto" mode (match the auction house region)
The Spelling Dialect setting (Settings → Appearance) gains a third option, "Auto — match the auction house region", which becomes the new default for sites that haven't chosen one:
- During the existing auction sync (
getAllAuctions),Dialect::captureRegionFromAuctions()reads the auction house's country from the auctionaddress(trailing segment;currencyas a fallback) and stores the resolved dialect in thespelling_dialect_autooption. No API calls happen on page loads — it's a fixed, site-wide value refreshed each sync, so it's fully page-cache-safe. - Mapping: US → Catalog; every other country → Catalogue (American "Catalog" is US-only; UK/IE/AU/NZ/CA/etc. use "Catalogue").
- The admin field shows what was detected, e.g. *"Detected region: United Kingdom → Catalogue."* United States and United Kingdom remain manual hard-overrides.
Default change: new/unconfigured sites now default to Auto (was UK). Until a site's first sync resolves the region, Auto falls back to UK, so behaviour is unchanged until detection kicks in — then it self-corrects (a US house starts showing "Catalog" automatically).
Internal
- Modified:
inc/helpers/Dialect.php—automode,FALLBACK_DIALECT,countryToDialect(),captureRegionFromAuctions();DEFAULT_DIALECT→auto. - Modified:
inc/import/AuctionList.php— capture the house region during the auctions sync. - Modified:
inc/controllers/Settings.php— Auto dropdown option + detected-region note. - Modified:
auctionforge.php— plugin header → 3.7.0.38.
---
[3.7.0.37] — 2026-06-02
Fixed — Per-lot "full price VAT" now drives the VAT statement
The standalone VAT line on lot pages (
vat-and-commission→.vat-field) was keyed off the auction-house VAT rule, so it always read *"VAT: X% on item price and commission"* regardless of a lot's full-price-VAT toggle — and toggling a lot off didn't change it. It now branches on the live per-lot flag (itemInfo.item.fullPriceVat), matching the price-breakdown calc ingetFullPriceInfo():- Full-price VAT on → *"VAT: X% on item price and commission"* (VAT applied to hammer + commission)
- off (default) → *"VAT: X% on commission only"*
This only changes the wording to match the amount
getFullPriceInfo()already computes per the toggle; the VAT figure itself was already correct. The separaterenderVatField()"no VAT on commission" path is unchanged.Internal
- Modified:
templates/js-chunks/vat-and-commission.php— VAT statement branches on the per-lotfullPriceVatinstead ofhouseCommissionVatRule. - Modified:
auctionforge.php— plugin header → 3.7.0.37.
---
[3.7.0.36] — 2026-06-02
A Site Statistics release: day-of-week analysis, an opt-in weekly email report, lot-focused "top pages", and several dashboard refinements.
Added — Day-of-week tabs on the hourly charts
The Hourly Distribution (Site Statistics → Overview) and Peak Viewing Times (Lot Analytics) charts now have a day tab strip — All days · Mon · Tue … Sun — each showing that weekday's total. Selecting a day redraws the 24-hour chart consolidated across every matching date in the range, so you can see *which days and times* visitors are active. The lot-analytics data already carried
day_of_week; it was previously flattened to 24 hours and is now surfaced.Added — Scheduled weekly email report (opt-in, off by default)
A new report under Site Statistics → Settings emails a polished weekly summary: headline KPI cards (Visitors / Pageviews / Sessions with this-week-vs-last-week change), a visitor journey funnel (Visitors → Viewed a lot → Left a bid), busiest hour for each weekday, and the week's most-viewed lots. Driven by a daily cron that sends once on a configurable weekday. Supports one or several recipients (comma/semicolon/space/newline separated, validated and de-duped) and a "Send test now" button that sends to whatever is currently in the Recipient(s) box. Built table-based with inline CSS for Gmail/Outlook/Apple Mail compatibility.
Added — New vs Returning visitors, auction markers, engagement funnel
- New vs Returning visitor KPI cards on the Overview (new = authoritative
af_stats_visitors.first_seenwithin the period, joined so it can never exceed total visitors). - Auction-date markers drawn on the *Traffic Over Time* chart (dashed lines + a captioned list) so traffic can be read against sale dates.
- Engagement funnel panel: Visitors → Viewed a lot → Viewed 5+ lots → Bidders (bidders from synced
af_bid_events).
Changed — "Top pages" is now lot-focused ("Top Lots" / "Most viewed lots")
Both the dashboard Overview panel and the email now list the most-viewed individual lot pages only (
/lot-…), excluding home, catalogue listing/pagination and profile pages as low-signal. Email page titles are shown in full (no mid-word truncation) with the site-name suffix stripped; the dedicated Pages tab remains the unfiltered drill-down.Changed — Lot Analytics "Peak Viewing Times" honours the global date range
Previously hard-coded to the last 30 days; it now respects the dashboard's range picker. The Lot Analytics tab also auto-loads on first click (previously required a manual refresh).
Changed — Chart.js bundled locally
The Statistics and plugin dashboards loaded Chart.js from a CDN at two different versions (
@4and@4.4.1). Chart.js 4.4.1 is now bundled atadmin/assets/js/chart.umd.min.jsand loaded from the plugin (version-busted), so charts work offline / behind strict CSP and the version is pinned.Internal
- Added:
inc/stats/class-stats-report.php—AF_Stats_Report(weekly cron, recipient parsing, HTML builder, "send test" AJAX).admin/assets/js/chart.umd.min.js— bundled Chart.js 4.4.1. - Modified:
inc/stats/class-stats-ajax.php— hourly-by-DOW query;new_visitors,auction_events,funnelin the dashboard payload; lot-only top pages; global date range passed to peak times; weekly-email settings saved. - Modified:
inc/model/LotAnalytics.php—getPeakViewingTimes()accepts an explicit$from/$torange. - Modified:
inc/stats/class-stats-init.php— require + registerAF_Stats_Report. - Modified:
admin/view/stats/dashboard.php— day-of-week tabs, KPI cards, auction-marker plugin, funnel, Top Lots, weekly-email settings UI + test button, local Chart.js. - Modified:
admin/view/dashboard.php— local Chart.js. - Modified:
auctionforge.php— plugin header → 3.7.0.36.
---
[3.7.0.35] — 2026-06-02
Added — Regional spelling dialect ("Catalog" US / "Catalogue" UK)
A new Spelling Dialect dropdown at the top of Settings → Appearance lets each site choose how the word "catalog" is presented on the public front end:
- United States — Catalog — US spelling (the plugin's original authored wording).
- United Kingdom — Catalogue — UK spelling.
When UK is selected, every front-end occurrence is rewritten — *"Catalogue will be available soon"*, *"View catalogue"*, *"Catalogue Coming Soon"*, *"Download catalogue"*, *"Back to the catalogue"*, the lot-page Catalogue tab, and the "catalogue published" app notification.
How it works. Every user-facing catalog string already passes through the
auctionforgetext domain (__()), so the swap is implemented as a singlegettext/gettext_with_contextfilter rather than by editing template files.Dialect::toUk()rewrites whole words only, preserving case and pluralisation (Catalog→Catalogue,View catalog→View catalogue,CATALOG→CATALOGUE,Catalogs→Catalogues), and is idempotent. The filter is front-end only (is_admin()-guarded, with awp_doing_ajax()exception so front-end AJAX is still covered) and is scoped to theauctionforgedomain, so it never touches WordPress core, the theme, or other plugins. Code identifiers, option keys (catalog_layout,bids_in_catalog), CSS classes (af-catalog-sidebar,catalog-section,bp-back-to-catalog-button) and JS variables are all left intact. On US sites the filter is not registered at all, so there is zero added overhead.Internal
- Added:
inc/helpers/Dialect.php—Dialecthelper:get()/isUk()(reads thespelling_dialectoption, default UK),word()for non-translated strings, the case/plural-preservingtoUk()transform, thegettextfilter callbacks, andregister()(no-op on US sites). - Modified:
inc/coreIncludes.php— requireDialect.phpand callDialect::register()at plugin load. - Modified:
inc/controllers/Settings.php—usetheDialecthelper; add the "Regional Spelling" sub-heading and Spelling Dialect field at the top of the Appearance section, plus thefill_bidspirit_options_spelling_dialect()renderer. The value persists through the existingbidspirit_settingsoption and sanitize callback. - Modified:
auctionforge.php— plugin header → 3.7.0.35.
Note for existing sites. The default dialect is UK ("Catalogue"). Any site that upgrades without an explicit
spelling_dialectvalue will therefore display "Catalogue" on the front end. Sites that should keep US spelling must set Settings → Appearance → Spelling Dialect to United States — Catalog (stored asspelling_dialect = us). Admin labels are unaffected in either dialect.---
[3.7.0.34] — 2026-05-30
Fixed — Duplicate lot posts created by concurrent catalog syncs for the same auction
When two catalog-sync jobs for the same auction ran at the same time, every lot was imported twice and the front-end showed each lot as a pair (the second post taking a
-2slug). Observed on a live sale: 157 lots became 313 publishedlotposts.Root cause. Two BidSpirit webhooks arriving in the same second (e.g.
CATALOG_PUBLISHED+AUCTION_CATALOG_UPDATED) each enqueue acatalog_sync.SyncQueue::enqueue()deduplicates with a non-atomic *SELECT-then-INSERT* (no DB unique constraint), so two simultaneous calls both pass the "already pending?" check and both jobs land in the queue. Job *claiming* is atomic per-job, but nothing serialised two different jobs for the same auction, so two workers ran concurrently. The matcherLot::findByBSMeta()only sees committedpublish/draftposts, so during the overlapping insert windows each worker saw "no existing post" for every lot and inserted a fresh full set. Worse,findByBSMetadoesGROUP BY lotId, so the resulting duplicate pair is collapsed to a single row in every later sync — the importer never detects or repairs it, and thecount($datum) > 1"Double" guard inInsertLotsBS::init()is effectively dead code.Fix.
CatalogSyncWorker::process()andprocessDayEnded()now take a per-auction MySQL advisory lock (GET_LOCK('<prefix>af_sync_<auctionId>', 0)) around the API-fetch + post-insertion work. If another worker already holds the lock for that auction, the duplicate job logsconcurrent-lock busyand returns cleanly — marked completed, since the in-flight sync already covers the work — instead of racing. The lock is held on the worker's own DB connection and auto-releases if the process dies, so it can never deadlock the queue. This serialises all catalog and day-ended processing per auction no matter how many duplicate jobs reach the queue.Internal
- Modified:
inc/sync/CatalogSyncWorker.php— newacquireAuctionLock()/releaseAuctionLock()helpers;process()andprocessDayEnded()acquire the lock up front (skip-on-busy) and release it in afinally. - Modified:
auctionforge.php— plugin header → 3.7.0.34.
Note for existing sites. This prevents *new* duplicates; it does not remove ones already created. To find them, look for published
lotposts where two share one BidSpirit lot id (bp_meta_data.id). Keep the most-recently-synced post per lot id and set the rest topost_status = 'trash'— thelotpost type has trash disabled, so do it via SQL, and do not usedraft(whichfindByBSMetastill matches). Then recount theauction_numberterm and clear caches.---
[3.7.0.33] — 2026-05-30
Fixed —
auctionforge_sync_lotscron never scheduled (Lots push to wpdeploy silently stopped)AF_Lots_Syncuses the custombp_ten_minutelyschedule. The sync crons were registered fromAF_Stats_Init::init(), which runs at plugin-load time — *before*auctionforge.phpaddsbp_ten_minutelyto thecron_schedulesfilter further down the same file. Sowp_schedule_event(time(), 'bp_ten_minutely', 'auctionforge_sync_lots')ran against an unknown schedule, failed silently, and left the hook permanently unscheduled. The other four sync hooks use core'shourlyand were unaffected — which is why only the Lots push to wpdeploy went stale (observed: 3 days with no push while every other stream ran on time). Fixed by deferring the syncregister()calls to theinithook, by which point every custom schedule is registered. The hook self-arms on the next request after upgrade.Fixed — Safety Sync admin run reported a false "Request failed (HTTP 200)"
auctionforge_ajax_run_cron_taskreturns its result as JSON, but a stray PHP notice/deprecation emitted by a task (e.g. the Safety Sync BidSpirit delta-check on PHP 8.1) printed into the response body ahead of the JSON, so the browser couldn't parse it and the modal showed a misleading "Request failed (HTTP 200)" even though the task succeeded. The handler now buffers task output, discards it before emitting JSON, and logs any suppressed output viaSyncLogger(contextadmin) so the offending notice can still be found and fixed at the root.Added — "Clear duplicate cron events" button on Sync Status
The one-shot
AF_Cron_Cleanupmigration only runs once per site, so per-hostname duplicate events created by an older build *after* the migration had already marked itself done were never swept. NewAF_Cron_Cleanup::force_sweep()runs the same sweep on demand, exposed via a Clear Cron Dupes button in the Sync Queue toolbar (AJAXauctionforge_clear_cron_dupes). It removes legacy-args duplicates of the four per-hostname hooks plus the two orphan hooks, keeping one clean event each.Internal
- Modified:
inc/stats/class-stats-init.php— syncregister()calls wrapped inadd_action('init', …, 20). - Modified:
inc/maintenance/class-cron-cleanup.php— new publicforce_sweep()reusing the existing sweep helpers. - Modified:
auctionforge.php—auctionforge_ajax_run_cron_task()output-buffered against stray output; newauctionforge_ajax_clear_cron_dupes()handler + registration; plugin header → 3.7.0.33. - Modified:
admin/view/sync-status.php— added the "Clear Cron Dupes" button and handler.
Note: with all scheduling now arg-free (3.7.0.28+), the duplicates do not regenerate once swept; a canonical-hostname redirect (www ↔ apex) at the web-server level remains good hygiene but is no longer required to keep the cron table clean.
---
[3.7.0.32] — 2026-05-30
Fixed — V3 sync worker never spawned on some hosts (silent import stall)
On some sites (confirmed on a LiteSpeed/cPanel install) the V3 queue worker never actually started, so catalog lots were staged into
import_historybut never converted to WordPress posts —postId IS NULL,syncFinished = 0,syncTimes = 0, and no errors logged. Clearing the queue and reprocessing re-stalled immediately.Root cause.
SyncQueue::spawnWorker()starts the worker by POSTing the REST route/wp-json/auctionforge/v1/sync-worker. It tried an HTTP loopback tohttp://127.0.0.1/with the site'sHost:header first, and the success test wasif (!is_wp_error($result))— it never inspected the HTTP status code. On hosts where127.0.0.1+Hostheader resolves to the wrong vhost (or the route is blocked), that request returns 404/403, which is not aWP_Error. The old code therefore treated the 404 as a successful spawn, loggedSpawned … (HTTP 404), returnedtrue, and never reached the HTTPS fallback. The worker never ran; nothing drained the queue or the staging table. Sites whose127.0.0.1loopback resolves correctly returned 200 and were unaffected — which is why this was host-specific.Fix.
spawnWorker()now reads the response code and only treats 2xx as a successful spawn. On any non-2xx (or aWP_Error) from the loopback it logs a warning and falls through to therest_url()HTTPS path, which is likewise status-checked. A new constant short-circuits the doomed attempt on known-bad hosts:```php
define('AUCTIONFORGE_WORKER_SKIP_LOOPBACK', true); // wp-config.php — skip 127.0.0.1, go straight to HTTPS
```
Added — Automatic staging recovery on the safety cron
The hourly
auctionforge_sync_safetycron previously only spawned a worker when the queue had pending jobs. Lots that were staged intoimport_historybut left unconverted (e.g. after a worker died, or while the spawn bug above was in effect) had no queue job attached, so nothing ever picked them up. The safety cron now also checksSyncQueue::stagingBacklogCount()and, when it is non-zero, kicks the staging processor (non-blocking) to convert the backlog into posts — making the pipeline self-healing without manual intervention.Added — "Process Staging" button on Sync Status
A new Process Staging button in the Sync Queue toolbar (Lots → Sync Status) runs
InsertLotsBSover the staging table on demand — for when lots are staged but not appearing on the site. It fires non-blocking and switches the Live Console into fast-poll so the operator can watch posts being created in batches of 50.Internal
- Modified:
inc/sync/SyncQueue.php—spawnWorker()rewritten with HTTP-status-aware fallback +AUCTIONFORGE_WORKER_SKIP_LOOPBACKsupport;triggerStagingProcessor()gained a$blockingparameter (non-blocking fire-and-forget mode for cron/admin); newstagingBacklogCount()helper (countsimport_historyrows withsyncFinished = 0). - Modified:
auctionforge.php—auctionforge_sync_safety_action()now drains orphaned staging via the non-blocking processor when a backlog exists; newprocess_stagingcase in theauctionforge_sync_enqueueAJAX handler; plugin header bumped to 3.7.0.32. - Modified:
admin/view/sync-status.php— added the "Process Staging" button and its click handler.
---
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.7.1.10 Stable Jul 19, 2026Download v3.7.1.10📦 9.24 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.1.9] — 2026-07-14
Fixed — Catalog pagination stayed broken for returning visitors (stale session)
Follow-up to 3.7.1.8. That fix stopped *new* sessions from getting a bad per-page baseline, but a returning visitor whose session already held the old baseline (the session cookie lives 24h) kept getting page 2+ reset to page 1 at any non-default items-per-page — only a full "clear site data" (which drops the session cookie) fixed it for them.
The server no longer resets pagination from session state at all. Per-page changes already return to page 1 on the client (the items-per-page dropdown navigates to the page-1 URL), so the session-based reset in
BidspiritLots::search_filter()andbidspirit_lot_per_page()was both redundant and the source of the stale-baseline problem — it has been removed. Pagination is now correct for every visitor (new or returning, any session state) on the next page load, with no cache/cookie clearing required. No schema or config-version change.[3.7.1.8] — 2026-07-14
Fixed — Catalog pagination broke at any non-default "Items per page"
On auction catalog pages, setting Items per page to any value other than the site's WordPress default (
posts_per_page) made page 2 and beyond redirect straight back to page 1 — so it appeared to "show the same lots" — while the value matching the default worked.BidspiritLots::bp_session_starter()seeded the per-page change-detection baseline fromget_option('posts_per_page'); because auction pages are served from full-page cache / behind a CDN that drops the PHP session cookie, that baseline reset to the default on every request, so any non-default size looked "just changed" on every page load and was force-redirected to page 1 bymaybe_redirect_to_first_page().The baseline is now seeded to
null("unknown previous value"), matching the existing category/search baselines, so a page reset only fires when a genuinely-known prior per-page value differs (isset && !== null && !=). The items-per-page dropdown additionally returns to page 1 client-side, so "change size → page 1" no longer depends on the session. No schema or config-version change.[3.7.1.7] — 2026-07-13
Added — Per-house toggle to bypass phone verification (UK houses auto-bypass)
BidSpirit's
users/phone/sendPhoneVerificationCodereturns a genericUNKNOWN_ERRORfor UK houses (e.g. rosanreeves, edwards) even for valid+44mobiles, because SMS/phone-verification is not provisioned on those BidSpirit accounts. As the requirement is enforced only plugin-side, this left UK bidders unable to bid.A new per-house setting
require_phone_verification(in thebidspirit_settingsoption) makes the client-side bid gate conditional:auto(default) — enforce EXCEPT on UK houses (Dialect::isUk()), which are bypassed while their BidSpirit accounts lack SMS provisioning.off— never enforce (per-house override, e.g.wp option patch update bidspirit_settings require_phone_verification off).on— always enforce.
BidspiritViews::bp_footer()emitswindow.afRequirePhoneVerification; both bid gates (submitBid,submitBidCatalog) and the profile “Verify phone” CTA honour it. Fails closed (enforces) when the flag is absent. No schema or config-version change.---
[3.7.1.2] — 2026-06-30
Fixed — Background sync worker spawn timing out ("Worker spawn HTTPS failed: cURL error 28")
On busy or memory-pressured hosts the background sync worker frequently failed to start, logging
Worker spawn HTTPS failed: cURL error 28: Operation timed out after 5002 milliseconds with 0 bytes received(observed ~29×/day on one site).SyncQueue::spawnWorker()triggers the worker with a blocking loopback HTTP request to the site's own REST endpoint; the timeout was hard-coded to 5 seconds, so whenever the site's own backend took longer than 5s to respond (DB/FPM contention) the spawn aborted *as a hard error* and raised a sync alert — even though the site was up and serving.Three changes make the spawn resilient:
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
AUCTIONFORGE_WORKER_SPAWN_TIMEOUTconstant or theauctionforge_worker_spawn_timeoutfilter. - Scheme-matched loopback. Strategy 1 now issues the
127.0.0.1loopback using the site's own scheme (https when the site is https) instead of always http. Previously an http→https redirect (301) made the loopback look like a failure and forced the slower public-URL fallback; the POST now reaches the worker endpoint directly. - Non-blocking best-effort fallback. If both blocking attempts can't confirm a start within the timeout, the worker is now triggered with a final non-blocking (fire-and-forget) request, logged at warning instead of error. A merely-slow backend still starts its worker, the hourly
SafetySynccron re-spawns if it genuinely didn't, and the condition no longer raises a hard sync alert.
No schema, DB, or config-version change. Behaviour is unchanged on healthy hosts (the loopback still returns 2xx immediately); the new paths engage only when the backend is slow. The underlying slow backend was a server-side issue (MariaDB query-cache lock contention + memory pressure) fixed at the infrastructure level; this release hardens the plugin so a transiently slow backend can no longer turn into a failed sync.
---
[3.7.1.1] — 2026-06-29
Added — Active uptime monitoring via Kuma keyword health marker
The keepalive mu-plugin now emits an invisible
<!-- af-health:ok -->marker into every rendered front-end page (hooked onwp_headandwp_footer, emitted once per request, pure echo — no DB/IO, lands inside cached HTML). Uptime Kuma monitors each site by pulling the homepage and asserting the marker is present, which measures real user-facing availability (DNS + TLS + nginx + PHP + WordPress + DB + render) instead of the old push-heartbeat model that only measured whether WP-Cron was firing — and routinely false-flagged perfectly healthy sites as down (the worst offenders flapped 50–60×/day while serving HTTP 200 the entire time).Because the marker lives in the mu-plugin (which WordPress can't deactivate and AuctionForge re-asserts on every load), it reflects "WordPress is rendering pages" independently of AuctionForge's own activation state, and it survives failed or in-progress plugin updates. The bundled keepalive template is bumped to 1.3.0, so the installer rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpon update. The legacy push heartbeat is retained in this release for a safe transition to active monitoring.---
[3.7.0.43] — 2026-06-22
Fixed — Submission "Need help?" contact was hardcoded (one tenant's details on every site)
The vehicle-submission help panel (
templates/js-chunks/submissions.php) rendered a single hardcoded email and phone, so every AuctionForge site displayed the same contact regardless of which auction house it was — e.g. one tenant'sinfo@absoluteclearance.co.uk/01536 674804showed on unrelated sites.The panel now resolves the contact dynamically, in priority order:
apply_filters('auctionforge_help_email', '')/apply_filters('auctionforge_help_phone', '')— explicit per-site override (any source, e.g. a theme/global/Elementor setting);- otherwise the SEO & AEO Optimizer LocalBusiness details stored in the
seo_aeo_optionsoption (local_email/local_phone); - if neither yields a value the corresponding row is omitted, and the whole "Need help?" block is hidden when no contact is available — so no placeholder or wrong-tenant detail is ever shown.
Internal
- Modified:
templates/js-chunks/submissions.php— replaced the two hardcoded<p>contact values with filtered,esc_html-escaped values resolved fromseo_aeo_options, each row guarded by anif. - Modified:
auctionforge.php— plugin header → 3.7.0.43. - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.42] — 2026-06-22
Fixed — Submission templates emitted extra
<h1>s and placeholder content site-wideThe vehicle-submission view (
templates/js-chunks/submissions.php) is rendered onwp_footeron every page, so its<script type="text/template">chunks shipped into the source of all pages. Two of those chunks used<h1>headings ("Submit your vehicle for auction" and "Detail received successfully"), meaning every page's HTML source contained three<h1>elements. While these live inside inert<script>templates (so a DOM-rendering crawler like Googlebot still resolves a single true H1), raw-source SEO tooling and some AI crawlers flag the duplicates, and the templates also shipped vendor placeholder text.- Both template headings demoted from
<h1>to<h2>— a single H1 per page in source. - Replaced Lorem-ipsum placeholder copy in the intro and progress panels with real submission guidance.
- Replaced the dummy "Need help?" contact details (
support@domain.com/+3265464325) with the live business contact (info@absoluteclearance.co.uk/01536 674804).
No functional/JS changes — template IDs and structure are unchanged.
---
[3.7.0.41] — 2026-06-19
Fixed — Duplicate auction terms (auction shown twice, catalogue split)
A newly published auction could be created twice in WordPress as two identical
auction_numberterms, so the auction rendered twice on the listing and its catalogue was split across the two copies (e.g. 145 lots stored as 144 + 1). The lot posts themselves stayed unique — this was a duplicate *auction*, not duplicate *lots*.Root cause: auction terms are only created in
Auction::save()viawp_insert_category, which inserts whenever the caller's lookup returned no term. That lookup (findAuctionByIdWithLang()→get_terms()meta_query) can return nothing for a brand-new auction when two syncs run concurrently — for example afull_sync_*bulk run (viaBulkSyncWorker/AuctionList) overlapping a catalogue webhook. Both see "no term" and both insert one. The only fallback,findAuctionByNumber(), looks up by the auction *number* slug and never matched the real term slug, so it could not catch the miss. This path is not covered by the existingCatalogSyncWorkerper-auction lock, which serialises catalogue syncs to prevent duplicate *lot posts* but does not guard *term* creation.Auction::save()now, before inserting a term:- acquires a per-auction MySQL advisory lock (
GET_LOCK('<prefix>af_term_<auctionId>', 10)) so concurrent creators serialise — the loser then finds the just-created term and updates it instead of inserting a second one; and - performs an authoritative, cache-bypassing lookup by
auction_id(directtermmeta⋈term_taxonomyquery) and switches to update-in-place when a term already exists.
Polylang-aware: under full multilingual support it only reuses a term in the requested language, so legitimate per-language terms are still created.
Internal
- Modified:
inc/model/Auction.php—save()gains a pre-insert dedup guard; new private helpersfindTermIdByAuctionId(),acquireTermLock(),releaseTermLock().InstanceDBwraps the global$wpdb, so the advisory lock and thewp_insert_categoryinsert share a single DB session (the same property that makes the existingCatalogSyncWorkerlock effective). - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.40] — 2026-06-14
Fixed — Lot "begin to close" countdown now shows minutes (and seconds in the final minute)
The lot-page countdown (
#auctionStartTimer, the *"Auction will begin to close in …"* line) only ever rendered whole days and whole hours, and computed once on page load. With less than an hour to go it therefore read "0 days and 0 hours" — e.g. a lot closing at 20:00 looked like a dead/expired timer from 19:00 onwards, even though ~46 minutes remained. The time maths was correct; the widget simply had no sub-hour granularity and never ticked.It now updates every second and shows the largest meaningful units, dropping leading zero units:
44 minutes•3 hours and 20 minutes•1 day, 2 hours and 5 minutes- In the final minute it counts the seconds down (
40 seconds→1 second) so it never sits on a flat "0". - Once the close time passes (
result < 0) the timer hides itself, as before.
getCountdownText()now wraps the suffix in a.bp-start-countdownspan so the script can rewrite the whole phrase; if that wrapper is ever absent (e.g. a stale page-cached markup),getLeftDate()falls back to the original days+hours behaviour rather than erroring — important becauseapp.jsalso carries the live bidding logic.Internal
- Modified:
inc/controllers/Settings.php—getCountdownText()suffix wrapped in.bp-start-countdown. - Modified:
assets/js/app.js—getLeftDate()rewritten: 1s ticking viasetInterval, days/hours/minutes/seconds, zero-unit omission, pluralisation, and a wrapper-missing fallback. This is the file actually served (front-end$jsFileis hardcoded to.js). - Modified:
src/js/app.js— samegetLeftDate()change, to keep the gulp source in sync.app.min.jsis intentionally not regenerated (unused — the unminifiedapp.jsis enqueued). - Modified:
auctionforge.php— plugin header → 3.7.0.40. (3.7.0.39 was present in the header with no changelog entry; this release supersedes it.)
---
[3.7.0.38] — 2026-06-02
Added — Spelling Dialect "Auto" mode (match the auction house region)
The Spelling Dialect setting (Settings → Appearance) gains a third option, "Auto — match the auction house region", which becomes the new default for sites that haven't chosen one:
- During the existing auction sync (
getAllAuctions),Dialect::captureRegionFromAuctions()reads the auction house's country from the auctionaddress(trailing segment;currencyas a fallback) and stores the resolved dialect in thespelling_dialect_autooption. No API calls happen on page loads — it's a fixed, site-wide value refreshed each sync, so it's fully page-cache-safe. - Mapping: US → Catalog; every other country → Catalogue (American "Catalog" is US-only; UK/IE/AU/NZ/CA/etc. use "Catalogue").
- The admin field shows what was detected, e.g. *"Detected region: United Kingdom → Catalogue."* United States and United Kingdom remain manual hard-overrides.
Default change: new/unconfigured sites now default to Auto (was UK). Until a site's first sync resolves the region, Auto falls back to UK, so behaviour is unchanged until detection kicks in — then it self-corrects (a US house starts showing "Catalog" automatically).
Internal
- Modified:
inc/helpers/Dialect.php—automode,FALLBACK_DIALECT,countryToDialect(),captureRegionFromAuctions();DEFAULT_DIALECT→auto. - Modified:
inc/import/AuctionList.php— capture the house region during the auctions sync. - Modified:
inc/controllers/Settings.php— Auto dropdown option + detected-region note. - Modified:
auctionforge.php— plugin header → 3.7.0.38.
---
[3.7.0.37] — 2026-06-02
Fixed — Per-lot "full price VAT" now drives the VAT statement
The standalone VAT line on lot pages (
vat-and-commission→.vat-field) was keyed off the auction-house VAT rule, so it always read *"VAT: X% on item price and commission"* regardless of a lot's full-price-VAT toggle — and toggling a lot off didn't change it. It now branches on the live per-lot flag (itemInfo.item.fullPriceVat), matching the price-breakdown calc ingetFullPriceInfo():- Full-price VAT on → *"VAT: X% on item price and commission"* (VAT applied to hammer + commission)
- off (default) → *"VAT: X% on commission only"*
This only changes the wording to match the amount
getFullPriceInfo()already computes per the toggle; the VAT figure itself was already correct. The separaterenderVatField()"no VAT on commission" path is unchanged.Internal
- Modified:
templates/js-chunks/vat-and-commission.php— VAT statement branches on the per-lotfullPriceVatinstead ofhouseCommissionVatRule. - Modified:
auctionforge.php— plugin header → 3.7.0.37.
---
[3.7.0.36] — 2026-06-02
A Site Statistics release: day-of-week analysis, an opt-in weekly email report, lot-focused "top pages", and several dashboard refinements.
Added — Day-of-week tabs on the hourly charts
The Hourly Distribution (Site Statistics → Overview) and Peak Viewing Times (Lot Analytics) charts now have a day tab strip — All days · Mon · Tue … Sun — each showing that weekday's total. Selecting a day redraws the 24-hour chart consolidated across every matching date in the range, so you can see *which days and times* visitors are active. The lot-analytics data already carried
day_of_week; it was previously flattened to 24 hours and is now surfaced.Added — Scheduled weekly email report (opt-in, off by default)
A new report under Site Statistics → Settings emails a polished weekly summary: headline KPI cards (Visitors / Pageviews / Sessions with this-week-vs-last-week change), a visitor journey funnel (Visitors → Viewed a lot → Left a bid), busiest hour for each weekday, and the week's most-viewed lots. Driven by a daily cron that sends once on a configurable weekday. Supports one or several recipients (comma/semicolon/space/newline separated, validated and de-duped) and a "Send test now" button that sends to whatever is currently in the Recipient(s) box. Built table-based with inline CSS for Gmail/Outlook/Apple Mail compatibility.
Added — New vs Returning visitors, auction markers, engagement funnel
- New vs Returning visitor KPI cards on the Overview (new = authoritative
af_stats_visitors.first_seenwithin the period, joined so it can never exceed total visitors). - Auction-date markers drawn on the *Traffic Over Time* chart (dashed lines + a captioned list) so traffic can be read against sale dates.
- Engagement funnel panel: Visitors → Viewed a lot → Viewed 5+ lots → Bidders (bidders from synced
af_bid_events).
Changed — "Top pages" is now lot-focused ("Top Lots" / "Most viewed lots")
Both the dashboard Overview panel and the email now list the most-viewed individual lot pages only (
/lot-…), excluding home, catalogue listing/pagination and profile pages as low-signal. Email page titles are shown in full (no mid-word truncation) with the site-name suffix stripped; the dedicated Pages tab remains the unfiltered drill-down.Changed — Lot Analytics "Peak Viewing Times" honours the global date range
Previously hard-coded to the last 30 days; it now respects the dashboard's range picker. The Lot Analytics tab also auto-loads on first click (previously required a manual refresh).
Changed — Chart.js bundled locally
The Statistics and plugin dashboards loaded Chart.js from a CDN at two different versions (
@4and@4.4.1). Chart.js 4.4.1 is now bundled atadmin/assets/js/chart.umd.min.jsand loaded from the plugin (version-busted), so charts work offline / behind strict CSP and the version is pinned.Internal
- Added:
inc/stats/class-stats-report.php—AF_Stats_Report(weekly cron, recipient parsing, HTML builder, "send test" AJAX).admin/assets/js/chart.umd.min.js— bundled Chart.js 4.4.1. - Modified:
inc/stats/class-stats-ajax.php— hourly-by-DOW query;new_visitors,auction_events,funnelin the dashboard payload; lot-only top pages; global date range passed to peak times; weekly-email settings saved. - Modified:
inc/model/LotAnalytics.php—getPeakViewingTimes()accepts an explicit$from/$torange. - Modified:
inc/stats/class-stats-init.php— require + registerAF_Stats_Report. - Modified:
admin/view/stats/dashboard.php— day-of-week tabs, KPI cards, auction-marker plugin, funnel, Top Lots, weekly-email settings UI + test button, local Chart.js. - Modified:
admin/view/dashboard.php— local Chart.js. - Modified:
auctionforge.php— plugin header → 3.7.0.36.
---
[3.7.0.35] — 2026-06-02
Added — Regional spelling dialect ("Catalog" US / "Catalogue" UK)
A new Spelling Dialect dropdown at the top of Settings → Appearance lets each site choose how the word "catalog" is presented on the public front end:
- United States — Catalog — US spelling (the plugin's original authored wording).
- United Kingdom — Catalogue — UK spelling.
When UK is selected, every front-end occurrence is rewritten — *"Catalogue will be available soon"*, *"View catalogue"*, *"Catalogue Coming Soon"*, *"Download catalogue"*, *"Back to the catalogue"*, the lot-page Catalogue tab, and the "catalogue published" app notification.
How it works. Every user-facing catalog string already passes through the
auctionforgetext domain (__()), so the swap is implemented as a singlegettext/gettext_with_contextfilter rather than by editing template files.Dialect::toUk()rewrites whole words only, preserving case and pluralisation (Catalog→Catalogue,View catalog→View catalogue,CATALOG→CATALOGUE,Catalogs→Catalogues), and is idempotent. The filter is front-end only (is_admin()-guarded, with awp_doing_ajax()exception so front-end AJAX is still covered) and is scoped to theauctionforgedomain, so it never touches WordPress core, the theme, or other plugins. Code identifiers, option keys (catalog_layout,bids_in_catalog), CSS classes (af-catalog-sidebar,catalog-section,bp-back-to-catalog-button) and JS variables are all left intact. On US sites the filter is not registered at all, so there is zero added overhead.Internal
- Added:
inc/helpers/Dialect.php—Dialecthelper:get()/isUk()(reads thespelling_dialectoption, default UK),word()for non-translated strings, the case/plural-preservingtoUk()transform, thegettextfilter callbacks, andregister()(no-op on US sites). - Modified:
inc/coreIncludes.php— requireDialect.phpand callDialect::register()at plugin load. - Modified:
inc/controllers/Settings.php—usetheDialecthelper; add the "Regional Spelling" sub-heading and Spelling Dialect field at the top of the Appearance section, plus thefill_bidspirit_options_spelling_dialect()renderer. The value persists through the existingbidspirit_settingsoption and sanitize callback. - Modified:
auctionforge.php— plugin header → 3.7.0.35.
Note for existing sites. The default dialect is UK ("Catalogue"). Any site that upgrades without an explicit
spelling_dialectvalue will therefore display "Catalogue" on the front end. Sites that should keep US spelling must set Settings → Appearance → Spelling Dialect to United States — Catalog (stored asspelling_dialect = us). Admin labels are unaffected in either dialect.---
[3.7.0.34] — 2026-05-30
Fixed — Duplicate lot posts created by concurrent catalog syncs for the same auction
When two catalog-sync jobs for the same auction ran at the same time, every lot was imported twice and the front-end showed each lot as a pair (the second post taking a
-2slug). Observed on a live sale: 157 lots became 313 publishedlotposts.Root cause. Two BidSpirit webhooks arriving in the same second (e.g.
CATALOG_PUBLISHED+AUCTION_CATALOG_UPDATED) each enqueue acatalog_sync.SyncQueue::enqueue()deduplicates with a non-atomic *SELECT-then-INSERT* (no DB unique constraint), so two simultaneous calls both pass the "already pending?" check and both jobs land in the queue. Job *claiming* is atomic per-job, but nothing serialised two different jobs for the same auction, so two workers ran concurrently. The matcherLot::findByBSMeta()only sees committedpublish/draftposts, so during the overlapping insert windows each worker saw "no existing post" for every lot and inserted a fresh full set. Worse,findByBSMetadoesGROUP BY lotId, so the resulting duplicate pair is collapsed to a single row in every later sync — the importer never detects or repairs it, and thecount($datum) > 1"Double" guard inInsertLotsBS::init()is effectively dead code.Fix.
CatalogSyncWorker::process()andprocessDayEnded()now take a per-auction MySQL advisory lock (GET_LOCK('<prefix>af_sync_<auctionId>', 0)) around the API-fetch + post-insertion work. If another worker already holds the lock for that auction, the duplicate job logsconcurrent-lock busyand returns cleanly — marked completed, since the in-flight sync already covers the work — instead of racing. The lock is held on the worker's own DB connection and auto-releases if the process dies, so it can never deadlock the queue. This serialises all catalog and day-ended processing per auction no matter how many duplicate jobs reach the queue.Internal
- Modified:
inc/sync/CatalogSyncWorker.php— newacquireAuctionLock()/releaseAuctionLock()helpers;process()andprocessDayEnded()acquire the lock up front (skip-on-busy) and release it in afinally. - Modified:
auctionforge.php— plugin header → 3.7.0.34.
Note for existing sites. This prevents *new* duplicates; it does not remove ones already created. To find them, look for published
lotposts where two share one BidSpirit lot id (bp_meta_data.id). Keep the most-recently-synced post per lot id and set the rest topost_status = 'trash'— thelotpost type has trash disabled, so do it via SQL, and do not usedraft(whichfindByBSMetastill matches). Then recount theauction_numberterm and clear caches.---
[3.7.0.33] — 2026-05-30
Fixed —
auctionforge_sync_lotscron never scheduled (Lots push to wpdeploy silently stopped)AF_Lots_Syncuses the custombp_ten_minutelyschedule. The sync crons were registered fromAF_Stats_Init::init(), which runs at plugin-load time — *before*auctionforge.phpaddsbp_ten_minutelyto thecron_schedulesfilter further down the same file. Sowp_schedule_event(time(), 'bp_ten_minutely', 'auctionforge_sync_lots')ran against an unknown schedule, failed silently, and left the hook permanently unscheduled. The other four sync hooks use core'shourlyand were unaffected — which is why only the Lots push to wpdeploy went stale (observed: 3 days with no push while every other stream ran on time). Fixed by deferring the syncregister()calls to theinithook, by which point every custom schedule is registered. The hook self-arms on the next request after upgrade.Fixed — Safety Sync admin run reported a false "Request failed (HTTP 200)"
auctionforge_ajax_run_cron_taskreturns its result as JSON, but a stray PHP notice/deprecation emitted by a task (e.g. the Safety Sync BidSpirit delta-check on PHP 8.1) printed into the response body ahead of the JSON, so the browser couldn't parse it and the modal showed a misleading "Request failed (HTTP 200)" even though the task succeeded. The handler now buffers task output, discards it before emitting JSON, and logs any suppressed output viaSyncLogger(contextadmin) so the offending notice can still be found and fixed at the root.Added — "Clear duplicate cron events" button on Sync Status
The one-shot
AF_Cron_Cleanupmigration only runs once per site, so per-hostname duplicate events created by an older build *after* the migration had already marked itself done were never swept. NewAF_Cron_Cleanup::force_sweep()runs the same sweep on demand, exposed via a Clear Cron Dupes button in the Sync Queue toolbar (AJAXauctionforge_clear_cron_dupes). It removes legacy-args duplicates of the four per-hostname hooks plus the two orphan hooks, keeping one clean event each.Internal
- Modified:
inc/stats/class-stats-init.php— syncregister()calls wrapped inadd_action('init', …, 20). - Modified:
inc/maintenance/class-cron-cleanup.php— new publicforce_sweep()reusing the existing sweep helpers. - Modified:
auctionforge.php—auctionforge_ajax_run_cron_task()output-buffered against stray output; newauctionforge_ajax_clear_cron_dupes()handler + registration; plugin header → 3.7.0.33. - Modified:
admin/view/sync-status.php— added the "Clear Cron Dupes" button and handler.
Note: with all scheduling now arg-free (3.7.0.28+), the duplicates do not regenerate once swept; a canonical-hostname redirect (www ↔ apex) at the web-server level remains good hygiene but is no longer required to keep the cron table clean.
---
[3.7.0.32] — 2026-05-30
Fixed — V3 sync worker never spawned on some hosts (silent import stall)
On some sites (confirmed on a LiteSpeed/cPanel install) the V3 queue worker never actually started, so catalog lots were staged into
import_historybut never converted to WordPress posts —postId IS NULL,syncFinished = 0,syncTimes = 0, and no errors logged. Clearing the queue and reprocessing re-stalled immediately.Root cause.
SyncQueue::spawnWorker()starts the worker by POSTing the REST route/wp-json/auctionforge/v1/sync-worker. It tried an HTTP loopback tohttp://127.0.0.1/with the site'sHost:header first, and the success test wasif (!is_wp_error($result))— it never inspected the HTTP status code. On hosts where127.0.0.1+Hostheader resolves to the wrong vhost (or the route is blocked), that request returns 404/403, which is not aWP_Error. The old code therefore treated the 404 as a successful spawn, loggedSpawned … (HTTP 404), returnedtrue, and never reached the HTTPS fallback. The worker never ran; nothing drained the queue or the staging table. Sites whose127.0.0.1loopback resolves correctly returned 200 and were unaffected — which is why this was host-specific.Fix.
spawnWorker()now reads the response code and only treats 2xx as a successful spawn. On any non-2xx (or aWP_Error) from the loopback it logs a warning and falls through to therest_url()HTTPS path, which is likewise status-checked. A new constant short-circuits the doomed attempt on known-bad hosts:```php
define('AUCTIONFORGE_WORKER_SKIP_LOOPBACK', true); // wp-config.php — skip 127.0.0.1, go straight to HTTPS
```
Added — Automatic staging recovery on the safety cron
The hourly
auctionforge_sync_safetycron previously only spawned a worker when the queue had pending jobs. Lots that were staged intoimport_historybut left unconverted (e.g. after a worker died, or while the spawn bug above was in effect) had no queue job attached, so nothing ever picked them up. The safety cron now also checksSyncQueue::stagingBacklogCount()and, when it is non-zero, kicks the staging processor (non-blocking) to convert the backlog into posts — making the pipeline self-healing without manual intervention.Added — "Process Staging" button on Sync Status
A new Process Staging button in the Sync Queue toolbar (Lots → Sync Status) runs
InsertLotsBSover the staging table on demand — for when lots are staged but not appearing on the site. It fires non-blocking and switches the Live Console into fast-poll so the operator can watch posts being created in batches of 50.Internal
- Modified:
inc/sync/SyncQueue.php—spawnWorker()rewritten with HTTP-status-aware fallback +AUCTIONFORGE_WORKER_SKIP_LOOPBACKsupport;triggerStagingProcessor()gained a$blockingparameter (non-blocking fire-and-forget mode for cron/admin); newstagingBacklogCount()helper (countsimport_historyrows withsyncFinished = 0). - Modified:
auctionforge.php—auctionforge_sync_safety_action()now drains orphaned staging via the non-blocking processor when a backlog exists; newprocess_stagingcase in theauctionforge_sync_enqueueAJAX handler; plugin header bumped to 3.7.0.32. - Modified:
admin/view/sync-status.php— added the "Process Staging" button and its click handler.
---
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.7.1.9 Stable Jul 14, 2026Download v3.7.1.9📦 9.33 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.1.9] — 2026-07-14
Fixed — Catalog pagination stayed broken for returning visitors (stale session)
Follow-up to 3.7.1.8. That fix stopped *new* sessions from getting a bad per-page baseline, but a returning visitor whose session already held the old baseline (the session cookie lives 24h) kept getting page 2+ reset to page 1 at any non-default items-per-page — only a full "clear site data" (which drops the session cookie) fixed it for them.
The server no longer resets pagination from session state at all. Per-page changes already return to page 1 on the client (the items-per-page dropdown navigates to the page-1 URL), so the session-based reset in
BidspiritLots::search_filter()andbidspirit_lot_per_page()was both redundant and the source of the stale-baseline problem — it has been removed. Pagination is now correct for every visitor (new or returning, any session state) on the next page load, with no cache/cookie clearing required. No schema or config-version change. -
v3.7.1.8 Stable Jul 14, 2026Download v3.7.1.8📦 9.33 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.1.8] — 2026-07-14
Fixed — Catalog pagination broke at any non-default "Items per page"
On auction catalog pages, setting Items per page to any value other than the site's WordPress default (
posts_per_page) made page 2 and beyond redirect straight back to page 1 — so it appeared to "show the same lots" — while the value matching the default worked.BidspiritLots::bp_session_starter()seeded the per-page change-detection baseline fromget_option('posts_per_page'); because auction pages are served from full-page cache / behind a CDN that drops the PHP session cookie, that baseline reset to the default on every request, so any non-default size looked "just changed" on every page load and was force-redirected to page 1 bymaybe_redirect_to_first_page().The baseline is now seeded to
null("unknown previous value"), matching the existing category/search baselines, so a page reset only fires when a genuinely-known prior per-page value differs (isset && !== null && !=). The items-per-page dropdown additionally returns to page 1 client-side, so "change size → page 1" no longer depends on the session. No schema or config-version change. -
v3.7.1.7 Stable Jul 13, 2026Download v3.7.1.7📦 9.33 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.1.7] — 2026-07-13
Added — Per-house toggle to bypass phone verification (UK houses auto-bypass)
BidSpirit's
users/phone/sendPhoneVerificationCodereturns a genericUNKNOWN_ERRORfor UK houses (e.g. rosanreeves, edwards) even for valid+44mobiles, because SMS/phone-verification is not provisioned on those BidSpirit accounts. As the requirement is enforced only plugin-side, this left UK bidders unable to bid.A new per-house setting
require_phone_verification(in thebidspirit_settingsoption) makes the client-side bid gate conditional:auto(default) — enforce EXCEPT on UK houses (Dialect::isUk()), which are bypassed while their BidSpirit accounts lack SMS provisioning.off— never enforce (per-house override, e.g.wp option patch update bidspirit_settings require_phone_verification off).on— always enforce.
BidspiritViews::bp_footer()emitswindow.afRequirePhoneVerification; both bid gates (submitBid,submitBidCatalog) and the profile “Verify phone” CTA honour it. Fails closed (enforces) when the flag is absent. No schema or config-version change.---
-
v3.7.1.6 Stable Jul 13, 2026Download v3.7.1.6📦 9.4 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.1.2] — 2026-06-30
Fixed — Background sync worker spawn timing out ("Worker spawn HTTPS failed: cURL error 28")
On busy or memory-pressured hosts the background sync worker frequently failed to start, logging
Worker spawn HTTPS failed: cURL error 28: Operation timed out after 5002 milliseconds with 0 bytes received(observed ~29×/day on one site).SyncQueue::spawnWorker()triggers the worker with a blocking loopback HTTP request to the site's own REST endpoint; the timeout was hard-coded to 5 seconds, so whenever the site's own backend took longer than 5s to respond (DB/FPM contention) the spawn aborted *as a hard error* and raised a sync alert — even though the site was up and serving.Three changes make the spawn resilient:
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
AUCTIONFORGE_WORKER_SPAWN_TIMEOUTconstant or theauctionforge_worker_spawn_timeoutfilter. - Scheme-matched loopback. Strategy 1 now issues the
127.0.0.1loopback using the site's own scheme (https when the site is https) instead of always http. Previously an http→https redirect (301) made the loopback look like a failure and forced the slower public-URL fallback; the POST now reaches the worker endpoint directly. - Non-blocking best-effort fallback. If both blocking attempts can't confirm a start within the timeout, the worker is now triggered with a final non-blocking (fire-and-forget) request, logged at warning instead of error. A merely-slow backend still starts its worker, the hourly
SafetySynccron re-spawns if it genuinely didn't, and the condition no longer raises a hard sync alert.
No schema, DB, or config-version change. Behaviour is unchanged on healthy hosts (the loopback still returns 2xx immediately); the new paths engage only when the backend is slow. The underlying slow backend was a server-side issue (MariaDB query-cache lock contention + memory pressure) fixed at the infrastructure level; this release hardens the plugin so a transiently slow backend can no longer turn into a failed sync.
---
[3.7.1.1] — 2026-06-29
Added — Active uptime monitoring via Kuma keyword health marker
The keepalive mu-plugin now emits an invisible
<!-- af-health:ok -->marker into every rendered front-end page (hooked onwp_headandwp_footer, emitted once per request, pure echo — no DB/IO, lands inside cached HTML). Uptime Kuma monitors each site by pulling the homepage and asserting the marker is present, which measures real user-facing availability (DNS + TLS + nginx + PHP + WordPress + DB + render) instead of the old push-heartbeat model that only measured whether WP-Cron was firing — and routinely false-flagged perfectly healthy sites as down (the worst offenders flapped 50–60×/day while serving HTTP 200 the entire time).Because the marker lives in the mu-plugin (which WordPress can't deactivate and AuctionForge re-asserts on every load), it reflects "WordPress is rendering pages" independently of AuctionForge's own activation state, and it survives failed or in-progress plugin updates. The bundled keepalive template is bumped to 1.3.0, so the installer rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpon update. The legacy push heartbeat is retained in this release for a safe transition to active monitoring.---
[3.7.0.43] — 2026-06-22
Fixed — Submission "Need help?" contact was hardcoded (one tenant's details on every site)
The vehicle-submission help panel (
templates/js-chunks/submissions.php) rendered a single hardcoded email and phone, so every AuctionForge site displayed the same contact regardless of which auction house it was — e.g. one tenant'sinfo@absoluteclearance.co.uk/01536 674804showed on unrelated sites.The panel now resolves the contact dynamically, in priority order:
apply_filters('auctionforge_help_email', '')/apply_filters('auctionforge_help_phone', '')— explicit per-site override (any source, e.g. a theme/global/Elementor setting);- otherwise the SEO & AEO Optimizer LocalBusiness details stored in the
seo_aeo_optionsoption (local_email/local_phone); - if neither yields a value the corresponding row is omitted, and the whole "Need help?" block is hidden when no contact is available — so no placeholder or wrong-tenant detail is ever shown.
Internal
- Modified:
templates/js-chunks/submissions.php— replaced the two hardcoded<p>contact values with filtered,esc_html-escaped values resolved fromseo_aeo_options, each row guarded by anif. - Modified:
auctionforge.php— plugin header → 3.7.0.43. - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.42] — 2026-06-22
Fixed — Submission templates emitted extra
<h1>s and placeholder content site-wideThe vehicle-submission view (
templates/js-chunks/submissions.php) is rendered onwp_footeron every page, so its<script type="text/template">chunks shipped into the source of all pages. Two of those chunks used<h1>headings ("Submit your vehicle for auction" and "Detail received successfully"), meaning every page's HTML source contained three<h1>elements. While these live inside inert<script>templates (so a DOM-rendering crawler like Googlebot still resolves a single true H1), raw-source SEO tooling and some AI crawlers flag the duplicates, and the templates also shipped vendor placeholder text.- Both template headings demoted from
<h1>to<h2>— a single H1 per page in source. - Replaced Lorem-ipsum placeholder copy in the intro and progress panels with real submission guidance.
- Replaced the dummy "Need help?" contact details (
support@domain.com/+3265464325) with the live business contact (info@absoluteclearance.co.uk/01536 674804).
No functional/JS changes — template IDs and structure are unchanged.
---
[3.7.0.41] — 2026-06-19
Fixed — Duplicate auction terms (auction shown twice, catalogue split)
A newly published auction could be created twice in WordPress as two identical
auction_numberterms, so the auction rendered twice on the listing and its catalogue was split across the two copies (e.g. 145 lots stored as 144 + 1). The lot posts themselves stayed unique — this was a duplicate *auction*, not duplicate *lots*.Root cause: auction terms are only created in
Auction::save()viawp_insert_category, which inserts whenever the caller's lookup returned no term. That lookup (findAuctionByIdWithLang()→get_terms()meta_query) can return nothing for a brand-new auction when two syncs run concurrently — for example afull_sync_*bulk run (viaBulkSyncWorker/AuctionList) overlapping a catalogue webhook. Both see "no term" and both insert one. The only fallback,findAuctionByNumber(), looks up by the auction *number* slug and never matched the real term slug, so it could not catch the miss. This path is not covered by the existingCatalogSyncWorkerper-auction lock, which serialises catalogue syncs to prevent duplicate *lot posts* but does not guard *term* creation.Auction::save()now, before inserting a term:- acquires a per-auction MySQL advisory lock (
GET_LOCK('<prefix>af_term_<auctionId>', 10)) so concurrent creators serialise — the loser then finds the just-created term and updates it instead of inserting a second one; and - performs an authoritative, cache-bypassing lookup by
auction_id(directtermmeta⋈term_taxonomyquery) and switches to update-in-place when a term already exists.
Polylang-aware: under full multilingual support it only reuses a term in the requested language, so legitimate per-language terms are still created.
Internal
- Modified:
inc/model/Auction.php—save()gains a pre-insert dedup guard; new private helpersfindTermIdByAuctionId(),acquireTermLock(),releaseTermLock().InstanceDBwraps the global$wpdb, so the advisory lock and thewp_insert_categoryinsert share a single DB session (the same property that makes the existingCatalogSyncWorkerlock effective). - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.40] — 2026-06-14
Fixed — Lot "begin to close" countdown now shows minutes (and seconds in the final minute)
The lot-page countdown (
#auctionStartTimer, the *"Auction will begin to close in …"* line) only ever rendered whole days and whole hours, and computed once on page load. With less than an hour to go it therefore read "0 days and 0 hours" — e.g. a lot closing at 20:00 looked like a dead/expired timer from 19:00 onwards, even though ~46 minutes remained. The time maths was correct; the widget simply had no sub-hour granularity and never ticked.It now updates every second and shows the largest meaningful units, dropping leading zero units:
44 minutes•3 hours and 20 minutes•1 day, 2 hours and 5 minutes- In the final minute it counts the seconds down (
40 seconds→1 second) so it never sits on a flat "0". - Once the close time passes (
result < 0) the timer hides itself, as before.
getCountdownText()now wraps the suffix in a.bp-start-countdownspan so the script can rewrite the whole phrase; if that wrapper is ever absent (e.g. a stale page-cached markup),getLeftDate()falls back to the original days+hours behaviour rather than erroring — important becauseapp.jsalso carries the live bidding logic.Internal
- Modified:
inc/controllers/Settings.php—getCountdownText()suffix wrapped in.bp-start-countdown. - Modified:
assets/js/app.js—getLeftDate()rewritten: 1s ticking viasetInterval, days/hours/minutes/seconds, zero-unit omission, pluralisation, and a wrapper-missing fallback. This is the file actually served (front-end$jsFileis hardcoded to.js). - Modified:
src/js/app.js— samegetLeftDate()change, to keep the gulp source in sync.app.min.jsis intentionally not regenerated (unused — the unminifiedapp.jsis enqueued). - Modified:
auctionforge.php— plugin header → 3.7.0.40. (3.7.0.39 was present in the header with no changelog entry; this release supersedes it.)
---
[3.7.0.38] — 2026-06-02
Added — Spelling Dialect "Auto" mode (match the auction house region)
The Spelling Dialect setting (Settings → Appearance) gains a third option, "Auto — match the auction house region", which becomes the new default for sites that haven't chosen one:
- During the existing auction sync (
getAllAuctions),Dialect::captureRegionFromAuctions()reads the auction house's country from the auctionaddress(trailing segment;currencyas a fallback) and stores the resolved dialect in thespelling_dialect_autooption. No API calls happen on page loads — it's a fixed, site-wide value refreshed each sync, so it's fully page-cache-safe. - Mapping: US → Catalog; every other country → Catalogue (American "Catalog" is US-only; UK/IE/AU/NZ/CA/etc. use "Catalogue").
- The admin field shows what was detected, e.g. *"Detected region: United Kingdom → Catalogue."* United States and United Kingdom remain manual hard-overrides.
Default change: new/unconfigured sites now default to Auto (was UK). Until a site's first sync resolves the region, Auto falls back to UK, so behaviour is unchanged until detection kicks in — then it self-corrects (a US house starts showing "Catalog" automatically).
Internal
- Modified:
inc/helpers/Dialect.php—automode,FALLBACK_DIALECT,countryToDialect(),captureRegionFromAuctions();DEFAULT_DIALECT→auto. - Modified:
inc/import/AuctionList.php— capture the house region during the auctions sync. - Modified:
inc/controllers/Settings.php— Auto dropdown option + detected-region note. - Modified:
auctionforge.php— plugin header → 3.7.0.38.
---
[3.7.0.37] — 2026-06-02
Fixed — Per-lot "full price VAT" now drives the VAT statement
The standalone VAT line on lot pages (
vat-and-commission→.vat-field) was keyed off the auction-house VAT rule, so it always read *"VAT: X% on item price and commission"* regardless of a lot's full-price-VAT toggle — and toggling a lot off didn't change it. It now branches on the live per-lot flag (itemInfo.item.fullPriceVat), matching the price-breakdown calc ingetFullPriceInfo():- Full-price VAT on → *"VAT: X% on item price and commission"* (VAT applied to hammer + commission)
- off (default) → *"VAT: X% on commission only"*
This only changes the wording to match the amount
getFullPriceInfo()already computes per the toggle; the VAT figure itself was already correct. The separaterenderVatField()"no VAT on commission" path is unchanged.Internal
- Modified:
templates/js-chunks/vat-and-commission.php— VAT statement branches on the per-lotfullPriceVatinstead ofhouseCommissionVatRule. - Modified:
auctionforge.php— plugin header → 3.7.0.37.
---
[3.7.0.36] — 2026-06-02
A Site Statistics release: day-of-week analysis, an opt-in weekly email report, lot-focused "top pages", and several dashboard refinements.
Added — Day-of-week tabs on the hourly charts
The Hourly Distribution (Site Statistics → Overview) and Peak Viewing Times (Lot Analytics) charts now have a day tab strip — All days · Mon · Tue … Sun — each showing that weekday's total. Selecting a day redraws the 24-hour chart consolidated across every matching date in the range, so you can see *which days and times* visitors are active. The lot-analytics data already carried
day_of_week; it was previously flattened to 24 hours and is now surfaced.Added — Scheduled weekly email report (opt-in, off by default)
A new report under Site Statistics → Settings emails a polished weekly summary: headline KPI cards (Visitors / Pageviews / Sessions with this-week-vs-last-week change), a visitor journey funnel (Visitors → Viewed a lot → Left a bid), busiest hour for each weekday, and the week's most-viewed lots. Driven by a daily cron that sends once on a configurable weekday. Supports one or several recipients (comma/semicolon/space/newline separated, validated and de-duped) and a "Send test now" button that sends to whatever is currently in the Recipient(s) box. Built table-based with inline CSS for Gmail/Outlook/Apple Mail compatibility.
Added — New vs Returning visitors, auction markers, engagement funnel
- New vs Returning visitor KPI cards on the Overview (new = authoritative
af_stats_visitors.first_seenwithin the period, joined so it can never exceed total visitors). - Auction-date markers drawn on the *Traffic Over Time* chart (dashed lines + a captioned list) so traffic can be read against sale dates.
- Engagement funnel panel: Visitors → Viewed a lot → Viewed 5+ lots → Bidders (bidders from synced
af_bid_events).
Changed — "Top pages" is now lot-focused ("Top Lots" / "Most viewed lots")
Both the dashboard Overview panel and the email now list the most-viewed individual lot pages only (
/lot-…), excluding home, catalogue listing/pagination and profile pages as low-signal. Email page titles are shown in full (no mid-word truncation) with the site-name suffix stripped; the dedicated Pages tab remains the unfiltered drill-down.Changed — Lot Analytics "Peak Viewing Times" honours the global date range
Previously hard-coded to the last 30 days; it now respects the dashboard's range picker. The Lot Analytics tab also auto-loads on first click (previously required a manual refresh).
Changed — Chart.js bundled locally
The Statistics and plugin dashboards loaded Chart.js from a CDN at two different versions (
@4and@4.4.1). Chart.js 4.4.1 is now bundled atadmin/assets/js/chart.umd.min.jsand loaded from the plugin (version-busted), so charts work offline / behind strict CSP and the version is pinned.Internal
- Added:
inc/stats/class-stats-report.php—AF_Stats_Report(weekly cron, recipient parsing, HTML builder, "send test" AJAX).admin/assets/js/chart.umd.min.js— bundled Chart.js 4.4.1. - Modified:
inc/stats/class-stats-ajax.php— hourly-by-DOW query;new_visitors,auction_events,funnelin the dashboard payload; lot-only top pages; global date range passed to peak times; weekly-email settings saved. - Modified:
inc/model/LotAnalytics.php—getPeakViewingTimes()accepts an explicit$from/$torange. - Modified:
inc/stats/class-stats-init.php— require + registerAF_Stats_Report. - Modified:
admin/view/stats/dashboard.php— day-of-week tabs, KPI cards, auction-marker plugin, funnel, Top Lots, weekly-email settings UI + test button, local Chart.js. - Modified:
admin/view/dashboard.php— local Chart.js. - Modified:
auctionforge.php— plugin header → 3.7.0.36.
---
[3.7.0.35] — 2026-06-02
Added — Regional spelling dialect ("Catalog" US / "Catalogue" UK)
A new Spelling Dialect dropdown at the top of Settings → Appearance lets each site choose how the word "catalog" is presented on the public front end:
- United States — Catalog — US spelling (the plugin's original authored wording).
- United Kingdom — Catalogue — UK spelling.
When UK is selected, every front-end occurrence is rewritten — *"Catalogue will be available soon"*, *"View catalogue"*, *"Catalogue Coming Soon"*, *"Download catalogue"*, *"Back to the catalogue"*, the lot-page Catalogue tab, and the "catalogue published" app notification.
How it works. Every user-facing catalog string already passes through the
auctionforgetext domain (__()), so the swap is implemented as a singlegettext/gettext_with_contextfilter rather than by editing template files.Dialect::toUk()rewrites whole words only, preserving case and pluralisation (Catalog→Catalogue,View catalog→View catalogue,CATALOG→CATALOGUE,Catalogs→Catalogues), and is idempotent. The filter is front-end only (is_admin()-guarded, with awp_doing_ajax()exception so front-end AJAX is still covered) and is scoped to theauctionforgedomain, so it never touches WordPress core, the theme, or other plugins. Code identifiers, option keys (catalog_layout,bids_in_catalog), CSS classes (af-catalog-sidebar,catalog-section,bp-back-to-catalog-button) and JS variables are all left intact. On US sites the filter is not registered at all, so there is zero added overhead.Internal
- Added:
inc/helpers/Dialect.php—Dialecthelper:get()/isUk()(reads thespelling_dialectoption, default UK),word()for non-translated strings, the case/plural-preservingtoUk()transform, thegettextfilter callbacks, andregister()(no-op on US sites). - Modified:
inc/coreIncludes.php— requireDialect.phpand callDialect::register()at plugin load. - Modified:
inc/controllers/Settings.php—usetheDialecthelper; add the "Regional Spelling" sub-heading and Spelling Dialect field at the top of the Appearance section, plus thefill_bidspirit_options_spelling_dialect()renderer. The value persists through the existingbidspirit_settingsoption and sanitize callback. - Modified:
auctionforge.php— plugin header → 3.7.0.35.
Note for existing sites. The default dialect is UK ("Catalogue"). Any site that upgrades without an explicit
spelling_dialectvalue will therefore display "Catalogue" on the front end. Sites that should keep US spelling must set Settings → Appearance → Spelling Dialect to United States — Catalog (stored asspelling_dialect = us). Admin labels are unaffected in either dialect.---
[3.7.0.34] — 2026-05-30
Fixed — Duplicate lot posts created by concurrent catalog syncs for the same auction
When two catalog-sync jobs for the same auction ran at the same time, every lot was imported twice and the front-end showed each lot as a pair (the second post taking a
-2slug). Observed on a live sale: 157 lots became 313 publishedlotposts.Root cause. Two BidSpirit webhooks arriving in the same second (e.g.
CATALOG_PUBLISHED+AUCTION_CATALOG_UPDATED) each enqueue acatalog_sync.SyncQueue::enqueue()deduplicates with a non-atomic *SELECT-then-INSERT* (no DB unique constraint), so two simultaneous calls both pass the "already pending?" check and both jobs land in the queue. Job *claiming* is atomic per-job, but nothing serialised two different jobs for the same auction, so two workers ran concurrently. The matcherLot::findByBSMeta()only sees committedpublish/draftposts, so during the overlapping insert windows each worker saw "no existing post" for every lot and inserted a fresh full set. Worse,findByBSMetadoesGROUP BY lotId, so the resulting duplicate pair is collapsed to a single row in every later sync — the importer never detects or repairs it, and thecount($datum) > 1"Double" guard inInsertLotsBS::init()is effectively dead code.Fix.
CatalogSyncWorker::process()andprocessDayEnded()now take a per-auction MySQL advisory lock (GET_LOCK('<prefix>af_sync_<auctionId>', 0)) around the API-fetch + post-insertion work. If another worker already holds the lock for that auction, the duplicate job logsconcurrent-lock busyand returns cleanly — marked completed, since the in-flight sync already covers the work — instead of racing. The lock is held on the worker's own DB connection and auto-releases if the process dies, so it can never deadlock the queue. This serialises all catalog and day-ended processing per auction no matter how many duplicate jobs reach the queue.Internal
- Modified:
inc/sync/CatalogSyncWorker.php— newacquireAuctionLock()/releaseAuctionLock()helpers;process()andprocessDayEnded()acquire the lock up front (skip-on-busy) and release it in afinally. - Modified:
auctionforge.php— plugin header → 3.7.0.34.
Note for existing sites. This prevents *new* duplicates; it does not remove ones already created. To find them, look for published
lotposts where two share one BidSpirit lot id (bp_meta_data.id). Keep the most-recently-synced post per lot id and set the rest topost_status = 'trash'— thelotpost type has trash disabled, so do it via SQL, and do not usedraft(whichfindByBSMetastill matches). Then recount theauction_numberterm and clear caches.---
[3.7.0.33] — 2026-05-30
Fixed —
auctionforge_sync_lotscron never scheduled (Lots push to wpdeploy silently stopped)AF_Lots_Syncuses the custombp_ten_minutelyschedule. The sync crons were registered fromAF_Stats_Init::init(), which runs at plugin-load time — *before*auctionforge.phpaddsbp_ten_minutelyto thecron_schedulesfilter further down the same file. Sowp_schedule_event(time(), 'bp_ten_minutely', 'auctionforge_sync_lots')ran against an unknown schedule, failed silently, and left the hook permanently unscheduled. The other four sync hooks use core'shourlyand were unaffected — which is why only the Lots push to wpdeploy went stale (observed: 3 days with no push while every other stream ran on time). Fixed by deferring the syncregister()calls to theinithook, by which point every custom schedule is registered. The hook self-arms on the next request after upgrade.Fixed — Safety Sync admin run reported a false "Request failed (HTTP 200)"
auctionforge_ajax_run_cron_taskreturns its result as JSON, but a stray PHP notice/deprecation emitted by a task (e.g. the Safety Sync BidSpirit delta-check on PHP 8.1) printed into the response body ahead of the JSON, so the browser couldn't parse it and the modal showed a misleading "Request failed (HTTP 200)" even though the task succeeded. The handler now buffers task output, discards it before emitting JSON, and logs any suppressed output viaSyncLogger(contextadmin) so the offending notice can still be found and fixed at the root.Added — "Clear duplicate cron events" button on Sync Status
The one-shot
AF_Cron_Cleanupmigration only runs once per site, so per-hostname duplicate events created by an older build *after* the migration had already marked itself done were never swept. NewAF_Cron_Cleanup::force_sweep()runs the same sweep on demand, exposed via a Clear Cron Dupes button in the Sync Queue toolbar (AJAXauctionforge_clear_cron_dupes). It removes legacy-args duplicates of the four per-hostname hooks plus the two orphan hooks, keeping one clean event each.Internal
- Modified:
inc/stats/class-stats-init.php— syncregister()calls wrapped inadd_action('init', …, 20). - Modified:
inc/maintenance/class-cron-cleanup.php— new publicforce_sweep()reusing the existing sweep helpers. - Modified:
auctionforge.php—auctionforge_ajax_run_cron_task()output-buffered against stray output; newauctionforge_ajax_clear_cron_dupes()handler + registration; plugin header → 3.7.0.33. - Modified:
admin/view/sync-status.php— added the "Clear Cron Dupes" button and handler.
Note: with all scheduling now arg-free (3.7.0.28+), the duplicates do not regenerate once swept; a canonical-hostname redirect (www ↔ apex) at the web-server level remains good hygiene but is no longer required to keep the cron table clean.
---
[3.7.0.32] — 2026-05-30
Fixed — V3 sync worker never spawned on some hosts (silent import stall)
On some sites (confirmed on a LiteSpeed/cPanel install) the V3 queue worker never actually started, so catalog lots were staged into
import_historybut never converted to WordPress posts —postId IS NULL,syncFinished = 0,syncTimes = 0, and no errors logged. Clearing the queue and reprocessing re-stalled immediately.Root cause.
SyncQueue::spawnWorker()starts the worker by POSTing the REST route/wp-json/auctionforge/v1/sync-worker. It tried an HTTP loopback tohttp://127.0.0.1/with the site'sHost:header first, and the success test wasif (!is_wp_error($result))— it never inspected the HTTP status code. On hosts where127.0.0.1+Hostheader resolves to the wrong vhost (or the route is blocked), that request returns 404/403, which is not aWP_Error. The old code therefore treated the 404 as a successful spawn, loggedSpawned … (HTTP 404), returnedtrue, and never reached the HTTPS fallback. The worker never ran; nothing drained the queue or the staging table. Sites whose127.0.0.1loopback resolves correctly returned 200 and were unaffected — which is why this was host-specific.Fix.
spawnWorker()now reads the response code and only treats 2xx as a successful spawn. On any non-2xx (or aWP_Error) from the loopback it logs a warning and falls through to therest_url()HTTPS path, which is likewise status-checked. A new constant short-circuits the doomed attempt on known-bad hosts:```php
define('AUCTIONFORGE_WORKER_SKIP_LOOPBACK', true); // wp-config.php — skip 127.0.0.1, go straight to HTTPS
```
Added — Automatic staging recovery on the safety cron
The hourly
auctionforge_sync_safetycron previously only spawned a worker when the queue had pending jobs. Lots that were staged intoimport_historybut left unconverted (e.g. after a worker died, or while the spawn bug above was in effect) had no queue job attached, so nothing ever picked them up. The safety cron now also checksSyncQueue::stagingBacklogCount()and, when it is non-zero, kicks the staging processor (non-blocking) to convert the backlog into posts — making the pipeline self-healing without manual intervention.Added — "Process Staging" button on Sync Status
A new Process Staging button in the Sync Queue toolbar (Lots → Sync Status) runs
InsertLotsBSover the staging table on demand — for when lots are staged but not appearing on the site. It fires non-blocking and switches the Live Console into fast-poll so the operator can watch posts being created in batches of 50.Internal
- Modified:
inc/sync/SyncQueue.php—spawnWorker()rewritten with HTTP-status-aware fallback +AUCTIONFORGE_WORKER_SKIP_LOOPBACKsupport;triggerStagingProcessor()gained a$blockingparameter (non-blocking fire-and-forget mode for cron/admin); newstagingBacklogCount()helper (countsimport_historyrows withsyncFinished = 0). - Modified:
auctionforge.php—auctionforge_sync_safety_action()now drains orphaned staging via the non-blocking processor when a backlog exists; newprocess_stagingcase in theauctionforge_sync_enqueueAJAX handler; plugin header bumped to 3.7.0.32. - Modified:
admin/view/sync-status.php— added the "Process Staging" button and its click handler.
---
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
-
v3.7.1.5 Stable Jul 12, 2026Download v3.7.1.5📦 9.21 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.1.2] — 2026-06-30
Fixed — Background sync worker spawn timing out ("Worker spawn HTTPS failed: cURL error 28")
On busy or memory-pressured hosts the background sync worker frequently failed to start, logging
Worker spawn HTTPS failed: cURL error 28: Operation timed out after 5002 milliseconds with 0 bytes received(observed ~29×/day on one site).SyncQueue::spawnWorker()triggers the worker with a blocking loopback HTTP request to the site's own REST endpoint; the timeout was hard-coded to 5 seconds, so whenever the site's own backend took longer than 5s to respond (DB/FPM contention) the spawn aborted *as a hard error* and raised a sync alert — even though the site was up and serving.Three changes make the spawn resilient:
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
AUCTIONFORGE_WORKER_SPAWN_TIMEOUTconstant or theauctionforge_worker_spawn_timeoutfilter. - Scheme-matched loopback. Strategy 1 now issues the
127.0.0.1loopback using the site's own scheme (https when the site is https) instead of always http. Previously an http→https redirect (301) made the loopback look like a failure and forced the slower public-URL fallback; the POST now reaches the worker endpoint directly. - Non-blocking best-effort fallback. If both blocking attempts can't confirm a start within the timeout, the worker is now triggered with a final non-blocking (fire-and-forget) request, logged at warning instead of error. A merely-slow backend still starts its worker, the hourly
SafetySynccron re-spawns if it genuinely didn't, and the condition no longer raises a hard sync alert.
No schema, DB, or config-version change. Behaviour is unchanged on healthy hosts (the loopback still returns 2xx immediately); the new paths engage only when the backend is slow. The underlying slow backend was a server-side issue (MariaDB query-cache lock contention + memory pressure) fixed at the infrastructure level; this release hardens the plugin so a transiently slow backend can no longer turn into a failed sync.
---
[3.7.1.1] — 2026-06-29
Added — Active uptime monitoring via Kuma keyword health marker
The keepalive mu-plugin now emits an invisible
<!-- af-health:ok -->marker into every rendered front-end page (hooked onwp_headandwp_footer, emitted once per request, pure echo — no DB/IO, lands inside cached HTML). Uptime Kuma monitors each site by pulling the homepage and asserting the marker is present, which measures real user-facing availability (DNS + TLS + nginx + PHP + WordPress + DB + render) instead of the old push-heartbeat model that only measured whether WP-Cron was firing — and routinely false-flagged perfectly healthy sites as down (the worst offenders flapped 50–60×/day while serving HTTP 200 the entire time).Because the marker lives in the mu-plugin (which WordPress can't deactivate and AuctionForge re-asserts on every load), it reflects "WordPress is rendering pages" independently of AuctionForge's own activation state, and it survives failed or in-progress plugin updates. The bundled keepalive template is bumped to 1.3.0, so the installer rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpon update. The legacy push heartbeat is retained in this release for a safe transition to active monitoring.---
[3.7.0.43] — 2026-06-22
Fixed — Submission "Need help?" contact was hardcoded (one tenant's details on every site)
The vehicle-submission help panel (
templates/js-chunks/submissions.php) rendered a single hardcoded email and phone, so every AuctionForge site displayed the same contact regardless of which auction house it was — e.g. one tenant'sinfo@absoluteclearance.co.uk/01536 674804showed on unrelated sites.The panel now resolves the contact dynamically, in priority order:
apply_filters('auctionforge_help_email', '')/apply_filters('auctionforge_help_phone', '')— explicit per-site override (any source, e.g. a theme/global/Elementor setting);- otherwise the SEO & AEO Optimizer LocalBusiness details stored in the
seo_aeo_optionsoption (local_email/local_phone); - if neither yields a value the corresponding row is omitted, and the whole "Need help?" block is hidden when no contact is available — so no placeholder or wrong-tenant detail is ever shown.
Internal
- Modified:
templates/js-chunks/submissions.php— replaced the two hardcoded<p>contact values with filtered,esc_html-escaped values resolved fromseo_aeo_options, each row guarded by anif. - Modified:
auctionforge.php— plugin header → 3.7.0.43. - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.42] — 2026-06-22
Fixed — Submission templates emitted extra
<h1>s and placeholder content site-wideThe vehicle-submission view (
templates/js-chunks/submissions.php) is rendered onwp_footeron every page, so its<script type="text/template">chunks shipped into the source of all pages. Two of those chunks used<h1>headings ("Submit your vehicle for auction" and "Detail received successfully"), meaning every page's HTML source contained three<h1>elements. While these live inside inert<script>templates (so a DOM-rendering crawler like Googlebot still resolves a single true H1), raw-source SEO tooling and some AI crawlers flag the duplicates, and the templates also shipped vendor placeholder text.- Both template headings demoted from
<h1>to<h2>— a single H1 per page in source. - Replaced Lorem-ipsum placeholder copy in the intro and progress panels with real submission guidance.
- Replaced the dummy "Need help?" contact details (
support@domain.com/+3265464325) with the live business contact (info@absoluteclearance.co.uk/01536 674804).
No functional/JS changes — template IDs and structure are unchanged.
---
[3.7.0.41] — 2026-06-19
Fixed — Duplicate auction terms (auction shown twice, catalogue split)
A newly published auction could be created twice in WordPress as two identical
auction_numberterms, so the auction rendered twice on the listing and its catalogue was split across the two copies (e.g. 145 lots stored as 144 + 1). The lot posts themselves stayed unique — this was a duplicate *auction*, not duplicate *lots*.Root cause: auction terms are only created in
Auction::save()viawp_insert_category, which inserts whenever the caller's lookup returned no term. That lookup (findAuctionByIdWithLang()→get_terms()meta_query) can return nothing for a brand-new auction when two syncs run concurrently — for example afull_sync_*bulk run (viaBulkSyncWorker/AuctionList) overlapping a catalogue webhook. Both see "no term" and both insert one. The only fallback,findAuctionByNumber(), looks up by the auction *number* slug and never matched the real term slug, so it could not catch the miss. This path is not covered by the existingCatalogSyncWorkerper-auction lock, which serialises catalogue syncs to prevent duplicate *lot posts* but does not guard *term* creation.Auction::save()now, before inserting a term:- acquires a per-auction MySQL advisory lock (
GET_LOCK('<prefix>af_term_<auctionId>', 10)) so concurrent creators serialise — the loser then finds the just-created term and updates it instead of inserting a second one; and - performs an authoritative, cache-bypassing lookup by
auction_id(directtermmeta⋈term_taxonomyquery) and switches to update-in-place when a term already exists.
Polylang-aware: under full multilingual support it only reuses a term in the requested language, so legitimate per-language terms are still created.
Internal
- Modified:
inc/model/Auction.php—save()gains a pre-insert dedup guard; new private helpersfindTermIdByAuctionId(),acquireTermLock(),releaseTermLock().InstanceDBwraps the global$wpdb, so the advisory lock and thewp_insert_categoryinsert share a single DB session (the same property that makes the existingCatalogSyncWorkerlock effective). - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.40] — 2026-06-14
Fixed — Lot "begin to close" countdown now shows minutes (and seconds in the final minute)
The lot-page countdown (
#auctionStartTimer, the *"Auction will begin to close in …"* line) only ever rendered whole days and whole hours, and computed once on page load. With less than an hour to go it therefore read "0 days and 0 hours" — e.g. a lot closing at 20:00 looked like a dead/expired timer from 19:00 onwards, even though ~46 minutes remained. The time maths was correct; the widget simply had no sub-hour granularity and never ticked.It now updates every second and shows the largest meaningful units, dropping leading zero units:
44 minutes•3 hours and 20 minutes•1 day, 2 hours and 5 minutes- In the final minute it counts the seconds down (
40 seconds→1 second) so it never sits on a flat "0". - Once the close time passes (
result < 0) the timer hides itself, as before.
getCountdownText()now wraps the suffix in a.bp-start-countdownspan so the script can rewrite the whole phrase; if that wrapper is ever absent (e.g. a stale page-cached markup),getLeftDate()falls back to the original days+hours behaviour rather than erroring — important becauseapp.jsalso carries the live bidding logic.Internal
- Modified:
inc/controllers/Settings.php—getCountdownText()suffix wrapped in.bp-start-countdown. - Modified:
assets/js/app.js—getLeftDate()rewritten: 1s ticking viasetInterval, days/hours/minutes/seconds, zero-unit omission, pluralisation, and a wrapper-missing fallback. This is the file actually served (front-end$jsFileis hardcoded to.js). - Modified:
src/js/app.js— samegetLeftDate()change, to keep the gulp source in sync.app.min.jsis intentionally not regenerated (unused — the unminifiedapp.jsis enqueued). - Modified:
auctionforge.php— plugin header → 3.7.0.40. (3.7.0.39 was present in the header with no changelog entry; this release supersedes it.)
---
[3.7.0.38] — 2026-06-02
Added — Spelling Dialect "Auto" mode (match the auction house region)
The Spelling Dialect setting (Settings → Appearance) gains a third option, "Auto — match the auction house region", which becomes the new default for sites that haven't chosen one:
- During the existing auction sync (
getAllAuctions),Dialect::captureRegionFromAuctions()reads the auction house's country from the auctionaddress(trailing segment;currencyas a fallback) and stores the resolved dialect in thespelling_dialect_autooption. No API calls happen on page loads — it's a fixed, site-wide value refreshed each sync, so it's fully page-cache-safe. - Mapping: US → Catalog; every other country → Catalogue (American "Catalog" is US-only; UK/IE/AU/NZ/CA/etc. use "Catalogue").
- The admin field shows what was detected, e.g. *"Detected region: United Kingdom → Catalogue."* United States and United Kingdom remain manual hard-overrides.
Default change: new/unconfigured sites now default to Auto (was UK). Until a site's first sync resolves the region, Auto falls back to UK, so behaviour is unchanged until detection kicks in — then it self-corrects (a US house starts showing "Catalog" automatically).
Internal
- Modified:
inc/helpers/Dialect.php—automode,FALLBACK_DIALECT,countryToDialect(),captureRegionFromAuctions();DEFAULT_DIALECT→auto. - Modified:
inc/import/AuctionList.php— capture the house region during the auctions sync. - Modified:
inc/controllers/Settings.php— Auto dropdown option + detected-region note. - Modified:
auctionforge.php— plugin header → 3.7.0.38.
---
[3.7.0.37] — 2026-06-02
Fixed — Per-lot "full price VAT" now drives the VAT statement
The standalone VAT line on lot pages (
vat-and-commission→.vat-field) was keyed off the auction-house VAT rule, so it always read *"VAT: X% on item price and commission"* regardless of a lot's full-price-VAT toggle — and toggling a lot off didn't change it. It now branches on the live per-lot flag (itemInfo.item.fullPriceVat), matching the price-breakdown calc ingetFullPriceInfo():- Full-price VAT on → *"VAT: X% on item price and commission"* (VAT applied to hammer + commission)
- off (default) → *"VAT: X% on commission only"*
This only changes the wording to match the amount
getFullPriceInfo()already computes per the toggle; the VAT figure itself was already correct. The separaterenderVatField()"no VAT on commission" path is unchanged.Internal
- Modified:
templates/js-chunks/vat-and-commission.php— VAT statement branches on the per-lotfullPriceVatinstead ofhouseCommissionVatRule. - Modified:
auctionforge.php— plugin header → 3.7.0.37.
---
[3.7.0.36] — 2026-06-02
A Site Statistics release: day-of-week analysis, an opt-in weekly email report, lot-focused "top pages", and several dashboard refinements.
Added — Day-of-week tabs on the hourly charts
The Hourly Distribution (Site Statistics → Overview) and Peak Viewing Times (Lot Analytics) charts now have a day tab strip — All days · Mon · Tue … Sun — each showing that weekday's total. Selecting a day redraws the 24-hour chart consolidated across every matching date in the range, so you can see *which days and times* visitors are active. The lot-analytics data already carried
day_of_week; it was previously flattened to 24 hours and is now surfaced.Added — Scheduled weekly email report (opt-in, off by default)
A new report under Site Statistics → Settings emails a polished weekly summary: headline KPI cards (Visitors / Pageviews / Sessions with this-week-vs-last-week change), a visitor journey funnel (Visitors → Viewed a lot → Left a bid), busiest hour for each weekday, and the week's most-viewed lots. Driven by a daily cron that sends once on a configurable weekday. Supports one or several recipients (comma/semicolon/space/newline separated, validated and de-duped) and a "Send test now" button that sends to whatever is currently in the Recipient(s) box. Built table-based with inline CSS for Gmail/Outlook/Apple Mail compatibility.
Added — New vs Returning visitors, auction markers, engagement funnel
- New vs Returning visitor KPI cards on the Overview (new = authoritative
af_stats_visitors.first_seenwithin the period, joined so it can never exceed total visitors). - Auction-date markers drawn on the *Traffic Over Time* chart (dashed lines + a captioned list) so traffic can be read against sale dates.
- Engagement funnel panel: Visitors → Viewed a lot → Viewed 5+ lots → Bidders (bidders from synced
af_bid_events).
Changed — "Top pages" is now lot-focused ("Top Lots" / "Most viewed lots")
Both the dashboard Overview panel and the email now list the most-viewed individual lot pages only (
/lot-…), excluding home, catalogue listing/pagination and profile pages as low-signal. Email page titles are shown in full (no mid-word truncation) with the site-name suffix stripped; the dedicated Pages tab remains the unfiltered drill-down.Changed — Lot Analytics "Peak Viewing Times" honours the global date range
Previously hard-coded to the last 30 days; it now respects the dashboard's range picker. The Lot Analytics tab also auto-loads on first click (previously required a manual refresh).
Changed — Chart.js bundled locally
The Statistics and plugin dashboards loaded Chart.js from a CDN at two different versions (
@4and@4.4.1). Chart.js 4.4.1 is now bundled atadmin/assets/js/chart.umd.min.jsand loaded from the plugin (version-busted), so charts work offline / behind strict CSP and the version is pinned.Internal
- Added:
inc/stats/class-stats-report.php—AF_Stats_Report(weekly cron, recipient parsing, HTML builder, "send test" AJAX).admin/assets/js/chart.umd.min.js— bundled Chart.js 4.4.1. - Modified:
inc/stats/class-stats-ajax.php— hourly-by-DOW query;new_visitors,auction_events,funnelin the dashboard payload; lot-only top pages; global date range passed to peak times; weekly-email settings saved. - Modified:
inc/model/LotAnalytics.php—getPeakViewingTimes()accepts an explicit$from/$torange. - Modified:
inc/stats/class-stats-init.php— require + registerAF_Stats_Report. - Modified:
admin/view/stats/dashboard.php— day-of-week tabs, KPI cards, auction-marker plugin, funnel, Top Lots, weekly-email settings UI + test button, local Chart.js. - Modified:
admin/view/dashboard.php— local Chart.js. - Modified:
auctionforge.php— plugin header → 3.7.0.36.
---
[3.7.0.35] — 2026-06-02
Added — Regional spelling dialect ("Catalog" US / "Catalogue" UK)
A new Spelling Dialect dropdown at the top of Settings → Appearance lets each site choose how the word "catalog" is presented on the public front end:
- United States — Catalog — US spelling (the plugin's original authored wording).
- United Kingdom — Catalogue — UK spelling.
When UK is selected, every front-end occurrence is rewritten — *"Catalogue will be available soon"*, *"View catalogue"*, *"Catalogue Coming Soon"*, *"Download catalogue"*, *"Back to the catalogue"*, the lot-page Catalogue tab, and the "catalogue published" app notification.
How it works. Every user-facing catalog string already passes through the
auctionforgetext domain (__()), so the swap is implemented as a singlegettext/gettext_with_contextfilter rather than by editing template files.Dialect::toUk()rewrites whole words only, preserving case and pluralisation (Catalog→Catalogue,View catalog→View catalogue,CATALOG→CATALOGUE,Catalogs→Catalogues), and is idempotent. The filter is front-end only (is_admin()-guarded, with awp_doing_ajax()exception so front-end AJAX is still covered) and is scoped to theauctionforgedomain, so it never touches WordPress core, the theme, or other plugins. Code identifiers, option keys (catalog_layout,bids_in_catalog), CSS classes (af-catalog-sidebar,catalog-section,bp-back-to-catalog-button) and JS variables are all left intact. On US sites the filter is not registered at all, so there is zero added overhead.Internal
- Added:
inc/helpers/Dialect.php—Dialecthelper:get()/isUk()(reads thespelling_dialectoption, default UK),word()for non-translated strings, the case/plural-preservingtoUk()transform, thegettextfilter callbacks, andregister()(no-op on US sites). - Modified:
inc/coreIncludes.php— requireDialect.phpand callDialect::register()at plugin load. - Modified:
inc/controllers/Settings.php—usetheDialecthelper; add the "Regional Spelling" sub-heading and Spelling Dialect field at the top of the Appearance section, plus thefill_bidspirit_options_spelling_dialect()renderer. The value persists through the existingbidspirit_settingsoption and sanitize callback. - Modified:
auctionforge.php— plugin header → 3.7.0.35.
Note for existing sites. The default dialect is UK ("Catalogue"). Any site that upgrades without an explicit
spelling_dialectvalue will therefore display "Catalogue" on the front end. Sites that should keep US spelling must set Settings → Appearance → Spelling Dialect to United States — Catalog (stored asspelling_dialect = us). Admin labels are unaffected in either dialect.---
[3.7.0.34] — 2026-05-30
Fixed — Duplicate lot posts created by concurrent catalog syncs for the same auction
When two catalog-sync jobs for the same auction ran at the same time, every lot was imported twice and the front-end showed each lot as a pair (the second post taking a
-2slug). Observed on a live sale: 157 lots became 313 publishedlotposts.Root cause. Two BidSpirit webhooks arriving in the same second (e.g.
CATALOG_PUBLISHED+AUCTION_CATALOG_UPDATED) each enqueue acatalog_sync.SyncQueue::enqueue()deduplicates with a non-atomic *SELECT-then-INSERT* (no DB unique constraint), so two simultaneous calls both pass the "already pending?" check and both jobs land in the queue. Job *claiming* is atomic per-job, but nothing serialised two different jobs for the same auction, so two workers ran concurrently. The matcherLot::findByBSMeta()only sees committedpublish/draftposts, so during the overlapping insert windows each worker saw "no existing post" for every lot and inserted a fresh full set. Worse,findByBSMetadoesGROUP BY lotId, so the resulting duplicate pair is collapsed to a single row in every later sync — the importer never detects or repairs it, and thecount($datum) > 1"Double" guard inInsertLotsBS::init()is effectively dead code.Fix.
CatalogSyncWorker::process()andprocessDayEnded()now take a per-auction MySQL advisory lock (GET_LOCK('<prefix>af_sync_<auctionId>', 0)) around the API-fetch + post-insertion work. If another worker already holds the lock for that auction, the duplicate job logsconcurrent-lock busyand returns cleanly — marked completed, since the in-flight sync already covers the work — instead of racing. The lock is held on the worker's own DB connection and auto-releases if the process dies, so it can never deadlock the queue. This serialises all catalog and day-ended processing per auction no matter how many duplicate jobs reach the queue.Internal
- Modified:
inc/sync/CatalogSyncWorker.php— newacquireAuctionLock()/releaseAuctionLock()helpers;process()andprocessDayEnded()acquire the lock up front (skip-on-busy) and release it in afinally. - Modified:
auctionforge.php— plugin header → 3.7.0.34.
Note for existing sites. This prevents *new* duplicates; it does not remove ones already created. To find them, look for published
lotposts where two share one BidSpirit lot id (bp_meta_data.id). Keep the most-recently-synced post per lot id and set the rest topost_status = 'trash'— thelotpost type has trash disabled, so do it via SQL, and do not usedraft(whichfindByBSMetastill matches). Then recount theauction_numberterm and clear caches.---
[3.7.0.33] — 2026-05-30
Fixed —
auctionforge_sync_lotscron never scheduled (Lots push to wpdeploy silently stopped)AF_Lots_Syncuses the custombp_ten_minutelyschedule. The sync crons were registered fromAF_Stats_Init::init(), which runs at plugin-load time — *before*auctionforge.phpaddsbp_ten_minutelyto thecron_schedulesfilter further down the same file. Sowp_schedule_event(time(), 'bp_ten_minutely', 'auctionforge_sync_lots')ran against an unknown schedule, failed silently, and left the hook permanently unscheduled. The other four sync hooks use core'shourlyand were unaffected — which is why only the Lots push to wpdeploy went stale (observed: 3 days with no push while every other stream ran on time). Fixed by deferring the syncregister()calls to theinithook, by which point every custom schedule is registered. The hook self-arms on the next request after upgrade.Fixed — Safety Sync admin run reported a false "Request failed (HTTP 200)"
auctionforge_ajax_run_cron_taskreturns its result as JSON, but a stray PHP notice/deprecation emitted by a task (e.g. the Safety Sync BidSpirit delta-check on PHP 8.1) printed into the response body ahead of the JSON, so the browser couldn't parse it and the modal showed a misleading "Request failed (HTTP 200)" even though the task succeeded. The handler now buffers task output, discards it before emitting JSON, and logs any suppressed output viaSyncLogger(contextadmin) so the offending notice can still be found and fixed at the root.Added — "Clear duplicate cron events" button on Sync Status
The one-shot
AF_Cron_Cleanupmigration only runs once per site, so per-hostname duplicate events created by an older build *after* the migration had already marked itself done were never swept. NewAF_Cron_Cleanup::force_sweep()runs the same sweep on demand, exposed via a Clear Cron Dupes button in the Sync Queue toolbar (AJAXauctionforge_clear_cron_dupes). It removes legacy-args duplicates of the four per-hostname hooks plus the two orphan hooks, keeping one clean event each.Internal
- Modified:
inc/stats/class-stats-init.php— syncregister()calls wrapped inadd_action('init', …, 20). - Modified:
inc/maintenance/class-cron-cleanup.php— new publicforce_sweep()reusing the existing sweep helpers. - Modified:
auctionforge.php—auctionforge_ajax_run_cron_task()output-buffered against stray output; newauctionforge_ajax_clear_cron_dupes()handler + registration; plugin header → 3.7.0.33. - Modified:
admin/view/sync-status.php— added the "Clear Cron Dupes" button and handler.
Note: with all scheduling now arg-free (3.7.0.28+), the duplicates do not regenerate once swept; a canonical-hostname redirect (www ↔ apex) at the web-server level remains good hygiene but is no longer required to keep the cron table clean.
---
[3.7.0.32] — 2026-05-30
Fixed — V3 sync worker never spawned on some hosts (silent import stall)
On some sites (confirmed on a LiteSpeed/cPanel install) the V3 queue worker never actually started, so catalog lots were staged into
import_historybut never converted to WordPress posts —postId IS NULL,syncFinished = 0,syncTimes = 0, and no errors logged. Clearing the queue and reprocessing re-stalled immediately.Root cause.
SyncQueue::spawnWorker()starts the worker by POSTing the REST route/wp-json/auctionforge/v1/sync-worker. It tried an HTTP loopback tohttp://127.0.0.1/with the site'sHost:header first, and the success test wasif (!is_wp_error($result))— it never inspected the HTTP status code. On hosts where127.0.0.1+Hostheader resolves to the wrong vhost (or the route is blocked), that request returns 404/403, which is not aWP_Error. The old code therefore treated the 404 as a successful spawn, loggedSpawned … (HTTP 404), returnedtrue, and never reached the HTTPS fallback. The worker never ran; nothing drained the queue or the staging table. Sites whose127.0.0.1loopback resolves correctly returned 200 and were unaffected — which is why this was host-specific.Fix.
spawnWorker()now reads the response code and only treats 2xx as a successful spawn. On any non-2xx (or aWP_Error) from the loopback it logs a warning and falls through to therest_url()HTTPS path, which is likewise status-checked. A new constant short-circuits the doomed attempt on known-bad hosts:```php
define('AUCTIONFORGE_WORKER_SKIP_LOOPBACK', true); // wp-config.php — skip 127.0.0.1, go straight to HTTPS
```
Added — Automatic staging recovery on the safety cron
The hourly
auctionforge_sync_safetycron previously only spawned a worker when the queue had pending jobs. Lots that were staged intoimport_historybut left unconverted (e.g. after a worker died, or while the spawn bug above was in effect) had no queue job attached, so nothing ever picked them up. The safety cron now also checksSyncQueue::stagingBacklogCount()and, when it is non-zero, kicks the staging processor (non-blocking) to convert the backlog into posts — making the pipeline self-healing without manual intervention.Added — "Process Staging" button on Sync Status
A new Process Staging button in the Sync Queue toolbar (Lots → Sync Status) runs
InsertLotsBSover the staging table on demand — for when lots are staged but not appearing on the site. It fires non-blocking and switches the Live Console into fast-poll so the operator can watch posts being created in batches of 50.Internal
- Modified:
inc/sync/SyncQueue.php—spawnWorker()rewritten with HTTP-status-aware fallback +AUCTIONFORGE_WORKER_SKIP_LOOPBACKsupport;triggerStagingProcessor()gained a$blockingparameter (non-blocking fire-and-forget mode for cron/admin); newstagingBacklogCount()helper (countsimport_historyrows withsyncFinished = 0). - Modified:
auctionforge.php—auctionforge_sync_safety_action()now drains orphaned staging via the non-blocking processor when a backlog exists; newprocess_stagingcase in theauctionforge_sync_enqueueAJAX handler; plugin header bumped to 3.7.0.32. - Modified:
admin/view/sync-status.php— added the "Process Staging" button and its click handler.
---
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
-
v3.7.1.4 Stable Jul 11, 2026Download v3.7.1.4📦 9.19 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.1.2] — 2026-06-30
Fixed — Background sync worker spawn timing out ("Worker spawn HTTPS failed: cURL error 28")
On busy or memory-pressured hosts the background sync worker frequently failed to start, logging
Worker spawn HTTPS failed: cURL error 28: Operation timed out after 5002 milliseconds with 0 bytes received(observed ~29×/day on one site).SyncQueue::spawnWorker()triggers the worker with a blocking loopback HTTP request to the site's own REST endpoint; the timeout was hard-coded to 5 seconds, so whenever the site's own backend took longer than 5s to respond (DB/FPM contention) the spawn aborted *as a hard error* and raised a sync alert — even though the site was up and serving.Three changes make the spawn resilient:
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
AUCTIONFORGE_WORKER_SPAWN_TIMEOUTconstant or theauctionforge_worker_spawn_timeoutfilter. - Scheme-matched loopback. Strategy 1 now issues the
127.0.0.1loopback using the site's own scheme (https when the site is https) instead of always http. Previously an http→https redirect (301) made the loopback look like a failure and forced the slower public-URL fallback; the POST now reaches the worker endpoint directly. - Non-blocking best-effort fallback. If both blocking attempts can't confirm a start within the timeout, the worker is now triggered with a final non-blocking (fire-and-forget) request, logged at warning instead of error. A merely-slow backend still starts its worker, the hourly
SafetySynccron re-spawns if it genuinely didn't, and the condition no longer raises a hard sync alert.
No schema, DB, or config-version change. Behaviour is unchanged on healthy hosts (the loopback still returns 2xx immediately); the new paths engage only when the backend is slow. The underlying slow backend was a server-side issue (MariaDB query-cache lock contention + memory pressure) fixed at the infrastructure level; this release hardens the plugin so a transiently slow backend can no longer turn into a failed sync.
---
[3.7.1.1] — 2026-06-29
Added — Active uptime monitoring via Kuma keyword health marker
The keepalive mu-plugin now emits an invisible
<!-- af-health:ok -->marker into every rendered front-end page (hooked onwp_headandwp_footer, emitted once per request, pure echo — no DB/IO, lands inside cached HTML). Uptime Kuma monitors each site by pulling the homepage and asserting the marker is present, which measures real user-facing availability (DNS + TLS + nginx + PHP + WordPress + DB + render) instead of the old push-heartbeat model that only measured whether WP-Cron was firing — and routinely false-flagged perfectly healthy sites as down (the worst offenders flapped 50–60×/day while serving HTTP 200 the entire time).Because the marker lives in the mu-plugin (which WordPress can't deactivate and AuctionForge re-asserts on every load), it reflects "WordPress is rendering pages" independently of AuctionForge's own activation state, and it survives failed or in-progress plugin updates. The bundled keepalive template is bumped to 1.3.0, so the installer rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpon update. The legacy push heartbeat is retained in this release for a safe transition to active monitoring.---
[3.7.0.43] — 2026-06-22
Fixed — Submission "Need help?" contact was hardcoded (one tenant's details on every site)
The vehicle-submission help panel (
templates/js-chunks/submissions.php) rendered a single hardcoded email and phone, so every AuctionForge site displayed the same contact regardless of which auction house it was — e.g. one tenant'sinfo@absoluteclearance.co.uk/01536 674804showed on unrelated sites.The panel now resolves the contact dynamically, in priority order:
apply_filters('auctionforge_help_email', '')/apply_filters('auctionforge_help_phone', '')— explicit per-site override (any source, e.g. a theme/global/Elementor setting);- otherwise the SEO & AEO Optimizer LocalBusiness details stored in the
seo_aeo_optionsoption (local_email/local_phone); - if neither yields a value the corresponding row is omitted, and the whole "Need help?" block is hidden when no contact is available — so no placeholder or wrong-tenant detail is ever shown.
Internal
- Modified:
templates/js-chunks/submissions.php— replaced the two hardcoded<p>contact values with filtered,esc_html-escaped values resolved fromseo_aeo_options, each row guarded by anif. - Modified:
auctionforge.php— plugin header → 3.7.0.43. - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.42] — 2026-06-22
Fixed — Submission templates emitted extra
<h1>s and placeholder content site-wideThe vehicle-submission view (
templates/js-chunks/submissions.php) is rendered onwp_footeron every page, so its<script type="text/template">chunks shipped into the source of all pages. Two of those chunks used<h1>headings ("Submit your vehicle for auction" and "Detail received successfully"), meaning every page's HTML source contained three<h1>elements. While these live inside inert<script>templates (so a DOM-rendering crawler like Googlebot still resolves a single true H1), raw-source SEO tooling and some AI crawlers flag the duplicates, and the templates also shipped vendor placeholder text.- Both template headings demoted from
<h1>to<h2>— a single H1 per page in source. - Replaced Lorem-ipsum placeholder copy in the intro and progress panels with real submission guidance.
- Replaced the dummy "Need help?" contact details (
support@domain.com/+3265464325) with the live business contact (info@absoluteclearance.co.uk/01536 674804).
No functional/JS changes — template IDs and structure are unchanged.
---
[3.7.0.41] — 2026-06-19
Fixed — Duplicate auction terms (auction shown twice, catalogue split)
A newly published auction could be created twice in WordPress as two identical
auction_numberterms, so the auction rendered twice on the listing and its catalogue was split across the two copies (e.g. 145 lots stored as 144 + 1). The lot posts themselves stayed unique — this was a duplicate *auction*, not duplicate *lots*.Root cause: auction terms are only created in
Auction::save()viawp_insert_category, which inserts whenever the caller's lookup returned no term. That lookup (findAuctionByIdWithLang()→get_terms()meta_query) can return nothing for a brand-new auction when two syncs run concurrently — for example afull_sync_*bulk run (viaBulkSyncWorker/AuctionList) overlapping a catalogue webhook. Both see "no term" and both insert one. The only fallback,findAuctionByNumber(), looks up by the auction *number* slug and never matched the real term slug, so it could not catch the miss. This path is not covered by the existingCatalogSyncWorkerper-auction lock, which serialises catalogue syncs to prevent duplicate *lot posts* but does not guard *term* creation.Auction::save()now, before inserting a term:- acquires a per-auction MySQL advisory lock (
GET_LOCK('<prefix>af_term_<auctionId>', 10)) so concurrent creators serialise — the loser then finds the just-created term and updates it instead of inserting a second one; and - performs an authoritative, cache-bypassing lookup by
auction_id(directtermmeta⋈term_taxonomyquery) and switches to update-in-place when a term already exists.
Polylang-aware: under full multilingual support it only reuses a term in the requested language, so legitimate per-language terms are still created.
Internal
- Modified:
inc/model/Auction.php—save()gains a pre-insert dedup guard; new private helpersfindTermIdByAuctionId(),acquireTermLock(),releaseTermLock().InstanceDBwraps the global$wpdb, so the advisory lock and thewp_insert_categoryinsert share a single DB session (the same property that makes the existingCatalogSyncWorkerlock effective). - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.40] — 2026-06-14
Fixed — Lot "begin to close" countdown now shows minutes (and seconds in the final minute)
The lot-page countdown (
#auctionStartTimer, the *"Auction will begin to close in …"* line) only ever rendered whole days and whole hours, and computed once on page load. With less than an hour to go it therefore read "0 days and 0 hours" — e.g. a lot closing at 20:00 looked like a dead/expired timer from 19:00 onwards, even though ~46 minutes remained. The time maths was correct; the widget simply had no sub-hour granularity and never ticked.It now updates every second and shows the largest meaningful units, dropping leading zero units:
44 minutes•3 hours and 20 minutes•1 day, 2 hours and 5 minutes- In the final minute it counts the seconds down (
40 seconds→1 second) so it never sits on a flat "0". - Once the close time passes (
result < 0) the timer hides itself, as before.
getCountdownText()now wraps the suffix in a.bp-start-countdownspan so the script can rewrite the whole phrase; if that wrapper is ever absent (e.g. a stale page-cached markup),getLeftDate()falls back to the original days+hours behaviour rather than erroring — important becauseapp.jsalso carries the live bidding logic.Internal
- Modified:
inc/controllers/Settings.php—getCountdownText()suffix wrapped in.bp-start-countdown. - Modified:
assets/js/app.js—getLeftDate()rewritten: 1s ticking viasetInterval, days/hours/minutes/seconds, zero-unit omission, pluralisation, and a wrapper-missing fallback. This is the file actually served (front-end$jsFileis hardcoded to.js). - Modified:
src/js/app.js— samegetLeftDate()change, to keep the gulp source in sync.app.min.jsis intentionally not regenerated (unused — the unminifiedapp.jsis enqueued). - Modified:
auctionforge.php— plugin header → 3.7.0.40. (3.7.0.39 was present in the header with no changelog entry; this release supersedes it.)
---
[3.7.0.38] — 2026-06-02
Added — Spelling Dialect "Auto" mode (match the auction house region)
The Spelling Dialect setting (Settings → Appearance) gains a third option, "Auto — match the auction house region", which becomes the new default for sites that haven't chosen one:
- During the existing auction sync (
getAllAuctions),Dialect::captureRegionFromAuctions()reads the auction house's country from the auctionaddress(trailing segment;currencyas a fallback) and stores the resolved dialect in thespelling_dialect_autooption. No API calls happen on page loads — it's a fixed, site-wide value refreshed each sync, so it's fully page-cache-safe. - Mapping: US → Catalog; every other country → Catalogue (American "Catalog" is US-only; UK/IE/AU/NZ/CA/etc. use "Catalogue").
- The admin field shows what was detected, e.g. *"Detected region: United Kingdom → Catalogue."* United States and United Kingdom remain manual hard-overrides.
Default change: new/unconfigured sites now default to Auto (was UK). Until a site's first sync resolves the region, Auto falls back to UK, so behaviour is unchanged until detection kicks in — then it self-corrects (a US house starts showing "Catalog" automatically).
Internal
- Modified:
inc/helpers/Dialect.php—automode,FALLBACK_DIALECT,countryToDialect(),captureRegionFromAuctions();DEFAULT_DIALECT→auto. - Modified:
inc/import/AuctionList.php— capture the house region during the auctions sync. - Modified:
inc/controllers/Settings.php— Auto dropdown option + detected-region note. - Modified:
auctionforge.php— plugin header → 3.7.0.38.
---
[3.7.0.37] — 2026-06-02
Fixed — Per-lot "full price VAT" now drives the VAT statement
The standalone VAT line on lot pages (
vat-and-commission→.vat-field) was keyed off the auction-house VAT rule, so it always read *"VAT: X% on item price and commission"* regardless of a lot's full-price-VAT toggle — and toggling a lot off didn't change it. It now branches on the live per-lot flag (itemInfo.item.fullPriceVat), matching the price-breakdown calc ingetFullPriceInfo():- Full-price VAT on → *"VAT: X% on item price and commission"* (VAT applied to hammer + commission)
- off (default) → *"VAT: X% on commission only"*
This only changes the wording to match the amount
getFullPriceInfo()already computes per the toggle; the VAT figure itself was already correct. The separaterenderVatField()"no VAT on commission" path is unchanged.Internal
- Modified:
templates/js-chunks/vat-and-commission.php— VAT statement branches on the per-lotfullPriceVatinstead ofhouseCommissionVatRule. - Modified:
auctionforge.php— plugin header → 3.7.0.37.
---
[3.7.0.36] — 2026-06-02
A Site Statistics release: day-of-week analysis, an opt-in weekly email report, lot-focused "top pages", and several dashboard refinements.
Added — Day-of-week tabs on the hourly charts
The Hourly Distribution (Site Statistics → Overview) and Peak Viewing Times (Lot Analytics) charts now have a day tab strip — All days · Mon · Tue … Sun — each showing that weekday's total. Selecting a day redraws the 24-hour chart consolidated across every matching date in the range, so you can see *which days and times* visitors are active. The lot-analytics data already carried
day_of_week; it was previously flattened to 24 hours and is now surfaced.Added — Scheduled weekly email report (opt-in, off by default)
A new report under Site Statistics → Settings emails a polished weekly summary: headline KPI cards (Visitors / Pageviews / Sessions with this-week-vs-last-week change), a visitor journey funnel (Visitors → Viewed a lot → Left a bid), busiest hour for each weekday, and the week's most-viewed lots. Driven by a daily cron that sends once on a configurable weekday. Supports one or several recipients (comma/semicolon/space/newline separated, validated and de-duped) and a "Send test now" button that sends to whatever is currently in the Recipient(s) box. Built table-based with inline CSS for Gmail/Outlook/Apple Mail compatibility.
Added — New vs Returning visitors, auction markers, engagement funnel
- New vs Returning visitor KPI cards on the Overview (new = authoritative
af_stats_visitors.first_seenwithin the period, joined so it can never exceed total visitors). - Auction-date markers drawn on the *Traffic Over Time* chart (dashed lines + a captioned list) so traffic can be read against sale dates.
- Engagement funnel panel: Visitors → Viewed a lot → Viewed 5+ lots → Bidders (bidders from synced
af_bid_events).
Changed — "Top pages" is now lot-focused ("Top Lots" / "Most viewed lots")
Both the dashboard Overview panel and the email now list the most-viewed individual lot pages only (
/lot-…), excluding home, catalogue listing/pagination and profile pages as low-signal. Email page titles are shown in full (no mid-word truncation) with the site-name suffix stripped; the dedicated Pages tab remains the unfiltered drill-down.Changed — Lot Analytics "Peak Viewing Times" honours the global date range
Previously hard-coded to the last 30 days; it now respects the dashboard's range picker. The Lot Analytics tab also auto-loads on first click (previously required a manual refresh).
Changed — Chart.js bundled locally
The Statistics and plugin dashboards loaded Chart.js from a CDN at two different versions (
@4and@4.4.1). Chart.js 4.4.1 is now bundled atadmin/assets/js/chart.umd.min.jsand loaded from the plugin (version-busted), so charts work offline / behind strict CSP and the version is pinned.Internal
- Added:
inc/stats/class-stats-report.php—AF_Stats_Report(weekly cron, recipient parsing, HTML builder, "send test" AJAX).admin/assets/js/chart.umd.min.js— bundled Chart.js 4.4.1. - Modified:
inc/stats/class-stats-ajax.php— hourly-by-DOW query;new_visitors,auction_events,funnelin the dashboard payload; lot-only top pages; global date range passed to peak times; weekly-email settings saved. - Modified:
inc/model/LotAnalytics.php—getPeakViewingTimes()accepts an explicit$from/$torange. - Modified:
inc/stats/class-stats-init.php— require + registerAF_Stats_Report. - Modified:
admin/view/stats/dashboard.php— day-of-week tabs, KPI cards, auction-marker plugin, funnel, Top Lots, weekly-email settings UI + test button, local Chart.js. - Modified:
admin/view/dashboard.php— local Chart.js. - Modified:
auctionforge.php— plugin header → 3.7.0.36.
---
[3.7.0.35] — 2026-06-02
Added — Regional spelling dialect ("Catalog" US / "Catalogue" UK)
A new Spelling Dialect dropdown at the top of Settings → Appearance lets each site choose how the word "catalog" is presented on the public front end:
- United States — Catalog — US spelling (the plugin's original authored wording).
- United Kingdom — Catalogue — UK spelling.
When UK is selected, every front-end occurrence is rewritten — *"Catalogue will be available soon"*, *"View catalogue"*, *"Catalogue Coming Soon"*, *"Download catalogue"*, *"Back to the catalogue"*, the lot-page Catalogue tab, and the "catalogue published" app notification.
How it works. Every user-facing catalog string already passes through the
auctionforgetext domain (__()), so the swap is implemented as a singlegettext/gettext_with_contextfilter rather than by editing template files.Dialect::toUk()rewrites whole words only, preserving case and pluralisation (Catalog→Catalogue,View catalog→View catalogue,CATALOG→CATALOGUE,Catalogs→Catalogues), and is idempotent. The filter is front-end only (is_admin()-guarded, with awp_doing_ajax()exception so front-end AJAX is still covered) and is scoped to theauctionforgedomain, so it never touches WordPress core, the theme, or other plugins. Code identifiers, option keys (catalog_layout,bids_in_catalog), CSS classes (af-catalog-sidebar,catalog-section,bp-back-to-catalog-button) and JS variables are all left intact. On US sites the filter is not registered at all, so there is zero added overhead.Internal
- Added:
inc/helpers/Dialect.php—Dialecthelper:get()/isUk()(reads thespelling_dialectoption, default UK),word()for non-translated strings, the case/plural-preservingtoUk()transform, thegettextfilter callbacks, andregister()(no-op on US sites). - Modified:
inc/coreIncludes.php— requireDialect.phpand callDialect::register()at plugin load. - Modified:
inc/controllers/Settings.php—usetheDialecthelper; add the "Regional Spelling" sub-heading and Spelling Dialect field at the top of the Appearance section, plus thefill_bidspirit_options_spelling_dialect()renderer. The value persists through the existingbidspirit_settingsoption and sanitize callback. - Modified:
auctionforge.php— plugin header → 3.7.0.35.
Note for existing sites. The default dialect is UK ("Catalogue"). Any site that upgrades without an explicit
spelling_dialectvalue will therefore display "Catalogue" on the front end. Sites that should keep US spelling must set Settings → Appearance → Spelling Dialect to United States — Catalog (stored asspelling_dialect = us). Admin labels are unaffected in either dialect.---
[3.7.0.34] — 2026-05-30
Fixed — Duplicate lot posts created by concurrent catalog syncs for the same auction
When two catalog-sync jobs for the same auction ran at the same time, every lot was imported twice and the front-end showed each lot as a pair (the second post taking a
-2slug). Observed on a live sale: 157 lots became 313 publishedlotposts.Root cause. Two BidSpirit webhooks arriving in the same second (e.g.
CATALOG_PUBLISHED+AUCTION_CATALOG_UPDATED) each enqueue acatalog_sync.SyncQueue::enqueue()deduplicates with a non-atomic *SELECT-then-INSERT* (no DB unique constraint), so two simultaneous calls both pass the "already pending?" check and both jobs land in the queue. Job *claiming* is atomic per-job, but nothing serialised two different jobs for the same auction, so two workers ran concurrently. The matcherLot::findByBSMeta()only sees committedpublish/draftposts, so during the overlapping insert windows each worker saw "no existing post" for every lot and inserted a fresh full set. Worse,findByBSMetadoesGROUP BY lotId, so the resulting duplicate pair is collapsed to a single row in every later sync — the importer never detects or repairs it, and thecount($datum) > 1"Double" guard inInsertLotsBS::init()is effectively dead code.Fix.
CatalogSyncWorker::process()andprocessDayEnded()now take a per-auction MySQL advisory lock (GET_LOCK('<prefix>af_sync_<auctionId>', 0)) around the API-fetch + post-insertion work. If another worker already holds the lock for that auction, the duplicate job logsconcurrent-lock busyand returns cleanly — marked completed, since the in-flight sync already covers the work — instead of racing. The lock is held on the worker's own DB connection and auto-releases if the process dies, so it can never deadlock the queue. This serialises all catalog and day-ended processing per auction no matter how many duplicate jobs reach the queue.Internal
- Modified:
inc/sync/CatalogSyncWorker.php— newacquireAuctionLock()/releaseAuctionLock()helpers;process()andprocessDayEnded()acquire the lock up front (skip-on-busy) and release it in afinally. - Modified:
auctionforge.php— plugin header → 3.7.0.34.
Note for existing sites. This prevents *new* duplicates; it does not remove ones already created. To find them, look for published
lotposts where two share one BidSpirit lot id (bp_meta_data.id). Keep the most-recently-synced post per lot id and set the rest topost_status = 'trash'— thelotpost type has trash disabled, so do it via SQL, and do not usedraft(whichfindByBSMetastill matches). Then recount theauction_numberterm and clear caches.---
[3.7.0.33] — 2026-05-30
Fixed —
auctionforge_sync_lotscron never scheduled (Lots push to wpdeploy silently stopped)AF_Lots_Syncuses the custombp_ten_minutelyschedule. The sync crons were registered fromAF_Stats_Init::init(), which runs at plugin-load time — *before*auctionforge.phpaddsbp_ten_minutelyto thecron_schedulesfilter further down the same file. Sowp_schedule_event(time(), 'bp_ten_minutely', 'auctionforge_sync_lots')ran against an unknown schedule, failed silently, and left the hook permanently unscheduled. The other four sync hooks use core'shourlyand were unaffected — which is why only the Lots push to wpdeploy went stale (observed: 3 days with no push while every other stream ran on time). Fixed by deferring the syncregister()calls to theinithook, by which point every custom schedule is registered. The hook self-arms on the next request after upgrade.Fixed — Safety Sync admin run reported a false "Request failed (HTTP 200)"
auctionforge_ajax_run_cron_taskreturns its result as JSON, but a stray PHP notice/deprecation emitted by a task (e.g. the Safety Sync BidSpirit delta-check on PHP 8.1) printed into the response body ahead of the JSON, so the browser couldn't parse it and the modal showed a misleading "Request failed (HTTP 200)" even though the task succeeded. The handler now buffers task output, discards it before emitting JSON, and logs any suppressed output viaSyncLogger(contextadmin) so the offending notice can still be found and fixed at the root.Added — "Clear duplicate cron events" button on Sync Status
The one-shot
AF_Cron_Cleanupmigration only runs once per site, so per-hostname duplicate events created by an older build *after* the migration had already marked itself done were never swept. NewAF_Cron_Cleanup::force_sweep()runs the same sweep on demand, exposed via a Clear Cron Dupes button in the Sync Queue toolbar (AJAXauctionforge_clear_cron_dupes). It removes legacy-args duplicates of the four per-hostname hooks plus the two orphan hooks, keeping one clean event each.Internal
- Modified:
inc/stats/class-stats-init.php— syncregister()calls wrapped inadd_action('init', …, 20). - Modified:
inc/maintenance/class-cron-cleanup.php— new publicforce_sweep()reusing the existing sweep helpers. - Modified:
auctionforge.php—auctionforge_ajax_run_cron_task()output-buffered against stray output; newauctionforge_ajax_clear_cron_dupes()handler + registration; plugin header → 3.7.0.33. - Modified:
admin/view/sync-status.php— added the "Clear Cron Dupes" button and handler.
Note: with all scheduling now arg-free (3.7.0.28+), the duplicates do not regenerate once swept; a canonical-hostname redirect (www ↔ apex) at the web-server level remains good hygiene but is no longer required to keep the cron table clean.
---
[3.7.0.32] — 2026-05-30
Fixed — V3 sync worker never spawned on some hosts (silent import stall)
On some sites (confirmed on a LiteSpeed/cPanel install) the V3 queue worker never actually started, so catalog lots were staged into
import_historybut never converted to WordPress posts —postId IS NULL,syncFinished = 0,syncTimes = 0, and no errors logged. Clearing the queue and reprocessing re-stalled immediately.Root cause.
SyncQueue::spawnWorker()starts the worker by POSTing the REST route/wp-json/auctionforge/v1/sync-worker. It tried an HTTP loopback tohttp://127.0.0.1/with the site'sHost:header first, and the success test wasif (!is_wp_error($result))— it never inspected the HTTP status code. On hosts where127.0.0.1+Hostheader resolves to the wrong vhost (or the route is blocked), that request returns 404/403, which is not aWP_Error. The old code therefore treated the 404 as a successful spawn, loggedSpawned … (HTTP 404), returnedtrue, and never reached the HTTPS fallback. The worker never ran; nothing drained the queue or the staging table. Sites whose127.0.0.1loopback resolves correctly returned 200 and were unaffected — which is why this was host-specific.Fix.
spawnWorker()now reads the response code and only treats 2xx as a successful spawn. On any non-2xx (or aWP_Error) from the loopback it logs a warning and falls through to therest_url()HTTPS path, which is likewise status-checked. A new constant short-circuits the doomed attempt on known-bad hosts:```php
define('AUCTIONFORGE_WORKER_SKIP_LOOPBACK', true); // wp-config.php — skip 127.0.0.1, go straight to HTTPS
```
Added — Automatic staging recovery on the safety cron
The hourly
auctionforge_sync_safetycron previously only spawned a worker when the queue had pending jobs. Lots that were staged intoimport_historybut left unconverted (e.g. after a worker died, or while the spawn bug above was in effect) had no queue job attached, so nothing ever picked them up. The safety cron now also checksSyncQueue::stagingBacklogCount()and, when it is non-zero, kicks the staging processor (non-blocking) to convert the backlog into posts — making the pipeline self-healing without manual intervention.Added — "Process Staging" button on Sync Status
A new Process Staging button in the Sync Queue toolbar (Lots → Sync Status) runs
InsertLotsBSover the staging table on demand — for when lots are staged but not appearing on the site. It fires non-blocking and switches the Live Console into fast-poll so the operator can watch posts being created in batches of 50.Internal
- Modified:
inc/sync/SyncQueue.php—spawnWorker()rewritten with HTTP-status-aware fallback +AUCTIONFORGE_WORKER_SKIP_LOOPBACKsupport;triggerStagingProcessor()gained a$blockingparameter (non-blocking fire-and-forget mode for cron/admin); newstagingBacklogCount()helper (countsimport_historyrows withsyncFinished = 0). - Modified:
auctionforge.php—auctionforge_sync_safety_action()now drains orphaned staging via the non-blocking processor when a backlog exists; newprocess_stagingcase in theauctionforge_sync_enqueueAJAX handler; plugin header bumped to 3.7.0.32. - Modified:
admin/view/sync-status.php— added the "Process Staging" button and its click handler.
---
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
-
v3.7.1.3 Stable Jul 02, 2026Download v3.7.1.3📦 9.14 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.1.2] — 2026-06-30
Fixed — Background sync worker spawn timing out ("Worker spawn HTTPS failed: cURL error 28")
On busy or memory-pressured hosts the background sync worker frequently failed to start, logging
Worker spawn HTTPS failed: cURL error 28: Operation timed out after 5002 milliseconds with 0 bytes received(observed ~29×/day on one site).SyncQueue::spawnWorker()triggers the worker with a blocking loopback HTTP request to the site's own REST endpoint; the timeout was hard-coded to 5 seconds, so whenever the site's own backend took longer than 5s to respond (DB/FPM contention) the spawn aborted *as a hard error* and raised a sync alert — even though the site was up and serving.Three changes make the spawn resilient:
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
AUCTIONFORGE_WORKER_SPAWN_TIMEOUTconstant or theauctionforge_worker_spawn_timeoutfilter. - Scheme-matched loopback. Strategy 1 now issues the
127.0.0.1loopback using the site's own scheme (https when the site is https) instead of always http. Previously an http→https redirect (301) made the loopback look like a failure and forced the slower public-URL fallback; the POST now reaches the worker endpoint directly. - Non-blocking best-effort fallback. If both blocking attempts can't confirm a start within the timeout, the worker is now triggered with a final non-blocking (fire-and-forget) request, logged at warning instead of error. A merely-slow backend still starts its worker, the hourly
SafetySynccron re-spawns if it genuinely didn't, and the condition no longer raises a hard sync alert.
No schema, DB, or config-version change. Behaviour is unchanged on healthy hosts (the loopback still returns 2xx immediately); the new paths engage only when the backend is slow. The underlying slow backend was a server-side issue (MariaDB query-cache lock contention + memory pressure) fixed at the infrastructure level; this release hardens the plugin so a transiently slow backend can no longer turn into a failed sync.
---
[3.7.1.1] — 2026-06-29
Added — Active uptime monitoring via Kuma keyword health marker
The keepalive mu-plugin now emits an invisible
<!-- af-health:ok -->marker into every rendered front-end page (hooked onwp_headandwp_footer, emitted once per request, pure echo — no DB/IO, lands inside cached HTML). Uptime Kuma monitors each site by pulling the homepage and asserting the marker is present, which measures real user-facing availability (DNS + TLS + nginx + PHP + WordPress + DB + render) instead of the old push-heartbeat model that only measured whether WP-Cron was firing — and routinely false-flagged perfectly healthy sites as down (the worst offenders flapped 50–60×/day while serving HTTP 200 the entire time).Because the marker lives in the mu-plugin (which WordPress can't deactivate and AuctionForge re-asserts on every load), it reflects "WordPress is rendering pages" independently of AuctionForge's own activation state, and it survives failed or in-progress plugin updates. The bundled keepalive template is bumped to 1.3.0, so the installer rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpon update. The legacy push heartbeat is retained in this release for a safe transition to active monitoring.---
[3.7.0.43] — 2026-06-22
Fixed — Submission "Need help?" contact was hardcoded (one tenant's details on every site)
The vehicle-submission help panel (
templates/js-chunks/submissions.php) rendered a single hardcoded email and phone, so every AuctionForge site displayed the same contact regardless of which auction house it was — e.g. one tenant'sinfo@absoluteclearance.co.uk/01536 674804showed on unrelated sites.The panel now resolves the contact dynamically, in priority order:
apply_filters('auctionforge_help_email', '')/apply_filters('auctionforge_help_phone', '')— explicit per-site override (any source, e.g. a theme/global/Elementor setting);- otherwise the SEO & AEO Optimizer LocalBusiness details stored in the
seo_aeo_optionsoption (local_email/local_phone); - if neither yields a value the corresponding row is omitted, and the whole "Need help?" block is hidden when no contact is available — so no placeholder or wrong-tenant detail is ever shown.
Internal
- Modified:
templates/js-chunks/submissions.php— replaced the two hardcoded<p>contact values with filtered,esc_html-escaped values resolved fromseo_aeo_options, each row guarded by anif. - Modified:
auctionforge.php— plugin header → 3.7.0.43. - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.42] — 2026-06-22
Fixed — Submission templates emitted extra
<h1>s and placeholder content site-wideThe vehicle-submission view (
templates/js-chunks/submissions.php) is rendered onwp_footeron every page, so its<script type="text/template">chunks shipped into the source of all pages. Two of those chunks used<h1>headings ("Submit your vehicle for auction" and "Detail received successfully"), meaning every page's HTML source contained three<h1>elements. While these live inside inert<script>templates (so a DOM-rendering crawler like Googlebot still resolves a single true H1), raw-source SEO tooling and some AI crawlers flag the duplicates, and the templates also shipped vendor placeholder text.- Both template headings demoted from
<h1>to<h2>— a single H1 per page in source. - Replaced Lorem-ipsum placeholder copy in the intro and progress panels with real submission guidance.
- Replaced the dummy "Need help?" contact details (
support@domain.com/+3265464325) with the live business contact (info@absoluteclearance.co.uk/01536 674804).
No functional/JS changes — template IDs and structure are unchanged.
---
[3.7.0.41] — 2026-06-19
Fixed — Duplicate auction terms (auction shown twice, catalogue split)
A newly published auction could be created twice in WordPress as two identical
auction_numberterms, so the auction rendered twice on the listing and its catalogue was split across the two copies (e.g. 145 lots stored as 144 + 1). The lot posts themselves stayed unique — this was a duplicate *auction*, not duplicate *lots*.Root cause: auction terms are only created in
Auction::save()viawp_insert_category, which inserts whenever the caller's lookup returned no term. That lookup (findAuctionByIdWithLang()→get_terms()meta_query) can return nothing for a brand-new auction when two syncs run concurrently — for example afull_sync_*bulk run (viaBulkSyncWorker/AuctionList) overlapping a catalogue webhook. Both see "no term" and both insert one. The only fallback,findAuctionByNumber(), looks up by the auction *number* slug and never matched the real term slug, so it could not catch the miss. This path is not covered by the existingCatalogSyncWorkerper-auction lock, which serialises catalogue syncs to prevent duplicate *lot posts* but does not guard *term* creation.Auction::save()now, before inserting a term:- acquires a per-auction MySQL advisory lock (
GET_LOCK('<prefix>af_term_<auctionId>', 10)) so concurrent creators serialise — the loser then finds the just-created term and updates it instead of inserting a second one; and - performs an authoritative, cache-bypassing lookup by
auction_id(directtermmeta⋈term_taxonomyquery) and switches to update-in-place when a term already exists.
Polylang-aware: under full multilingual support it only reuses a term in the requested language, so legitimate per-language terms are still created.
Internal
- Modified:
inc/model/Auction.php—save()gains a pre-insert dedup guard; new private helpersfindTermIdByAuctionId(),acquireTermLock(),releaseTermLock().InstanceDBwraps the global$wpdb, so the advisory lock and thewp_insert_categoryinsert share a single DB session (the same property that makes the existingCatalogSyncWorkerlock effective). - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.40] — 2026-06-14
Fixed — Lot "begin to close" countdown now shows minutes (and seconds in the final minute)
The lot-page countdown (
#auctionStartTimer, the *"Auction will begin to close in …"* line) only ever rendered whole days and whole hours, and computed once on page load. With less than an hour to go it therefore read "0 days and 0 hours" — e.g. a lot closing at 20:00 looked like a dead/expired timer from 19:00 onwards, even though ~46 minutes remained. The time maths was correct; the widget simply had no sub-hour granularity and never ticked.It now updates every second and shows the largest meaningful units, dropping leading zero units:
44 minutes•3 hours and 20 minutes•1 day, 2 hours and 5 minutes- In the final minute it counts the seconds down (
40 seconds→1 second) so it never sits on a flat "0". - Once the close time passes (
result < 0) the timer hides itself, as before.
getCountdownText()now wraps the suffix in a.bp-start-countdownspan so the script can rewrite the whole phrase; if that wrapper is ever absent (e.g. a stale page-cached markup),getLeftDate()falls back to the original days+hours behaviour rather than erroring — important becauseapp.jsalso carries the live bidding logic.Internal
- Modified:
inc/controllers/Settings.php—getCountdownText()suffix wrapped in.bp-start-countdown. - Modified:
assets/js/app.js—getLeftDate()rewritten: 1s ticking viasetInterval, days/hours/minutes/seconds, zero-unit omission, pluralisation, and a wrapper-missing fallback. This is the file actually served (front-end$jsFileis hardcoded to.js). - Modified:
src/js/app.js— samegetLeftDate()change, to keep the gulp source in sync.app.min.jsis intentionally not regenerated (unused — the unminifiedapp.jsis enqueued). - Modified:
auctionforge.php— plugin header → 3.7.0.40. (3.7.0.39 was present in the header with no changelog entry; this release supersedes it.)
---
[3.7.0.38] — 2026-06-02
Added — Spelling Dialect "Auto" mode (match the auction house region)
The Spelling Dialect setting (Settings → Appearance) gains a third option, "Auto — match the auction house region", which becomes the new default for sites that haven't chosen one:
- During the existing auction sync (
getAllAuctions),Dialect::captureRegionFromAuctions()reads the auction house's country from the auctionaddress(trailing segment;currencyas a fallback) and stores the resolved dialect in thespelling_dialect_autooption. No API calls happen on page loads — it's a fixed, site-wide value refreshed each sync, so it's fully page-cache-safe. - Mapping: US → Catalog; every other country → Catalogue (American "Catalog" is US-only; UK/IE/AU/NZ/CA/etc. use "Catalogue").
- The admin field shows what was detected, e.g. *"Detected region: United Kingdom → Catalogue."* United States and United Kingdom remain manual hard-overrides.
Default change: new/unconfigured sites now default to Auto (was UK). Until a site's first sync resolves the region, Auto falls back to UK, so behaviour is unchanged until detection kicks in — then it self-corrects (a US house starts showing "Catalog" automatically).
Internal
- Modified:
inc/helpers/Dialect.php—automode,FALLBACK_DIALECT,countryToDialect(),captureRegionFromAuctions();DEFAULT_DIALECT→auto. - Modified:
inc/import/AuctionList.php— capture the house region during the auctions sync. - Modified:
inc/controllers/Settings.php— Auto dropdown option + detected-region note. - Modified:
auctionforge.php— plugin header → 3.7.0.38.
---
[3.7.0.37] — 2026-06-02
Fixed — Per-lot "full price VAT" now drives the VAT statement
The standalone VAT line on lot pages (
vat-and-commission→.vat-field) was keyed off the auction-house VAT rule, so it always read *"VAT: X% on item price and commission"* regardless of a lot's full-price-VAT toggle — and toggling a lot off didn't change it. It now branches on the live per-lot flag (itemInfo.item.fullPriceVat), matching the price-breakdown calc ingetFullPriceInfo():- Full-price VAT on → *"VAT: X% on item price and commission"* (VAT applied to hammer + commission)
- off (default) → *"VAT: X% on commission only"*
This only changes the wording to match the amount
getFullPriceInfo()already computes per the toggle; the VAT figure itself was already correct. The separaterenderVatField()"no VAT on commission" path is unchanged.Internal
- Modified:
templates/js-chunks/vat-and-commission.php— VAT statement branches on the per-lotfullPriceVatinstead ofhouseCommissionVatRule. - Modified:
auctionforge.php— plugin header → 3.7.0.37.
---
[3.7.0.36] — 2026-06-02
A Site Statistics release: day-of-week analysis, an opt-in weekly email report, lot-focused "top pages", and several dashboard refinements.
Added — Day-of-week tabs on the hourly charts
The Hourly Distribution (Site Statistics → Overview) and Peak Viewing Times (Lot Analytics) charts now have a day tab strip — All days · Mon · Tue … Sun — each showing that weekday's total. Selecting a day redraws the 24-hour chart consolidated across every matching date in the range, so you can see *which days and times* visitors are active. The lot-analytics data already carried
day_of_week; it was previously flattened to 24 hours and is now surfaced.Added — Scheduled weekly email report (opt-in, off by default)
A new report under Site Statistics → Settings emails a polished weekly summary: headline KPI cards (Visitors / Pageviews / Sessions with this-week-vs-last-week change), a visitor journey funnel (Visitors → Viewed a lot → Left a bid), busiest hour for each weekday, and the week's most-viewed lots. Driven by a daily cron that sends once on a configurable weekday. Supports one or several recipients (comma/semicolon/space/newline separated, validated and de-duped) and a "Send test now" button that sends to whatever is currently in the Recipient(s) box. Built table-based with inline CSS for Gmail/Outlook/Apple Mail compatibility.
Added — New vs Returning visitors, auction markers, engagement funnel
- New vs Returning visitor KPI cards on the Overview (new = authoritative
af_stats_visitors.first_seenwithin the period, joined so it can never exceed total visitors). - Auction-date markers drawn on the *Traffic Over Time* chart (dashed lines + a captioned list) so traffic can be read against sale dates.
- Engagement funnel panel: Visitors → Viewed a lot → Viewed 5+ lots → Bidders (bidders from synced
af_bid_events).
Changed — "Top pages" is now lot-focused ("Top Lots" / "Most viewed lots")
Both the dashboard Overview panel and the email now list the most-viewed individual lot pages only (
/lot-…), excluding home, catalogue listing/pagination and profile pages as low-signal. Email page titles are shown in full (no mid-word truncation) with the site-name suffix stripped; the dedicated Pages tab remains the unfiltered drill-down.Changed — Lot Analytics "Peak Viewing Times" honours the global date range
Previously hard-coded to the last 30 days; it now respects the dashboard's range picker. The Lot Analytics tab also auto-loads on first click (previously required a manual refresh).
Changed — Chart.js bundled locally
The Statistics and plugin dashboards loaded Chart.js from a CDN at two different versions (
@4and@4.4.1). Chart.js 4.4.1 is now bundled atadmin/assets/js/chart.umd.min.jsand loaded from the plugin (version-busted), so charts work offline / behind strict CSP and the version is pinned.Internal
- Added:
inc/stats/class-stats-report.php—AF_Stats_Report(weekly cron, recipient parsing, HTML builder, "send test" AJAX).admin/assets/js/chart.umd.min.js— bundled Chart.js 4.4.1. - Modified:
inc/stats/class-stats-ajax.php— hourly-by-DOW query;new_visitors,auction_events,funnelin the dashboard payload; lot-only top pages; global date range passed to peak times; weekly-email settings saved. - Modified:
inc/model/LotAnalytics.php—getPeakViewingTimes()accepts an explicit$from/$torange. - Modified:
inc/stats/class-stats-init.php— require + registerAF_Stats_Report. - Modified:
admin/view/stats/dashboard.php— day-of-week tabs, KPI cards, auction-marker plugin, funnel, Top Lots, weekly-email settings UI + test button, local Chart.js. - Modified:
admin/view/dashboard.php— local Chart.js. - Modified:
auctionforge.php— plugin header → 3.7.0.36.
---
[3.7.0.35] — 2026-06-02
Added — Regional spelling dialect ("Catalog" US / "Catalogue" UK)
A new Spelling Dialect dropdown at the top of Settings → Appearance lets each site choose how the word "catalog" is presented on the public front end:
- United States — Catalog — US spelling (the plugin's original authored wording).
- United Kingdom — Catalogue — UK spelling.
When UK is selected, every front-end occurrence is rewritten — *"Catalogue will be available soon"*, *"View catalogue"*, *"Catalogue Coming Soon"*, *"Download catalogue"*, *"Back to the catalogue"*, the lot-page Catalogue tab, and the "catalogue published" app notification.
How it works. Every user-facing catalog string already passes through the
auctionforgetext domain (__()), so the swap is implemented as a singlegettext/gettext_with_contextfilter rather than by editing template files.Dialect::toUk()rewrites whole words only, preserving case and pluralisation (Catalog→Catalogue,View catalog→View catalogue,CATALOG→CATALOGUE,Catalogs→Catalogues), and is idempotent. The filter is front-end only (is_admin()-guarded, with awp_doing_ajax()exception so front-end AJAX is still covered) and is scoped to theauctionforgedomain, so it never touches WordPress core, the theme, or other plugins. Code identifiers, option keys (catalog_layout,bids_in_catalog), CSS classes (af-catalog-sidebar,catalog-section,bp-back-to-catalog-button) and JS variables are all left intact. On US sites the filter is not registered at all, so there is zero added overhead.Internal
- Added:
inc/helpers/Dialect.php—Dialecthelper:get()/isUk()(reads thespelling_dialectoption, default UK),word()for non-translated strings, the case/plural-preservingtoUk()transform, thegettextfilter callbacks, andregister()(no-op on US sites). - Modified:
inc/coreIncludes.php— requireDialect.phpand callDialect::register()at plugin load. - Modified:
inc/controllers/Settings.php—usetheDialecthelper; add the "Regional Spelling" sub-heading and Spelling Dialect field at the top of the Appearance section, plus thefill_bidspirit_options_spelling_dialect()renderer. The value persists through the existingbidspirit_settingsoption and sanitize callback. - Modified:
auctionforge.php— plugin header → 3.7.0.35.
Note for existing sites. The default dialect is UK ("Catalogue"). Any site that upgrades without an explicit
spelling_dialectvalue will therefore display "Catalogue" on the front end. Sites that should keep US spelling must set Settings → Appearance → Spelling Dialect to United States — Catalog (stored asspelling_dialect = us). Admin labels are unaffected in either dialect.---
[3.7.0.34] — 2026-05-30
Fixed — Duplicate lot posts created by concurrent catalog syncs for the same auction
When two catalog-sync jobs for the same auction ran at the same time, every lot was imported twice and the front-end showed each lot as a pair (the second post taking a
-2slug). Observed on a live sale: 157 lots became 313 publishedlotposts.Root cause. Two BidSpirit webhooks arriving in the same second (e.g.
CATALOG_PUBLISHED+AUCTION_CATALOG_UPDATED) each enqueue acatalog_sync.SyncQueue::enqueue()deduplicates with a non-atomic *SELECT-then-INSERT* (no DB unique constraint), so two simultaneous calls both pass the "already pending?" check and both jobs land in the queue. Job *claiming* is atomic per-job, but nothing serialised two different jobs for the same auction, so two workers ran concurrently. The matcherLot::findByBSMeta()only sees committedpublish/draftposts, so during the overlapping insert windows each worker saw "no existing post" for every lot and inserted a fresh full set. Worse,findByBSMetadoesGROUP BY lotId, so the resulting duplicate pair is collapsed to a single row in every later sync — the importer never detects or repairs it, and thecount($datum) > 1"Double" guard inInsertLotsBS::init()is effectively dead code.Fix.
CatalogSyncWorker::process()andprocessDayEnded()now take a per-auction MySQL advisory lock (GET_LOCK('<prefix>af_sync_<auctionId>', 0)) around the API-fetch + post-insertion work. If another worker already holds the lock for that auction, the duplicate job logsconcurrent-lock busyand returns cleanly — marked completed, since the in-flight sync already covers the work — instead of racing. The lock is held on the worker's own DB connection and auto-releases if the process dies, so it can never deadlock the queue. This serialises all catalog and day-ended processing per auction no matter how many duplicate jobs reach the queue.Internal
- Modified:
inc/sync/CatalogSyncWorker.php— newacquireAuctionLock()/releaseAuctionLock()helpers;process()andprocessDayEnded()acquire the lock up front (skip-on-busy) and release it in afinally. - Modified:
auctionforge.php— plugin header → 3.7.0.34.
Note for existing sites. This prevents *new* duplicates; it does not remove ones already created. To find them, look for published
lotposts where two share one BidSpirit lot id (bp_meta_data.id). Keep the most-recently-synced post per lot id and set the rest topost_status = 'trash'— thelotpost type has trash disabled, so do it via SQL, and do not usedraft(whichfindByBSMetastill matches). Then recount theauction_numberterm and clear caches.---
[3.7.0.33] — 2026-05-30
Fixed —
auctionforge_sync_lotscron never scheduled (Lots push to wpdeploy silently stopped)AF_Lots_Syncuses the custombp_ten_minutelyschedule. The sync crons were registered fromAF_Stats_Init::init(), which runs at plugin-load time — *before*auctionforge.phpaddsbp_ten_minutelyto thecron_schedulesfilter further down the same file. Sowp_schedule_event(time(), 'bp_ten_minutely', 'auctionforge_sync_lots')ran against an unknown schedule, failed silently, and left the hook permanently unscheduled. The other four sync hooks use core'shourlyand were unaffected — which is why only the Lots push to wpdeploy went stale (observed: 3 days with no push while every other stream ran on time). Fixed by deferring the syncregister()calls to theinithook, by which point every custom schedule is registered. The hook self-arms on the next request after upgrade.Fixed — Safety Sync admin run reported a false "Request failed (HTTP 200)"
auctionforge_ajax_run_cron_taskreturns its result as JSON, but a stray PHP notice/deprecation emitted by a task (e.g. the Safety Sync BidSpirit delta-check on PHP 8.1) printed into the response body ahead of the JSON, so the browser couldn't parse it and the modal showed a misleading "Request failed (HTTP 200)" even though the task succeeded. The handler now buffers task output, discards it before emitting JSON, and logs any suppressed output viaSyncLogger(contextadmin) so the offending notice can still be found and fixed at the root.Added — "Clear duplicate cron events" button on Sync Status
The one-shot
AF_Cron_Cleanupmigration only runs once per site, so per-hostname duplicate events created by an older build *after* the migration had already marked itself done were never swept. NewAF_Cron_Cleanup::force_sweep()runs the same sweep on demand, exposed via a Clear Cron Dupes button in the Sync Queue toolbar (AJAXauctionforge_clear_cron_dupes). It removes legacy-args duplicates of the four per-hostname hooks plus the two orphan hooks, keeping one clean event each.Internal
- Modified:
inc/stats/class-stats-init.php— syncregister()calls wrapped inadd_action('init', …, 20). - Modified:
inc/maintenance/class-cron-cleanup.php— new publicforce_sweep()reusing the existing sweep helpers. - Modified:
auctionforge.php—auctionforge_ajax_run_cron_task()output-buffered against stray output; newauctionforge_ajax_clear_cron_dupes()handler + registration; plugin header → 3.7.0.33. - Modified:
admin/view/sync-status.php— added the "Clear Cron Dupes" button and handler.
Note: with all scheduling now arg-free (3.7.0.28+), the duplicates do not regenerate once swept; a canonical-hostname redirect (www ↔ apex) at the web-server level remains good hygiene but is no longer required to keep the cron table clean.
---
[3.7.0.32] — 2026-05-30
Fixed — V3 sync worker never spawned on some hosts (silent import stall)
On some sites (confirmed on a LiteSpeed/cPanel install) the V3 queue worker never actually started, so catalog lots were staged into
import_historybut never converted to WordPress posts —postId IS NULL,syncFinished = 0,syncTimes = 0, and no errors logged. Clearing the queue and reprocessing re-stalled immediately.Root cause.
SyncQueue::spawnWorker()starts the worker by POSTing the REST route/wp-json/auctionforge/v1/sync-worker. It tried an HTTP loopback tohttp://127.0.0.1/with the site'sHost:header first, and the success test wasif (!is_wp_error($result))— it never inspected the HTTP status code. On hosts where127.0.0.1+Hostheader resolves to the wrong vhost (or the route is blocked), that request returns 404/403, which is not aWP_Error. The old code therefore treated the 404 as a successful spawn, loggedSpawned … (HTTP 404), returnedtrue, and never reached the HTTPS fallback. The worker never ran; nothing drained the queue or the staging table. Sites whose127.0.0.1loopback resolves correctly returned 200 and were unaffected — which is why this was host-specific.Fix.
spawnWorker()now reads the response code and only treats 2xx as a successful spawn. On any non-2xx (or aWP_Error) from the loopback it logs a warning and falls through to therest_url()HTTPS path, which is likewise status-checked. A new constant short-circuits the doomed attempt on known-bad hosts:```php
define('AUCTIONFORGE_WORKER_SKIP_LOOPBACK', true); // wp-config.php — skip 127.0.0.1, go straight to HTTPS
```
Added — Automatic staging recovery on the safety cron
The hourly
auctionforge_sync_safetycron previously only spawned a worker when the queue had pending jobs. Lots that were staged intoimport_historybut left unconverted (e.g. after a worker died, or while the spawn bug above was in effect) had no queue job attached, so nothing ever picked them up. The safety cron now also checksSyncQueue::stagingBacklogCount()and, when it is non-zero, kicks the staging processor (non-blocking) to convert the backlog into posts — making the pipeline self-healing without manual intervention.Added — "Process Staging" button on Sync Status
A new Process Staging button in the Sync Queue toolbar (Lots → Sync Status) runs
InsertLotsBSover the staging table on demand — for when lots are staged but not appearing on the site. It fires non-blocking and switches the Live Console into fast-poll so the operator can watch posts being created in batches of 50.Internal
- Modified:
inc/sync/SyncQueue.php—spawnWorker()rewritten with HTTP-status-aware fallback +AUCTIONFORGE_WORKER_SKIP_LOOPBACKsupport;triggerStagingProcessor()gained a$blockingparameter (non-blocking fire-and-forget mode for cron/admin); newstagingBacklogCount()helper (countsimport_historyrows withsyncFinished = 0). - Modified:
auctionforge.php—auctionforge_sync_safety_action()now drains orphaned staging via the non-blocking processor when a backlog exists; newprocess_stagingcase in theauctionforge_sync_enqueueAJAX handler; plugin header bumped to 3.7.0.32. - Modified:
admin/view/sync-status.php— added the "Process Staging" button and its click handler.
---
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
-
v3.7.1.2 Stable Jun 30, 2026Download v3.7.1.2📦 9.22 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.1.2] — 2026-06-30
Fixed — Background sync worker spawn timing out ("Worker spawn HTTPS failed: cURL error 28")
On busy or memory-pressured hosts the background sync worker frequently failed to start, logging
Worker spawn HTTPS failed: cURL error 28: Operation timed out after 5002 milliseconds with 0 bytes received(observed ~29×/day on one site).SyncQueue::spawnWorker()triggers the worker with a blocking loopback HTTP request to the site's own REST endpoint; the timeout was hard-coded to 5 seconds, so whenever the site's own backend took longer than 5s to respond (DB/FPM contention) the spawn aborted *as a hard error* and raised a sync alert — even though the site was up and serving.Three changes make the spawn resilient:
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
AUCTIONFORGE_WORKER_SPAWN_TIMEOUTconstant or theauctionforge_worker_spawn_timeoutfilter. - Scheme-matched loopback. Strategy 1 now issues the
127.0.0.1loopback using the site's own scheme (https when the site is https) instead of always http. Previously an http→https redirect (301) made the loopback look like a failure and forced the slower public-URL fallback; the POST now reaches the worker endpoint directly. - Non-blocking best-effort fallback. If both blocking attempts can't confirm a start within the timeout, the worker is now triggered with a final non-blocking (fire-and-forget) request, logged at warning instead of error. A merely-slow backend still starts its worker, the hourly
SafetySynccron re-spawns if it genuinely didn't, and the condition no longer raises a hard sync alert.
No schema, DB, or config-version change. Behaviour is unchanged on healthy hosts (the loopback still returns 2xx immediately); the new paths engage only when the backend is slow. The underlying slow backend was a server-side issue (MariaDB query-cache lock contention + memory pressure) fixed at the infrastructure level; this release hardens the plugin so a transiently slow backend can no longer turn into a failed sync.
---
- Configurable, longer timeout. The 5s hard-coded timeout is now 15s by default, overridable via the
-
v3.7.1.1 Stable Jun 29, 2026Download v3.7.1.1📦 9.37 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.1.1] — 2026-06-29
Added — Active uptime monitoring via Kuma keyword health marker
The keepalive mu-plugin now emits an invisible
<!-- af-health:ok -->marker into every rendered front-end page (hooked onwp_headandwp_footer, emitted once per request, pure echo — no DB/IO, lands inside cached HTML). Uptime Kuma monitors each site by pulling the homepage and asserting the marker is present, which measures real user-facing availability (DNS + TLS + nginx + PHP + WordPress + DB + render) instead of the old push-heartbeat model that only measured whether WP-Cron was firing — and routinely false-flagged perfectly healthy sites as down (the worst offenders flapped 50–60×/day while serving HTTP 200 the entire time).Because the marker lives in the mu-plugin (which WordPress can't deactivate and AuctionForge re-asserts on every load), it reflects "WordPress is rendering pages" independently of AuctionForge's own activation state, and it survives failed or in-progress plugin updates. The bundled keepalive template is bumped to 1.3.0, so the installer rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpon update. The legacy push heartbeat is retained in this release for a safe transition to active monitoring.---
-
v3.7.1.0 Stable Jun 25, 2026Download v3.7.1.0📦 9.37 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.0.43] — 2026-06-22
Fixed — Submission "Need help?" contact was hardcoded (one tenant's details on every site)
The vehicle-submission help panel (
templates/js-chunks/submissions.php) rendered a single hardcoded email and phone, so every AuctionForge site displayed the same contact regardless of which auction house it was — e.g. one tenant'sinfo@absoluteclearance.co.uk/01536 674804showed on unrelated sites.The panel now resolves the contact dynamically, in priority order:
apply_filters('auctionforge_help_email', '')/apply_filters('auctionforge_help_phone', '')— explicit per-site override (any source, e.g. a theme/global/Elementor setting);- otherwise the SEO & AEO Optimizer LocalBusiness details stored in the
seo_aeo_optionsoption (local_email/local_phone); - if neither yields a value the corresponding row is omitted, and the whole "Need help?" block is hidden when no contact is available — so no placeholder or wrong-tenant detail is ever shown.
Internal
- Modified:
templates/js-chunks/submissions.php— replaced the two hardcoded<p>contact values with filtered,esc_html-escaped values resolved fromseo_aeo_options, each row guarded by anif. - Modified:
auctionforge.php— plugin header → 3.7.0.43. - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.42] — 2026-06-22
Fixed — Submission templates emitted extra
<h1>s and placeholder content site-wideThe vehicle-submission view (
templates/js-chunks/submissions.php) is rendered onwp_footeron every page, so its<script type="text/template">chunks shipped into the source of all pages. Two of those chunks used<h1>headings ("Submit your vehicle for auction" and "Detail received successfully"), meaning every page's HTML source contained three<h1>elements. While these live inside inert<script>templates (so a DOM-rendering crawler like Googlebot still resolves a single true H1), raw-source SEO tooling and some AI crawlers flag the duplicates, and the templates also shipped vendor placeholder text.- Both template headings demoted from
<h1>to<h2>— a single H1 per page in source. - Replaced Lorem-ipsum placeholder copy in the intro and progress panels with real submission guidance.
- Replaced the dummy "Need help?" contact details (
support@domain.com/+3265464325) with the live business contact (info@absoluteclearance.co.uk/01536 674804).
No functional/JS changes — template IDs and structure are unchanged.
---
[3.7.0.41] — 2026-06-19
Fixed — Duplicate auction terms (auction shown twice, catalogue split)
A newly published auction could be created twice in WordPress as two identical
auction_numberterms, so the auction rendered twice on the listing and its catalogue was split across the two copies (e.g. 145 lots stored as 144 + 1). The lot posts themselves stayed unique — this was a duplicate *auction*, not duplicate *lots*.Root cause: auction terms are only created in
Auction::save()viawp_insert_category, which inserts whenever the caller's lookup returned no term. That lookup (findAuctionByIdWithLang()→get_terms()meta_query) can return nothing for a brand-new auction when two syncs run concurrently — for example afull_sync_*bulk run (viaBulkSyncWorker/AuctionList) overlapping a catalogue webhook. Both see "no term" and both insert one. The only fallback,findAuctionByNumber(), looks up by the auction *number* slug and never matched the real term slug, so it could not catch the miss. This path is not covered by the existingCatalogSyncWorkerper-auction lock, which serialises catalogue syncs to prevent duplicate *lot posts* but does not guard *term* creation.Auction::save()now, before inserting a term:- acquires a per-auction MySQL advisory lock (
GET_LOCK('<prefix>af_term_<auctionId>', 10)) so concurrent creators serialise — the loser then finds the just-created term and updates it instead of inserting a second one; and - performs an authoritative, cache-bypassing lookup by
auction_id(directtermmeta⋈term_taxonomyquery) and switches to update-in-place when a term already exists.
Polylang-aware: under full multilingual support it only reuses a term in the requested language, so legitimate per-language terms are still created.
Internal
- Modified:
inc/model/Auction.php—save()gains a pre-insert dedup guard; new private helpersfindTermIdByAuctionId(),acquireTermLock(),releaseTermLock().InstanceDBwraps the global$wpdb, so the advisory lock and thewp_insert_categoryinsert share a single DB session (the same property that makes the existingCatalogSyncWorkerlock effective). - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
[3.7.0.40] — 2026-06-14
Fixed — Lot "begin to close" countdown now shows minutes (and seconds in the final minute)
The lot-page countdown (
#auctionStartTimer, the *"Auction will begin to close in …"* line) only ever rendered whole days and whole hours, and computed once on page load. With less than an hour to go it therefore read "0 days and 0 hours" — e.g. a lot closing at 20:00 looked like a dead/expired timer from 19:00 onwards, even though ~46 minutes remained. The time maths was correct; the widget simply had no sub-hour granularity and never ticked.It now updates every second and shows the largest meaningful units, dropping leading zero units:
44 minutes•3 hours and 20 minutes•1 day, 2 hours and 5 minutes- In the final minute it counts the seconds down (
40 seconds→1 second) so it never sits on a flat "0". - Once the close time passes (
result < 0) the timer hides itself, as before.
getCountdownText()now wraps the suffix in a.bp-start-countdownspan so the script can rewrite the whole phrase; if that wrapper is ever absent (e.g. a stale page-cached markup),getLeftDate()falls back to the original days+hours behaviour rather than erroring — important becauseapp.jsalso carries the live bidding logic.Internal
- Modified:
inc/controllers/Settings.php—getCountdownText()suffix wrapped in.bp-start-countdown. - Modified:
assets/js/app.js—getLeftDate()rewritten: 1s ticking viasetInterval, days/hours/minutes/seconds, zero-unit omission, pluralisation, and a wrapper-missing fallback. This is the file actually served (front-end$jsFileis hardcoded to.js). - Modified:
src/js/app.js— samegetLeftDate()change, to keep the gulp source in sync.app.min.jsis intentionally not regenerated (unused — the unminifiedapp.jsis enqueued). - Modified:
auctionforge.php— plugin header → 3.7.0.40. (3.7.0.39 was present in the header with no changelog entry; this release supersedes it.)
---
[3.7.0.38] — 2026-06-02
Added — Spelling Dialect "Auto" mode (match the auction house region)
The Spelling Dialect setting (Settings → Appearance) gains a third option, "Auto — match the auction house region", which becomes the new default for sites that haven't chosen one:
- During the existing auction sync (
getAllAuctions),Dialect::captureRegionFromAuctions()reads the auction house's country from the auctionaddress(trailing segment;currencyas a fallback) and stores the resolved dialect in thespelling_dialect_autooption. No API calls happen on page loads — it's a fixed, site-wide value refreshed each sync, so it's fully page-cache-safe. - Mapping: US → Catalog; every other country → Catalogue (American "Catalog" is US-only; UK/IE/AU/NZ/CA/etc. use "Catalogue").
- The admin field shows what was detected, e.g. *"Detected region: United Kingdom → Catalogue."* United States and United Kingdom remain manual hard-overrides.
Default change: new/unconfigured sites now default to Auto (was UK). Until a site's first sync resolves the region, Auto falls back to UK, so behaviour is unchanged until detection kicks in — then it self-corrects (a US house starts showing "Catalog" automatically).
Internal
- Modified:
inc/helpers/Dialect.php—automode,FALLBACK_DIALECT,countryToDialect(),captureRegionFromAuctions();DEFAULT_DIALECT→auto. - Modified:
inc/import/AuctionList.php— capture the house region during the auctions sync. - Modified:
inc/controllers/Settings.php— Auto dropdown option + detected-region note. - Modified:
auctionforge.php— plugin header → 3.7.0.38.
---
[3.7.0.37] — 2026-06-02
Fixed — Per-lot "full price VAT" now drives the VAT statement
The standalone VAT line on lot pages (
vat-and-commission→.vat-field) was keyed off the auction-house VAT rule, so it always read *"VAT: X% on item price and commission"* regardless of a lot's full-price-VAT toggle — and toggling a lot off didn't change it. It now branches on the live per-lot flag (itemInfo.item.fullPriceVat), matching the price-breakdown calc ingetFullPriceInfo():- Full-price VAT on → *"VAT: X% on item price and commission"* (VAT applied to hammer + commission)
- off (default) → *"VAT: X% on commission only"*
This only changes the wording to match the amount
getFullPriceInfo()already computes per the toggle; the VAT figure itself was already correct. The separaterenderVatField()"no VAT on commission" path is unchanged.Internal
- Modified:
templates/js-chunks/vat-and-commission.php— VAT statement branches on the per-lotfullPriceVatinstead ofhouseCommissionVatRule. - Modified:
auctionforge.php— plugin header → 3.7.0.37.
---
[3.7.0.36] — 2026-06-02
A Site Statistics release: day-of-week analysis, an opt-in weekly email report, lot-focused "top pages", and several dashboard refinements.
Added — Day-of-week tabs on the hourly charts
The Hourly Distribution (Site Statistics → Overview) and Peak Viewing Times (Lot Analytics) charts now have a day tab strip — All days · Mon · Tue … Sun — each showing that weekday's total. Selecting a day redraws the 24-hour chart consolidated across every matching date in the range, so you can see *which days and times* visitors are active. The lot-analytics data already carried
day_of_week; it was previously flattened to 24 hours and is now surfaced.Added — Scheduled weekly email report (opt-in, off by default)
A new report under Site Statistics → Settings emails a polished weekly summary: headline KPI cards (Visitors / Pageviews / Sessions with this-week-vs-last-week change), a visitor journey funnel (Visitors → Viewed a lot → Left a bid), busiest hour for each weekday, and the week's most-viewed lots. Driven by a daily cron that sends once on a configurable weekday. Supports one or several recipients (comma/semicolon/space/newline separated, validated and de-duped) and a "Send test now" button that sends to whatever is currently in the Recipient(s) box. Built table-based with inline CSS for Gmail/Outlook/Apple Mail compatibility.
Added — New vs Returning visitors, auction markers, engagement funnel
- New vs Returning visitor KPI cards on the Overview (new = authoritative
af_stats_visitors.first_seenwithin the period, joined so it can never exceed total visitors). - Auction-date markers drawn on the *Traffic Over Time* chart (dashed lines + a captioned list) so traffic can be read against sale dates.
- Engagement funnel panel: Visitors → Viewed a lot → Viewed 5+ lots → Bidders (bidders from synced
af_bid_events).
Changed — "Top pages" is now lot-focused ("Top Lots" / "Most viewed lots")
Both the dashboard Overview panel and the email now list the most-viewed individual lot pages only (
/lot-…), excluding home, catalogue listing/pagination and profile pages as low-signal. Email page titles are shown in full (no mid-word truncation) with the site-name suffix stripped; the dedicated Pages tab remains the unfiltered drill-down.Changed — Lot Analytics "Peak Viewing Times" honours the global date range
Previously hard-coded to the last 30 days; it now respects the dashboard's range picker. The Lot Analytics tab also auto-loads on first click (previously required a manual refresh).
Changed — Chart.js bundled locally
The Statistics and plugin dashboards loaded Chart.js from a CDN at two different versions (
@4and@4.4.1). Chart.js 4.4.1 is now bundled atadmin/assets/js/chart.umd.min.jsand loaded from the plugin (version-busted), so charts work offline / behind strict CSP and the version is pinned.Internal
- Added:
inc/stats/class-stats-report.php—AF_Stats_Report(weekly cron, recipient parsing, HTML builder, "send test" AJAX).admin/assets/js/chart.umd.min.js— bundled Chart.js 4.4.1. - Modified:
inc/stats/class-stats-ajax.php— hourly-by-DOW query;new_visitors,auction_events,funnelin the dashboard payload; lot-only top pages; global date range passed to peak times; weekly-email settings saved. - Modified:
inc/model/LotAnalytics.php—getPeakViewingTimes()accepts an explicit$from/$torange. - Modified:
inc/stats/class-stats-init.php— require + registerAF_Stats_Report. - Modified:
admin/view/stats/dashboard.php— day-of-week tabs, KPI cards, auction-marker plugin, funnel, Top Lots, weekly-email settings UI + test button, local Chart.js. - Modified:
admin/view/dashboard.php— local Chart.js. - Modified:
auctionforge.php— plugin header → 3.7.0.36.
---
[3.7.0.35] — 2026-06-02
Added — Regional spelling dialect ("Catalog" US / "Catalogue" UK)
A new Spelling Dialect dropdown at the top of Settings → Appearance lets each site choose how the word "catalog" is presented on the public front end:
- United States — Catalog — US spelling (the plugin's original authored wording).
- United Kingdom — Catalogue — UK spelling.
When UK is selected, every front-end occurrence is rewritten — *"Catalogue will be available soon"*, *"View catalogue"*, *"Catalogue Coming Soon"*, *"Download catalogue"*, *"Back to the catalogue"*, the lot-page Catalogue tab, and the "catalogue published" app notification.
How it works. Every user-facing catalog string already passes through the
auctionforgetext domain (__()), so the swap is implemented as a singlegettext/gettext_with_contextfilter rather than by editing template files.Dialect::toUk()rewrites whole words only, preserving case and pluralisation (Catalog→Catalogue,View catalog→View catalogue,CATALOG→CATALOGUE,Catalogs→Catalogues), and is idempotent. The filter is front-end only (is_admin()-guarded, with awp_doing_ajax()exception so front-end AJAX is still covered) and is scoped to theauctionforgedomain, so it never touches WordPress core, the theme, or other plugins. Code identifiers, option keys (catalog_layout,bids_in_catalog), CSS classes (af-catalog-sidebar,catalog-section,bp-back-to-catalog-button) and JS variables are all left intact. On US sites the filter is not registered at all, so there is zero added overhead.Internal
- Added:
inc/helpers/Dialect.php—Dialecthelper:get()/isUk()(reads thespelling_dialectoption, default UK),word()for non-translated strings, the case/plural-preservingtoUk()transform, thegettextfilter callbacks, andregister()(no-op on US sites). - Modified:
inc/coreIncludes.php— requireDialect.phpand callDialect::register()at plugin load. - Modified:
inc/controllers/Settings.php—usetheDialecthelper; add the "Regional Spelling" sub-heading and Spelling Dialect field at the top of the Appearance section, plus thefill_bidspirit_options_spelling_dialect()renderer. The value persists through the existingbidspirit_settingsoption and sanitize callback. - Modified:
auctionforge.php— plugin header → 3.7.0.35.
Note for existing sites. The default dialect is UK ("Catalogue"). Any site that upgrades without an explicit
spelling_dialectvalue will therefore display "Catalogue" on the front end. Sites that should keep US spelling must set Settings → Appearance → Spelling Dialect to United States — Catalog (stored asspelling_dialect = us). Admin labels are unaffected in either dialect.---
[3.7.0.34] — 2026-05-30
Fixed — Duplicate lot posts created by concurrent catalog syncs for the same auction
When two catalog-sync jobs for the same auction ran at the same time, every lot was imported twice and the front-end showed each lot as a pair (the second post taking a
-2slug). Observed on a live sale: 157 lots became 313 publishedlotposts.Root cause. Two BidSpirit webhooks arriving in the same second (e.g.
CATALOG_PUBLISHED+AUCTION_CATALOG_UPDATED) each enqueue acatalog_sync.SyncQueue::enqueue()deduplicates with a non-atomic *SELECT-then-INSERT* (no DB unique constraint), so two simultaneous calls both pass the "already pending?" check and both jobs land in the queue. Job *claiming* is atomic per-job, but nothing serialised two different jobs for the same auction, so two workers ran concurrently. The matcherLot::findByBSMeta()only sees committedpublish/draftposts, so during the overlapping insert windows each worker saw "no existing post" for every lot and inserted a fresh full set. Worse,findByBSMetadoesGROUP BY lotId, so the resulting duplicate pair is collapsed to a single row in every later sync — the importer never detects or repairs it, and thecount($datum) > 1"Double" guard inInsertLotsBS::init()is effectively dead code.Fix.
CatalogSyncWorker::process()andprocessDayEnded()now take a per-auction MySQL advisory lock (GET_LOCK('<prefix>af_sync_<auctionId>', 0)) around the API-fetch + post-insertion work. If another worker already holds the lock for that auction, the duplicate job logsconcurrent-lock busyand returns cleanly — marked completed, since the in-flight sync already covers the work — instead of racing. The lock is held on the worker's own DB connection and auto-releases if the process dies, so it can never deadlock the queue. This serialises all catalog and day-ended processing per auction no matter how many duplicate jobs reach the queue.Internal
- Modified:
inc/sync/CatalogSyncWorker.php— newacquireAuctionLock()/releaseAuctionLock()helpers;process()andprocessDayEnded()acquire the lock up front (skip-on-busy) and release it in afinally. - Modified:
auctionforge.php— plugin header → 3.7.0.34.
Note for existing sites. This prevents *new* duplicates; it does not remove ones already created. To find them, look for published
lotposts where two share one BidSpirit lot id (bp_meta_data.id). Keep the most-recently-synced post per lot id and set the rest topost_status = 'trash'— thelotpost type has trash disabled, so do it via SQL, and do not usedraft(whichfindByBSMetastill matches). Then recount theauction_numberterm and clear caches.---
[3.7.0.33] — 2026-05-30
Fixed —
auctionforge_sync_lotscron never scheduled (Lots push to wpdeploy silently stopped)AF_Lots_Syncuses the custombp_ten_minutelyschedule. The sync crons were registered fromAF_Stats_Init::init(), which runs at plugin-load time — *before*auctionforge.phpaddsbp_ten_minutelyto thecron_schedulesfilter further down the same file. Sowp_schedule_event(time(), 'bp_ten_minutely', 'auctionforge_sync_lots')ran against an unknown schedule, failed silently, and left the hook permanently unscheduled. The other four sync hooks use core'shourlyand were unaffected — which is why only the Lots push to wpdeploy went stale (observed: 3 days with no push while every other stream ran on time). Fixed by deferring the syncregister()calls to theinithook, by which point every custom schedule is registered. The hook self-arms on the next request after upgrade.Fixed — Safety Sync admin run reported a false "Request failed (HTTP 200)"
auctionforge_ajax_run_cron_taskreturns its result as JSON, but a stray PHP notice/deprecation emitted by a task (e.g. the Safety Sync BidSpirit delta-check on PHP 8.1) printed into the response body ahead of the JSON, so the browser couldn't parse it and the modal showed a misleading "Request failed (HTTP 200)" even though the task succeeded. The handler now buffers task output, discards it before emitting JSON, and logs any suppressed output viaSyncLogger(contextadmin) so the offending notice can still be found and fixed at the root.Added — "Clear duplicate cron events" button on Sync Status
The one-shot
AF_Cron_Cleanupmigration only runs once per site, so per-hostname duplicate events created by an older build *after* the migration had already marked itself done were never swept. NewAF_Cron_Cleanup::force_sweep()runs the same sweep on demand, exposed via a Clear Cron Dupes button in the Sync Queue toolbar (AJAXauctionforge_clear_cron_dupes). It removes legacy-args duplicates of the four per-hostname hooks plus the two orphan hooks, keeping one clean event each.Internal
- Modified:
inc/stats/class-stats-init.php— syncregister()calls wrapped inadd_action('init', …, 20). - Modified:
inc/maintenance/class-cron-cleanup.php— new publicforce_sweep()reusing the existing sweep helpers. - Modified:
auctionforge.php—auctionforge_ajax_run_cron_task()output-buffered against stray output; newauctionforge_ajax_clear_cron_dupes()handler + registration; plugin header → 3.7.0.33. - Modified:
admin/view/sync-status.php— added the "Clear Cron Dupes" button and handler.
Note: with all scheduling now arg-free (3.7.0.28+), the duplicates do not regenerate once swept; a canonical-hostname redirect (www ↔ apex) at the web-server level remains good hygiene but is no longer required to keep the cron table clean.
---
[3.7.0.32] — 2026-05-30
Fixed — V3 sync worker never spawned on some hosts (silent import stall)
On some sites (confirmed on a LiteSpeed/cPanel install) the V3 queue worker never actually started, so catalog lots were staged into
import_historybut never converted to WordPress posts —postId IS NULL,syncFinished = 0,syncTimes = 0, and no errors logged. Clearing the queue and reprocessing re-stalled immediately.Root cause.
SyncQueue::spawnWorker()starts the worker by POSTing the REST route/wp-json/auctionforge/v1/sync-worker. It tried an HTTP loopback tohttp://127.0.0.1/with the site'sHost:header first, and the success test wasif (!is_wp_error($result))— it never inspected the HTTP status code. On hosts where127.0.0.1+Hostheader resolves to the wrong vhost (or the route is blocked), that request returns 404/403, which is not aWP_Error. The old code therefore treated the 404 as a successful spawn, loggedSpawned … (HTTP 404), returnedtrue, and never reached the HTTPS fallback. The worker never ran; nothing drained the queue or the staging table. Sites whose127.0.0.1loopback resolves correctly returned 200 and were unaffected — which is why this was host-specific.Fix.
spawnWorker()now reads the response code and only treats 2xx as a successful spawn. On any non-2xx (or aWP_Error) from the loopback it logs a warning and falls through to therest_url()HTTPS path, which is likewise status-checked. A new constant short-circuits the doomed attempt on known-bad hosts:```php
define('AUCTIONFORGE_WORKER_SKIP_LOOPBACK', true); // wp-config.php — skip 127.0.0.1, go straight to HTTPS
```
Added — Automatic staging recovery on the safety cron
The hourly
auctionforge_sync_safetycron previously only spawned a worker when the queue had pending jobs. Lots that were staged intoimport_historybut left unconverted (e.g. after a worker died, or while the spawn bug above was in effect) had no queue job attached, so nothing ever picked them up. The safety cron now also checksSyncQueue::stagingBacklogCount()and, when it is non-zero, kicks the staging processor (non-blocking) to convert the backlog into posts — making the pipeline self-healing without manual intervention.Added — "Process Staging" button on Sync Status
A new Process Staging button in the Sync Queue toolbar (Lots → Sync Status) runs
InsertLotsBSover the staging table on demand — for when lots are staged but not appearing on the site. It fires non-blocking and switches the Live Console into fast-poll so the operator can watch posts being created in batches of 50.Internal
- Modified:
inc/sync/SyncQueue.php—spawnWorker()rewritten with HTTP-status-aware fallback +AUCTIONFORGE_WORKER_SKIP_LOOPBACKsupport;triggerStagingProcessor()gained a$blockingparameter (non-blocking fire-and-forget mode for cron/admin); newstagingBacklogCount()helper (countsimport_historyrows withsyncFinished = 0). - Modified:
auctionforge.php—auctionforge_sync_safety_action()now drains orphaned staging via the non-blocking processor when a backlog exists; newprocess_stagingcase in theauctionforge_sync_enqueueAJAX handler; plugin header bumped to 3.7.0.32. - Modified:
admin/view/sync-status.php— added the "Process Staging" button and its click handler.
---
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.7.0.43 Stable Jun 22, 2026Download v3.7.0.43📦 9.37 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.0.43] — 2026-06-22
Fixed — Submission "Need help?" contact was hardcoded (one tenant's details on every site)
The vehicle-submission help panel (
templates/js-chunks/submissions.php) rendered a single hardcoded email and phone, so every AuctionForge site displayed the same contact regardless of which auction house it was — e.g. one tenant'sinfo@absoluteclearance.co.uk/01536 674804showed on unrelated sites.The panel now resolves the contact dynamically, in priority order:
apply_filters('auctionforge_help_email', '')/apply_filters('auctionforge_help_phone', '')— explicit per-site override (any source, e.g. a theme/global/Elementor setting);- otherwise the SEO & AEO Optimizer LocalBusiness details stored in the
seo_aeo_optionsoption (local_email/local_phone); - if neither yields a value the corresponding row is omitted, and the whole "Need help?" block is hidden when no contact is available — so no placeholder or wrong-tenant detail is ever shown.
Internal
- Modified:
templates/js-chunks/submissions.php— replaced the two hardcoded<p>contact values with filtered,esc_html-escaped values resolved fromseo_aeo_options, each row guarded by anif. - Modified:
auctionforge.php— plugin header → 3.7.0.43. - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
-
v3.7.0.42 Stable Jun 22, 2026Download v3.7.0.42📦 9.38 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.0.42] — 2026-06-22
Fixed — Submission templates emitted extra
<h1>s and placeholder content site-wideThe vehicle-submission view (
templates/js-chunks/submissions.php) is rendered onwp_footeron every page, so its<script type="text/template">chunks shipped into the source of all pages. Two of those chunks used<h1>headings ("Submit your vehicle for auction" and "Detail received successfully"), meaning every page's HTML source contained three<h1>elements. While these live inside inert<script>templates (so a DOM-rendering crawler like Googlebot still resolves a single true H1), raw-source SEO tooling and some AI crawlers flag the duplicates, and the templates also shipped vendor placeholder text.- Both template headings demoted from
<h1>to<h2>— a single H1 per page in source. - Replaced Lorem-ipsum placeholder copy in the intro and progress panels with real submission guidance.
- Replaced the dummy "Need help?" contact details (
support@domain.com/+3265464325) with the live business contact (info@absoluteclearance.co.uk/01536 674804).
No functional/JS changes — template IDs and structure are unchanged.
---
- Both template headings demoted from
-
v3.7.0.41 Stable Jun 19, 2026Download v3.7.0.41📦 9.38 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.0.41] — 2026-06-19
Fixed — Duplicate auction terms (auction shown twice, catalogue split)
A newly published auction could be created twice in WordPress as two identical
auction_numberterms, so the auction rendered twice on the listing and its catalogue was split across the two copies (e.g. 145 lots stored as 144 + 1). The lot posts themselves stayed unique — this was a duplicate *auction*, not duplicate *lots*.Root cause: auction terms are only created in
Auction::save()viawp_insert_category, which inserts whenever the caller's lookup returned no term. That lookup (findAuctionByIdWithLang()→get_terms()meta_query) can return nothing for a brand-new auction when two syncs run concurrently — for example afull_sync_*bulk run (viaBulkSyncWorker/AuctionList) overlapping a catalogue webhook. Both see "no term" and both insert one. The only fallback,findAuctionByNumber(), looks up by the auction *number* slug and never matched the real term slug, so it could not catch the miss. This path is not covered by the existingCatalogSyncWorkerper-auction lock, which serialises catalogue syncs to prevent duplicate *lot posts* but does not guard *term* creation.Auction::save()now, before inserting a term:- acquires a per-auction MySQL advisory lock (
GET_LOCK('<prefix>af_term_<auctionId>', 10)) so concurrent creators serialise — the loser then finds the just-created term and updates it instead of inserting a second one; and - performs an authoritative, cache-bypassing lookup by
auction_id(directtermmeta⋈term_taxonomyquery) and switches to update-in-place when a term already exists.
Polylang-aware: under full multilingual support it only reuses a term in the requested language, so legitimate per-language terms are still created.
Internal
- Modified:
inc/model/Auction.php—save()gains a pre-insert dedup guard; new private helpersfindTermIdByAuctionId(),acquireTermLock(),releaseTermLock().InstanceDBwraps the global$wpdb, so the advisory lock and thewp_insert_categoryinsert share a single DB session (the same property that makes the existingCatalogSyncWorkerlock effective). - No database schema change (
AUCTIONFORGE_DB_VERSIONunchanged at 25).
---
- acquires a per-auction MySQL advisory lock (
-
v3.7.0.40 Stable Jun 14, 2026Download v3.7.0.40📦 9.22 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.0.40] — 2026-06-14
Fixed — Lot "begin to close" countdown now shows minutes (and seconds in the final minute)
The lot-page countdown (
#auctionStartTimer, the *"Auction will begin to close in …"* line) only ever rendered whole days and whole hours, and computed once on page load. With less than an hour to go it therefore read "0 days and 0 hours" — e.g. a lot closing at 20:00 looked like a dead/expired timer from 19:00 onwards, even though ~46 minutes remained. The time maths was correct; the widget simply had no sub-hour granularity and never ticked.It now updates every second and shows the largest meaningful units, dropping leading zero units:
44 minutes•3 hours and 20 minutes•1 day, 2 hours and 5 minutes- In the final minute it counts the seconds down (
40 seconds→1 second) so it never sits on a flat "0". - Once the close time passes (
result < 0) the timer hides itself, as before.
getCountdownText()now wraps the suffix in a.bp-start-countdownspan so the script can rewrite the whole phrase; if that wrapper is ever absent (e.g. a stale page-cached markup),getLeftDate()falls back to the original days+hours behaviour rather than erroring — important becauseapp.jsalso carries the live bidding logic.Internal
- Modified:
inc/controllers/Settings.php—getCountdownText()suffix wrapped in.bp-start-countdown. - Modified:
assets/js/app.js—getLeftDate()rewritten: 1s ticking viasetInterval, days/hours/minutes/seconds, zero-unit omission, pluralisation, and a wrapper-missing fallback. This is the file actually served (front-end$jsFileis hardcoded to.js). - Modified:
src/js/app.js— samegetLeftDate()change, to keep the gulp source in sync.app.min.jsis intentionally not regenerated (unused — the unminifiedapp.jsis enqueued). - Modified:
auctionforge.php— plugin header → 3.7.0.40. (3.7.0.39 was present in the header with no changelog entry; this release supersedes it.)
---
-
v3.7.0.39 Stable Jun 10, 2026Download v3.7.0.39📦 9.11 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.0.38] — 2026-06-02
Added — Spelling Dialect "Auto" mode (match the auction house region)
The Spelling Dialect setting (Settings → Appearance) gains a third option, "Auto — match the auction house region", which becomes the new default for sites that haven't chosen one:
- During the existing auction sync (
getAllAuctions),Dialect::captureRegionFromAuctions()reads the auction house's country from the auctionaddress(trailing segment;currencyas a fallback) and stores the resolved dialect in thespelling_dialect_autooption. No API calls happen on page loads — it's a fixed, site-wide value refreshed each sync, so it's fully page-cache-safe. - Mapping: US → Catalog; every other country → Catalogue (American "Catalog" is US-only; UK/IE/AU/NZ/CA/etc. use "Catalogue").
- The admin field shows what was detected, e.g. *"Detected region: United Kingdom → Catalogue."* United States and United Kingdom remain manual hard-overrides.
Default change: new/unconfigured sites now default to Auto (was UK). Until a site's first sync resolves the region, Auto falls back to UK, so behaviour is unchanged until detection kicks in — then it self-corrects (a US house starts showing "Catalog" automatically).
Internal
- Modified:
inc/helpers/Dialect.php—automode,FALLBACK_DIALECT,countryToDialect(),captureRegionFromAuctions();DEFAULT_DIALECT→auto. - Modified:
inc/import/AuctionList.php— capture the house region during the auctions sync. - Modified:
inc/controllers/Settings.php— Auto dropdown option + detected-region note. - Modified:
auctionforge.php— plugin header → 3.7.0.38.
---
[3.7.0.37] — 2026-06-02
Fixed — Per-lot "full price VAT" now drives the VAT statement
The standalone VAT line on lot pages (
vat-and-commission→.vat-field) was keyed off the auction-house VAT rule, so it always read *"VAT: X% on item price and commission"* regardless of a lot's full-price-VAT toggle — and toggling a lot off didn't change it. It now branches on the live per-lot flag (itemInfo.item.fullPriceVat), matching the price-breakdown calc ingetFullPriceInfo():- Full-price VAT on → *"VAT: X% on item price and commission"* (VAT applied to hammer + commission)
- off (default) → *"VAT: X% on commission only"*
This only changes the wording to match the amount
getFullPriceInfo()already computes per the toggle; the VAT figure itself was already correct. The separaterenderVatField()"no VAT on commission" path is unchanged.Internal
- Modified:
templates/js-chunks/vat-and-commission.php— VAT statement branches on the per-lotfullPriceVatinstead ofhouseCommissionVatRule. - Modified:
auctionforge.php— plugin header → 3.7.0.37.
---
[3.7.0.36] — 2026-06-02
A Site Statistics release: day-of-week analysis, an opt-in weekly email report, lot-focused "top pages", and several dashboard refinements.
Added — Day-of-week tabs on the hourly charts
The Hourly Distribution (Site Statistics → Overview) and Peak Viewing Times (Lot Analytics) charts now have a day tab strip — All days · Mon · Tue … Sun — each showing that weekday's total. Selecting a day redraws the 24-hour chart consolidated across every matching date in the range, so you can see *which days and times* visitors are active. The lot-analytics data already carried
day_of_week; it was previously flattened to 24 hours and is now surfaced.Added — Scheduled weekly email report (opt-in, off by default)
A new report under Site Statistics → Settings emails a polished weekly summary: headline KPI cards (Visitors / Pageviews / Sessions with this-week-vs-last-week change), a visitor journey funnel (Visitors → Viewed a lot → Left a bid), busiest hour for each weekday, and the week's most-viewed lots. Driven by a daily cron that sends once on a configurable weekday. Supports one or several recipients (comma/semicolon/space/newline separated, validated and de-duped) and a "Send test now" button that sends to whatever is currently in the Recipient(s) box. Built table-based with inline CSS for Gmail/Outlook/Apple Mail compatibility.
Added — New vs Returning visitors, auction markers, engagement funnel
- New vs Returning visitor KPI cards on the Overview (new = authoritative
af_stats_visitors.first_seenwithin the period, joined so it can never exceed total visitors). - Auction-date markers drawn on the *Traffic Over Time* chart (dashed lines + a captioned list) so traffic can be read against sale dates.
- Engagement funnel panel: Visitors → Viewed a lot → Viewed 5+ lots → Bidders (bidders from synced
af_bid_events).
Changed — "Top pages" is now lot-focused ("Top Lots" / "Most viewed lots")
Both the dashboard Overview panel and the email now list the most-viewed individual lot pages only (
/lot-…), excluding home, catalogue listing/pagination and profile pages as low-signal. Email page titles are shown in full (no mid-word truncation) with the site-name suffix stripped; the dedicated Pages tab remains the unfiltered drill-down.Changed — Lot Analytics "Peak Viewing Times" honours the global date range
Previously hard-coded to the last 30 days; it now respects the dashboard's range picker. The Lot Analytics tab also auto-loads on first click (previously required a manual refresh).
Changed — Chart.js bundled locally
The Statistics and plugin dashboards loaded Chart.js from a CDN at two different versions (
@4and@4.4.1). Chart.js 4.4.1 is now bundled atadmin/assets/js/chart.umd.min.jsand loaded from the plugin (version-busted), so charts work offline / behind strict CSP and the version is pinned.Internal
- Added:
inc/stats/class-stats-report.php—AF_Stats_Report(weekly cron, recipient parsing, HTML builder, "send test" AJAX).admin/assets/js/chart.umd.min.js— bundled Chart.js 4.4.1. - Modified:
inc/stats/class-stats-ajax.php— hourly-by-DOW query;new_visitors,auction_events,funnelin the dashboard payload; lot-only top pages; global date range passed to peak times; weekly-email settings saved. - Modified:
inc/model/LotAnalytics.php—getPeakViewingTimes()accepts an explicit$from/$torange. - Modified:
inc/stats/class-stats-init.php— require + registerAF_Stats_Report. - Modified:
admin/view/stats/dashboard.php— day-of-week tabs, KPI cards, auction-marker plugin, funnel, Top Lots, weekly-email settings UI + test button, local Chart.js. - Modified:
admin/view/dashboard.php— local Chart.js. - Modified:
auctionforge.php— plugin header → 3.7.0.36.
---
[3.7.0.35] — 2026-06-02
Added — Regional spelling dialect ("Catalog" US / "Catalogue" UK)
A new Spelling Dialect dropdown at the top of Settings → Appearance lets each site choose how the word "catalog" is presented on the public front end:
- United States — Catalog — US spelling (the plugin's original authored wording).
- United Kingdom — Catalogue — UK spelling.
When UK is selected, every front-end occurrence is rewritten — *"Catalogue will be available soon"*, *"View catalogue"*, *"Catalogue Coming Soon"*, *"Download catalogue"*, *"Back to the catalogue"*, the lot-page Catalogue tab, and the "catalogue published" app notification.
How it works. Every user-facing catalog string already passes through the
auctionforgetext domain (__()), so the swap is implemented as a singlegettext/gettext_with_contextfilter rather than by editing template files.Dialect::toUk()rewrites whole words only, preserving case and pluralisation (Catalog→Catalogue,View catalog→View catalogue,CATALOG→CATALOGUE,Catalogs→Catalogues), and is idempotent. The filter is front-end only (is_admin()-guarded, with awp_doing_ajax()exception so front-end AJAX is still covered) and is scoped to theauctionforgedomain, so it never touches WordPress core, the theme, or other plugins. Code identifiers, option keys (catalog_layout,bids_in_catalog), CSS classes (af-catalog-sidebar,catalog-section,bp-back-to-catalog-button) and JS variables are all left intact. On US sites the filter is not registered at all, so there is zero added overhead.Internal
- Added:
inc/helpers/Dialect.php—Dialecthelper:get()/isUk()(reads thespelling_dialectoption, default UK),word()for non-translated strings, the case/plural-preservingtoUk()transform, thegettextfilter callbacks, andregister()(no-op on US sites). - Modified:
inc/coreIncludes.php— requireDialect.phpand callDialect::register()at plugin load. - Modified:
inc/controllers/Settings.php—usetheDialecthelper; add the "Regional Spelling" sub-heading and Spelling Dialect field at the top of the Appearance section, plus thefill_bidspirit_options_spelling_dialect()renderer. The value persists through the existingbidspirit_settingsoption and sanitize callback. - Modified:
auctionforge.php— plugin header → 3.7.0.35.
Note for existing sites. The default dialect is UK ("Catalogue"). Any site that upgrades without an explicit
spelling_dialectvalue will therefore display "Catalogue" on the front end. Sites that should keep US spelling must set Settings → Appearance → Spelling Dialect to United States — Catalog (stored asspelling_dialect = us). Admin labels are unaffected in either dialect.---
[3.7.0.34] — 2026-05-30
Fixed — Duplicate lot posts created by concurrent catalog syncs for the same auction
When two catalog-sync jobs for the same auction ran at the same time, every lot was imported twice and the front-end showed each lot as a pair (the second post taking a
-2slug). Observed on a live sale: 157 lots became 313 publishedlotposts.Root cause. Two BidSpirit webhooks arriving in the same second (e.g.
CATALOG_PUBLISHED+AUCTION_CATALOG_UPDATED) each enqueue acatalog_sync.SyncQueue::enqueue()deduplicates with a non-atomic *SELECT-then-INSERT* (no DB unique constraint), so two simultaneous calls both pass the "already pending?" check and both jobs land in the queue. Job *claiming* is atomic per-job, but nothing serialised two different jobs for the same auction, so two workers ran concurrently. The matcherLot::findByBSMeta()only sees committedpublish/draftposts, so during the overlapping insert windows each worker saw "no existing post" for every lot and inserted a fresh full set. Worse,findByBSMetadoesGROUP BY lotId, so the resulting duplicate pair is collapsed to a single row in every later sync — the importer never detects or repairs it, and thecount($datum) > 1"Double" guard inInsertLotsBS::init()is effectively dead code.Fix.
CatalogSyncWorker::process()andprocessDayEnded()now take a per-auction MySQL advisory lock (GET_LOCK('<prefix>af_sync_<auctionId>', 0)) around the API-fetch + post-insertion work. If another worker already holds the lock for that auction, the duplicate job logsconcurrent-lock busyand returns cleanly — marked completed, since the in-flight sync already covers the work — instead of racing. The lock is held on the worker's own DB connection and auto-releases if the process dies, so it can never deadlock the queue. This serialises all catalog and day-ended processing per auction no matter how many duplicate jobs reach the queue.Internal
- Modified:
inc/sync/CatalogSyncWorker.php— newacquireAuctionLock()/releaseAuctionLock()helpers;process()andprocessDayEnded()acquire the lock up front (skip-on-busy) and release it in afinally. - Modified:
auctionforge.php— plugin header → 3.7.0.34.
Note for existing sites. This prevents *new* duplicates; it does not remove ones already created. To find them, look for published
lotposts where two share one BidSpirit lot id (bp_meta_data.id). Keep the most-recently-synced post per lot id and set the rest topost_status = 'trash'— thelotpost type has trash disabled, so do it via SQL, and do not usedraft(whichfindByBSMetastill matches). Then recount theauction_numberterm and clear caches.---
[3.7.0.33] — 2026-05-30
Fixed —
auctionforge_sync_lotscron never scheduled (Lots push to wpdeploy silently stopped)AF_Lots_Syncuses the custombp_ten_minutelyschedule. The sync crons were registered fromAF_Stats_Init::init(), which runs at plugin-load time — *before*auctionforge.phpaddsbp_ten_minutelyto thecron_schedulesfilter further down the same file. Sowp_schedule_event(time(), 'bp_ten_minutely', 'auctionforge_sync_lots')ran against an unknown schedule, failed silently, and left the hook permanently unscheduled. The other four sync hooks use core'shourlyand were unaffected — which is why only the Lots push to wpdeploy went stale (observed: 3 days with no push while every other stream ran on time). Fixed by deferring the syncregister()calls to theinithook, by which point every custom schedule is registered. The hook self-arms on the next request after upgrade.Fixed — Safety Sync admin run reported a false "Request failed (HTTP 200)"
auctionforge_ajax_run_cron_taskreturns its result as JSON, but a stray PHP notice/deprecation emitted by a task (e.g. the Safety Sync BidSpirit delta-check on PHP 8.1) printed into the response body ahead of the JSON, so the browser couldn't parse it and the modal showed a misleading "Request failed (HTTP 200)" even though the task succeeded. The handler now buffers task output, discards it before emitting JSON, and logs any suppressed output viaSyncLogger(contextadmin) so the offending notice can still be found and fixed at the root.Added — "Clear duplicate cron events" button on Sync Status
The one-shot
AF_Cron_Cleanupmigration only runs once per site, so per-hostname duplicate events created by an older build *after* the migration had already marked itself done were never swept. NewAF_Cron_Cleanup::force_sweep()runs the same sweep on demand, exposed via a Clear Cron Dupes button in the Sync Queue toolbar (AJAXauctionforge_clear_cron_dupes). It removes legacy-args duplicates of the four per-hostname hooks plus the two orphan hooks, keeping one clean event each.Internal
- Modified:
inc/stats/class-stats-init.php— syncregister()calls wrapped inadd_action('init', …, 20). - Modified:
inc/maintenance/class-cron-cleanup.php— new publicforce_sweep()reusing the existing sweep helpers. - Modified:
auctionforge.php—auctionforge_ajax_run_cron_task()output-buffered against stray output; newauctionforge_ajax_clear_cron_dupes()handler + registration; plugin header → 3.7.0.33. - Modified:
admin/view/sync-status.php— added the "Clear Cron Dupes" button and handler.
Note: with all scheduling now arg-free (3.7.0.28+), the duplicates do not regenerate once swept; a canonical-hostname redirect (www ↔ apex) at the web-server level remains good hygiene but is no longer required to keep the cron table clean.
---
[3.7.0.32] — 2026-05-30
Fixed — V3 sync worker never spawned on some hosts (silent import stall)
On some sites (confirmed on a LiteSpeed/cPanel install) the V3 queue worker never actually started, so catalog lots were staged into
import_historybut never converted to WordPress posts —postId IS NULL,syncFinished = 0,syncTimes = 0, and no errors logged. Clearing the queue and reprocessing re-stalled immediately.Root cause.
SyncQueue::spawnWorker()starts the worker by POSTing the REST route/wp-json/auctionforge/v1/sync-worker. It tried an HTTP loopback tohttp://127.0.0.1/with the site'sHost:header first, and the success test wasif (!is_wp_error($result))— it never inspected the HTTP status code. On hosts where127.0.0.1+Hostheader resolves to the wrong vhost (or the route is blocked), that request returns 404/403, which is not aWP_Error. The old code therefore treated the 404 as a successful spawn, loggedSpawned … (HTTP 404), returnedtrue, and never reached the HTTPS fallback. The worker never ran; nothing drained the queue or the staging table. Sites whose127.0.0.1loopback resolves correctly returned 200 and were unaffected — which is why this was host-specific.Fix.
spawnWorker()now reads the response code and only treats 2xx as a successful spawn. On any non-2xx (or aWP_Error) from the loopback it logs a warning and falls through to therest_url()HTTPS path, which is likewise status-checked. A new constant short-circuits the doomed attempt on known-bad hosts:```php
define('AUCTIONFORGE_WORKER_SKIP_LOOPBACK', true); // wp-config.php — skip 127.0.0.1, go straight to HTTPS
```
Added — Automatic staging recovery on the safety cron
The hourly
auctionforge_sync_safetycron previously only spawned a worker when the queue had pending jobs. Lots that were staged intoimport_historybut left unconverted (e.g. after a worker died, or while the spawn bug above was in effect) had no queue job attached, so nothing ever picked them up. The safety cron now also checksSyncQueue::stagingBacklogCount()and, when it is non-zero, kicks the staging processor (non-blocking) to convert the backlog into posts — making the pipeline self-healing without manual intervention.Added — "Process Staging" button on Sync Status
A new Process Staging button in the Sync Queue toolbar (Lots → Sync Status) runs
InsertLotsBSover the staging table on demand — for when lots are staged but not appearing on the site. It fires non-blocking and switches the Live Console into fast-poll so the operator can watch posts being created in batches of 50.Internal
- Modified:
inc/sync/SyncQueue.php—spawnWorker()rewritten with HTTP-status-aware fallback +AUCTIONFORGE_WORKER_SKIP_LOOPBACKsupport;triggerStagingProcessor()gained a$blockingparameter (non-blocking fire-and-forget mode for cron/admin); newstagingBacklogCount()helper (countsimport_historyrows withsyncFinished = 0). - Modified:
auctionforge.php—auctionforge_sync_safety_action()now drains orphaned staging via the non-blocking processor when a backlog exists; newprocess_stagingcase in theauctionforge_sync_enqueueAJAX handler; plugin header bumped to 3.7.0.32. - Modified:
admin/view/sync-status.php— added the "Process Staging" button and its click handler.
---
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- During the existing auction sync (
-
v3.7.0.38 Stable Jun 04, 2026Download v3.7.0.38📦 9.21 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.0.38] — 2026-06-02
Added — Spelling Dialect "Auto" mode (match the auction house region)
The Spelling Dialect setting (Settings → Appearance) gains a third option, "Auto — match the auction house region", which becomes the new default for sites that haven't chosen one:
- During the existing auction sync (
getAllAuctions),Dialect::captureRegionFromAuctions()reads the auction house's country from the auctionaddress(trailing segment;currencyas a fallback) and stores the resolved dialect in thespelling_dialect_autooption. No API calls happen on page loads — it's a fixed, site-wide value refreshed each sync, so it's fully page-cache-safe. - Mapping: US → Catalog; every other country → Catalogue (American "Catalog" is US-only; UK/IE/AU/NZ/CA/etc. use "Catalogue").
- The admin field shows what was detected, e.g. *"Detected region: United Kingdom → Catalogue."* United States and United Kingdom remain manual hard-overrides.
Default change: new/unconfigured sites now default to Auto (was UK). Until a site's first sync resolves the region, Auto falls back to UK, so behaviour is unchanged until detection kicks in — then it self-corrects (a US house starts showing "Catalog" automatically).
Internal
- Modified:
inc/helpers/Dialect.php—automode,FALLBACK_DIALECT,countryToDialect(),captureRegionFromAuctions();DEFAULT_DIALECT→auto. - Modified:
inc/import/AuctionList.php— capture the house region during the auctions sync. - Modified:
inc/controllers/Settings.php— Auto dropdown option + detected-region note. - Modified:
auctionforge.php— plugin header → 3.7.0.38.
---
- During the existing auction sync (
-
v3.7.0.37 Stable Jun 02, 2026Download v3.7.0.37📦 9.21 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.0.37] — 2026-06-02
Fixed — Per-lot "full price VAT" now drives the VAT statement
The standalone VAT line on lot pages (
vat-and-commission→.vat-field) was keyed off the auction-house VAT rule, so it always read *"VAT: X% on item price and commission"* regardless of a lot's full-price-VAT toggle — and toggling a lot off didn't change it. It now branches on the live per-lot flag (itemInfo.item.fullPriceVat), matching the price-breakdown calc ingetFullPriceInfo():- Full-price VAT on → *"VAT: X% on item price and commission"* (VAT applied to hammer + commission)
- off (default) → *"VAT: X% on commission only"*
This only changes the wording to match the amount
getFullPriceInfo()already computes per the toggle; the VAT figure itself was already correct. The separaterenderVatField()"no VAT on commission" path is unchanged.Internal
- Modified:
templates/js-chunks/vat-and-commission.php— VAT statement branches on the per-lotfullPriceVatinstead ofhouseCommissionVatRule. - Modified:
auctionforge.php— plugin header → 3.7.0.37.
---
-
v3.7.0.36 Stable Jun 02, 2026Download v3.7.0.36📦 9.21 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.0.36] — 2026-06-02
A Site Statistics release: day-of-week analysis, an opt-in weekly email report, lot-focused "top pages", and several dashboard refinements.
Added — Day-of-week tabs on the hourly charts
The Hourly Distribution (Site Statistics → Overview) and Peak Viewing Times (Lot Analytics) charts now have a day tab strip — All days · Mon · Tue … Sun — each showing that weekday's total. Selecting a day redraws the 24-hour chart consolidated across every matching date in the range, so you can see *which days and times* visitors are active. The lot-analytics data already carried
day_of_week; it was previously flattened to 24 hours and is now surfaced.Added — Scheduled weekly email report (opt-in, off by default)
A new report under Site Statistics → Settings emails a polished weekly summary: headline KPI cards (Visitors / Pageviews / Sessions with this-week-vs-last-week change), a visitor journey funnel (Visitors → Viewed a lot → Left a bid), busiest hour for each weekday, and the week's most-viewed lots. Driven by a daily cron that sends once on a configurable weekday. Supports one or several recipients (comma/semicolon/space/newline separated, validated and de-duped) and a "Send test now" button that sends to whatever is currently in the Recipient(s) box. Built table-based with inline CSS for Gmail/Outlook/Apple Mail compatibility.
Added — New vs Returning visitors, auction markers, engagement funnel
- New vs Returning visitor KPI cards on the Overview (new = authoritative
af_stats_visitors.first_seenwithin the period, joined so it can never exceed total visitors). - Auction-date markers drawn on the *Traffic Over Time* chart (dashed lines + a captioned list) so traffic can be read against sale dates.
- Engagement funnel panel: Visitors → Viewed a lot → Viewed 5+ lots → Bidders (bidders from synced
af_bid_events).
Changed — "Top pages" is now lot-focused ("Top Lots" / "Most viewed lots")
Both the dashboard Overview panel and the email now list the most-viewed individual lot pages only (
/lot-…), excluding home, catalogue listing/pagination and profile pages as low-signal. Email page titles are shown in full (no mid-word truncation) with the site-name suffix stripped; the dedicated Pages tab remains the unfiltered drill-down.Changed — Lot Analytics "Peak Viewing Times" honours the global date range
Previously hard-coded to the last 30 days; it now respects the dashboard's range picker. The Lot Analytics tab also auto-loads on first click (previously required a manual refresh).
Changed — Chart.js bundled locally
The Statistics and plugin dashboards loaded Chart.js from a CDN at two different versions (
@4and@4.4.1). Chart.js 4.4.1 is now bundled atadmin/assets/js/chart.umd.min.jsand loaded from the plugin (version-busted), so charts work offline / behind strict CSP and the version is pinned.Internal
- Added:
inc/stats/class-stats-report.php—AF_Stats_Report(weekly cron, recipient parsing, HTML builder, "send test" AJAX).admin/assets/js/chart.umd.min.js— bundled Chart.js 4.4.1. - Modified:
inc/stats/class-stats-ajax.php— hourly-by-DOW query;new_visitors,auction_events,funnelin the dashboard payload; lot-only top pages; global date range passed to peak times; weekly-email settings saved. - Modified:
inc/model/LotAnalytics.php—getPeakViewingTimes()accepts an explicit$from/$torange. - Modified:
inc/stats/class-stats-init.php— require + registerAF_Stats_Report. - Modified:
admin/view/stats/dashboard.php— day-of-week tabs, KPI cards, auction-marker plugin, funnel, Top Lots, weekly-email settings UI + test button, local Chart.js. - Modified:
admin/view/dashboard.php— local Chart.js. - Modified:
auctionforge.php— plugin header → 3.7.0.36.
---
- New vs Returning visitor KPI cards on the Overview (new = authoritative
-
v3.7.0.35 Stable Jun 02, 2026Download v3.7.0.35📦 9.13 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.0.35] — 2026-06-02
Added — Regional spelling dialect ("Catalog" US / "Catalogue" UK)
A new Spelling Dialect dropdown at the top of Settings → Appearance lets each site choose how the word "catalog" is presented on the public front end:
- United States — Catalog — US spelling (the plugin's original authored wording).
- United Kingdom — Catalogue — UK spelling.
When UK is selected, every front-end occurrence is rewritten — *"Catalogue will be available soon"*, *"View catalogue"*, *"Catalogue Coming Soon"*, *"Download catalogue"*, *"Back to the catalogue"*, the lot-page Catalogue tab, and the "catalogue published" app notification.
How it works. Every user-facing catalog string already passes through the
auctionforgetext domain (__()), so the swap is implemented as a singlegettext/gettext_with_contextfilter rather than by editing template files.Dialect::toUk()rewrites whole words only, preserving case and pluralisation (Catalog→Catalogue,View catalog→View catalogue,CATALOG→CATALOGUE,Catalogs→Catalogues), and is idempotent. The filter is front-end only (is_admin()-guarded, with awp_doing_ajax()exception so front-end AJAX is still covered) and is scoped to theauctionforgedomain, so it never touches WordPress core, the theme, or other plugins. Code identifiers, option keys (catalog_layout,bids_in_catalog), CSS classes (af-catalog-sidebar,catalog-section,bp-back-to-catalog-button) and JS variables are all left intact. On US sites the filter is not registered at all, so there is zero added overhead.Internal
- Added:
inc/helpers/Dialect.php—Dialecthelper:get()/isUk()(reads thespelling_dialectoption, default UK),word()for non-translated strings, the case/plural-preservingtoUk()transform, thegettextfilter callbacks, andregister()(no-op on US sites). - Modified:
inc/coreIncludes.php— requireDialect.phpand callDialect::register()at plugin load. - Modified:
inc/controllers/Settings.php—usetheDialecthelper; add the "Regional Spelling" sub-heading and Spelling Dialect field at the top of the Appearance section, plus thefill_bidspirit_options_spelling_dialect()renderer. The value persists through the existingbidspirit_settingsoption and sanitize callback. - Modified:
auctionforge.php— plugin header → 3.7.0.35.
Note for existing sites. The default dialect is UK ("Catalogue"). Any site that upgrades without an explicit
spelling_dialectvalue will therefore display "Catalogue" on the front end. Sites that should keep US spelling must set Settings → Appearance → Spelling Dialect to United States — Catalog (stored asspelling_dialect = us). Admin labels are unaffected in either dialect.---
-
v3.7.0.34 Stable May 30, 2026Download v3.7.0.34📦 9.28 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.0.34] — 2026-05-30
Fixed — Duplicate lot posts created by concurrent catalog syncs for the same auction
When two catalog-sync jobs for the same auction ran at the same time, every lot was imported twice and the front-end showed each lot as a pair (the second post taking a
-2slug). Observed on a live sale: 157 lots became 313 publishedlotposts.Root cause. Two BidSpirit webhooks arriving in the same second (e.g.
CATALOG_PUBLISHED+AUCTION_CATALOG_UPDATED) each enqueue acatalog_sync.SyncQueue::enqueue()deduplicates with a non-atomic *SELECT-then-INSERT* (no DB unique constraint), so two simultaneous calls both pass the "already pending?" check and both jobs land in the queue. Job *claiming* is atomic per-job, but nothing serialised two different jobs for the same auction, so two workers ran concurrently. The matcherLot::findByBSMeta()only sees committedpublish/draftposts, so during the overlapping insert windows each worker saw "no existing post" for every lot and inserted a fresh full set. Worse,findByBSMetadoesGROUP BY lotId, so the resulting duplicate pair is collapsed to a single row in every later sync — the importer never detects or repairs it, and thecount($datum) > 1"Double" guard inInsertLotsBS::init()is effectively dead code.Fix.
CatalogSyncWorker::process()andprocessDayEnded()now take a per-auction MySQL advisory lock (GET_LOCK('<prefix>af_sync_<auctionId>', 0)) around the API-fetch + post-insertion work. If another worker already holds the lock for that auction, the duplicate job logsconcurrent-lock busyand returns cleanly — marked completed, since the in-flight sync already covers the work — instead of racing. The lock is held on the worker's own DB connection and auto-releases if the process dies, so it can never deadlock the queue. This serialises all catalog and day-ended processing per auction no matter how many duplicate jobs reach the queue.Internal
- Modified:
inc/sync/CatalogSyncWorker.php— newacquireAuctionLock()/releaseAuctionLock()helpers;process()andprocessDayEnded()acquire the lock up front (skip-on-busy) and release it in afinally. - Modified:
auctionforge.php— plugin header → 3.7.0.34.
Note for existing sites. This prevents *new* duplicates; it does not remove ones already created. To find them, look for published
lotposts where two share one BidSpirit lot id (bp_meta_data.id). Keep the most-recently-synced post per lot id and set the rest topost_status = 'trash'— thelotpost type has trash disabled, so do it via SQL, and do not usedraft(whichfindByBSMetastill matches). Then recount theauction_numberterm and clear caches.---
- Modified:
-
v3.7.0.33 Stable May 30, 2026Download v3.7.0.33📦 9.44 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.0.33] — 2026-05-30
Fixed —
auctionforge_sync_lotscron never scheduled (Lots push to wpdeploy silently stopped)AF_Lots_Syncuses the custombp_ten_minutelyschedule. The sync crons were registered fromAF_Stats_Init::init(), which runs at plugin-load time — *before*auctionforge.phpaddsbp_ten_minutelyto thecron_schedulesfilter further down the same file. Sowp_schedule_event(time(), 'bp_ten_minutely', 'auctionforge_sync_lots')ran against an unknown schedule, failed silently, and left the hook permanently unscheduled. The other four sync hooks use core'shourlyand were unaffected — which is why only the Lots push to wpdeploy went stale (observed: 3 days with no push while every other stream ran on time). Fixed by deferring the syncregister()calls to theinithook, by which point every custom schedule is registered. The hook self-arms on the next request after upgrade.Fixed — Safety Sync admin run reported a false "Request failed (HTTP 200)"
auctionforge_ajax_run_cron_taskreturns its result as JSON, but a stray PHP notice/deprecation emitted by a task (e.g. the Safety Sync BidSpirit delta-check on PHP 8.1) printed into the response body ahead of the JSON, so the browser couldn't parse it and the modal showed a misleading "Request failed (HTTP 200)" even though the task succeeded. The handler now buffers task output, discards it before emitting JSON, and logs any suppressed output viaSyncLogger(contextadmin) so the offending notice can still be found and fixed at the root.Added — "Clear duplicate cron events" button on Sync Status
The one-shot
AF_Cron_Cleanupmigration only runs once per site, so per-hostname duplicate events created by an older build *after* the migration had already marked itself done were never swept. NewAF_Cron_Cleanup::force_sweep()runs the same sweep on demand, exposed via a Clear Cron Dupes button in the Sync Queue toolbar (AJAXauctionforge_clear_cron_dupes). It removes legacy-args duplicates of the four per-hostname hooks plus the two orphan hooks, keeping one clean event each.Internal
- Modified:
inc/stats/class-stats-init.php— syncregister()calls wrapped inadd_action('init', …, 20). - Modified:
inc/maintenance/class-cron-cleanup.php— new publicforce_sweep()reusing the existing sweep helpers. - Modified:
auctionforge.php—auctionforge_ajax_run_cron_task()output-buffered against stray output; newauctionforge_ajax_clear_cron_dupes()handler + registration; plugin header → 3.7.0.33. - Modified:
admin/view/sync-status.php— added the "Clear Cron Dupes" button and handler.
Note: with all scheduling now arg-free (3.7.0.28+), the duplicates do not regenerate once swept; a canonical-hostname redirect (www ↔ apex) at the web-server level remains good hygiene but is no longer required to keep the cron table clean.
---
- Modified:
-
v3.7.0.32 Stable May 30, 2026Download v3.7.0.32📦 9.44 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.0.32] — 2026-05-30
Fixed — V3 sync worker never spawned on some hosts (silent import stall)
On some sites (confirmed on a LiteSpeed/cPanel install) the V3 queue worker never actually started, so catalog lots were staged into
import_historybut never converted to WordPress posts —postId IS NULL,syncFinished = 0,syncTimes = 0, and no errors logged. Clearing the queue and reprocessing re-stalled immediately.Root cause.
SyncQueue::spawnWorker()starts the worker by POSTing the REST route/wp-json/auctionforge/v1/sync-worker. It tried an HTTP loopback tohttp://127.0.0.1/with the site'sHost:header first, and the success test wasif (!is_wp_error($result))— it never inspected the HTTP status code. On hosts where127.0.0.1+Hostheader resolves to the wrong vhost (or the route is blocked), that request returns 404/403, which is not aWP_Error. The old code therefore treated the 404 as a successful spawn, loggedSpawned … (HTTP 404), returnedtrue, and never reached the HTTPS fallback. The worker never ran; nothing drained the queue or the staging table. Sites whose127.0.0.1loopback resolves correctly returned 200 and were unaffected — which is why this was host-specific.Fix.
spawnWorker()now reads the response code and only treats 2xx as a successful spawn. On any non-2xx (or aWP_Error) from the loopback it logs a warning and falls through to therest_url()HTTPS path, which is likewise status-checked. A new constant short-circuits the doomed attempt on known-bad hosts:```php
define('AUCTIONFORGE_WORKER_SKIP_LOOPBACK', true); // wp-config.php — skip 127.0.0.1, go straight to HTTPS
```
Added — Automatic staging recovery on the safety cron
The hourly
auctionforge_sync_safetycron previously only spawned a worker when the queue had pending jobs. Lots that were staged intoimport_historybut left unconverted (e.g. after a worker died, or while the spawn bug above was in effect) had no queue job attached, so nothing ever picked them up. The safety cron now also checksSyncQueue::stagingBacklogCount()and, when it is non-zero, kicks the staging processor (non-blocking) to convert the backlog into posts — making the pipeline self-healing without manual intervention.Added — "Process Staging" button on Sync Status
A new Process Staging button in the Sync Queue toolbar (Lots → Sync Status) runs
InsertLotsBSover the staging table on demand — for when lots are staged but not appearing on the site. It fires non-blocking and switches the Live Console into fast-poll so the operator can watch posts being created in batches of 50.Internal
- Modified:
inc/sync/SyncQueue.php—spawnWorker()rewritten with HTTP-status-aware fallback +AUCTIONFORGE_WORKER_SKIP_LOOPBACKsupport;triggerStagingProcessor()gained a$blockingparameter (non-blocking fire-and-forget mode for cron/admin); newstagingBacklogCount()helper (countsimport_historyrows withsyncFinished = 0). - Modified:
auctionforge.php—auctionforge_sync_safety_action()now drains orphaned staging via the non-blocking processor when a backlog exists; newprocess_stagingcase in theauctionforge_sync_enqueueAJAX handler; plugin header bumped to 3.7.0.32. - Modified:
admin/view/sync-status.php— added the "Process Staging" button and its click handler.
---
- Modified:
-
v3.7.0.31 Stable May 29, 2026Download v3.7.0.31📦 9.17 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.7.0.30 Stable May 29, 2026Download v3.7.0.30📦 9.17 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.7.0.29 Stable May 27, 2026Download v3.7.0.29📦 9.17 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.7.0.28 Stable May 21, 2026Download v3.7.0.28📦 9.34 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.0.28] — 2026-05-21
Fixed — WP-Cron duplicate-event accumulation
A scan across 18 WordPress installs found that 16 sites had duplicate cron events for the AuctionForge sync hooks — some with up to 20 copies of
bp_auto_closeand 6 copies ofauctionforge_sync_safety. Root-cause analysis identified three distinct bugs in the plugin's scheduling code, all fixed in this release.Bug 1 —
$_SERVER['SERVER_NAME']baked into scheduling args.auctionforge.php:2511-2554scheduledbidspirit_import,data_import,bidspirit_archive_import, andauctionforge_sync_safetywith$attr = array($_SERVER['SERVER_NAME']). WP-Cron'swp_next_scheduled()guard matches on hook plus args signature, so every distinct hostname the site was reached under (canonical,www., IP-direct, internal cron caller hitting127.0.0.1, an old domain from before a rename) bypassed the guard and scheduled its own copy. The handlersunset($host)— the argument was always vestigial. Removed$attrentirely; events now schedule with empty args. Also dropped the!empty($_SERVER['SERVER_NAME'])outer guards so wp-cli / OS-cron bootstraps can prime the schedule on a fresh install.Bug 2 —
bp_auto_closeper-term scheduling with no cleanup.inc/controllers/BidspiritLots.phpscheduled one 10-minutely event per running auction term, keyed on['term_id' => …, 'slug' => …]— correct in principle, but nothing ever unscheduled the event when the auction ended or its term was deleted. Long-running sites accumulated dozens of dangling per-term events polling the BidSpirit API for auctions that no longer existed. Added two cleanup paths:bidspirit_auto_close_action()now self-cleans: if the term has been deleted, or if the remote auction state returned by BidSpirit is notRUNNINGorENDING, the handler writes the remote state toauction_stateterm meta and callswp_clear_scheduled_hook('bp_auto_close', $args)for that term. The meta write is necessary becausecheck_is_live()runs on everywp_loadedand re-armsbp_auto_closefor any term still markedSTATE_RUNNINGin local meta — without writing the remote state through, the unschedule would be undone on the next request. The meta write is consistent with whatSafetySync::run()andinc/import/AuctionList.php:229already do on non-RUNNING upstream states.- New
pre_delete_termhook on theauction_numbertaxonomy unschedules the matching event when an auction term is deleted, so a deleted auction can't leave a poll behind.
Bug 3 — Orphan hooks from removed plugin code.
bidspirit_meta_migrate_syncandbidspirit_migrate_syncwere scheduled by a previous plugin version whose code has since been removed. They sat inwp_options.cronfiring into the void on every site (theMIGRATE_SYNCconstant inSettings.php:36is the lone survivor — commented out). No upgrade path ever calledwp_clear_scheduled_hook()for them.Added — One-shot migration:
AF_Cron_CleanupNew class at
inc/maintenance/class-cron-cleanup.php, registered fromclass-stats-init.php, runs once on the first request after upgrading and:1. Walks
_get_cron_array()and unschedules every event for the four hooks rewritten in Bug 1 whose args are non-empty — clearing per-hostname stragglers. The new scheduling code at the top ofauctionforge.phphas already armed a clean no-args event by the time the migration runs (the scheduling code runs at plugin-load time; the migration runs oninitpriority 20). Preserving empty-args events means the schedule remains continuously populated — no gap between sweep and the next page load.2. Unschedules every event (any args) for the two orphan hooks from Bug 3 — those have no callbacks anywhere in the codebase, so empty-args entries are also dead weight.
3. Marks itself done via the
auctionforge_cron_cleanup_v1option and never runs again.Note that
wp_clear_scheduled_hook($hook)alone only matches events with empty args — events scheduled with non-empty args (which is exactly our duplicate population) are left behind. The migration walks the cron array directly and callswp_unschedule_event($timestamp, $hook, $event['args'])for each match.Logs a summary line via
SyncLoggerso the per-site outcome is auditable: e.g.cron-cleanup: Cleared duplicate/orphan cron events: {"bp_auto_close":20,"auctionforge_sync_safety":3,"bidspirit_meta_migrate_sync":2}.Failure modes after this release
| State | Outcome |
|---|---|
| Fresh install (no duplicates) | Migration runs, finds nothing, sets the option, exits in microseconds. |
| Upgrade on a site with N duplicates | Migration clears them on first load post-upgrade; scheduling code re-arms each hook once with empty args. |
| Site reached under multiple hostnames going forward | No new duplicates — args no longer carry the hostname, so the
wp_next_scheduled()guard catches re-registration regardless of which hostname triggers it. || Auction term deleted via wp-admin |
pre_delete_termclears the matchingbp_auto_closeevent in the same request. || Auction ends remotely (state != RUNNING/ENDING) | Next handler run sees the state change and self-cleans the event. |
| Auction term deleted while a
bp_auto_closecron is mid-flight | Handler'sget_term()returns null, falls throughunschedule_auto_close()cleanup branch, returns without an API call. |Internal
- Created:
inc/maintenance/class-cron-cleanup.php(~80 LOC). - Modified:
auctionforge.phplines 2511-2573 — removedSERVER_NAMEargs and outer guards frombidspirit_import/data_import/bidspirit_archive_import/auctionforge_sync_safetyscheduling blocks; handler signatures changed from($host)to();add_action(..., 10, 1)simplified toadd_action(...). - Modified:
inc/controllers/BidspiritLots.php— addedpre_delete_termaction registration; rewrotebidspirit_auto_close_action()with term-existence check and state-based self-clean; addedunschedule_auto_close()andcleanup_auto_close_on_term_delete()helpers; dropped theSERVER_NAMEguard from the per-term scheduling loop. - Modified:
inc/stats/class-stats-init.php— requires and registersAF_Cron_CleanupalongsideAF_Data_Purge.
---
-
v3.7.0.27 Stable May 20, 2026Download v3.7.0.27📦 9.16 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Run Identity Check — calls the new wpdeploy
-
v3.7.0.26 Stable May 13, 2026Download v3.7.0.26📦 9.16 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Run Identity Check — calls the new wpdeploy
-
v3.7.0.25 Stable May 07, 2026Download v3.7.0.25📦 9.01 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Run Identity Check — calls the new wpdeploy
-
v3.7.0.24 Stable May 07, 2026Download v3.7.0.24📦 9 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Run Identity Check — calls the new wpdeploy
-
v3.7.0.23 Stable May 07, 2026Download v3.7.0.23📦 9 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Run Identity Check — calls the new wpdeploy
-
v3.7.0.22 Stable May 06, 2026Download v3.7.0.22📦 8.99 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.7.0.21] — 2026-05-05
Added — Connection Diagnostics on the Updates admin page
A new Connection Diagnostics card on Lots → Updates that detects and one-click-heals identity mismatches between this site and the deployment server.
The problem it solves. When a WordPress install is cloned (template → customer site, dev → prod, backup → restore), the cloned wp_options carry the original site's
auctionforge_update_api_keyover to the newhome_url(). The new install then makes API calls to wpdeploy *authenticated as the original site*. wpdeploy looks up by api_key, not by URL, so every/sync-user-tracking,/check-updates,/report-health, and most importantly the Kuma push URL, gets recorded against the WRONGwpdeploy.sitesrow. The cloned site's monitor stays silent; the original's monitor shows traffic that isn't from it.What the diagnostic does:
- Run Identity Check — calls the new wpdeploy
GET /api.php/whoamiendpoint and compares the returnedcaller_site_urlagainsthome_url(). Renders a verdict ("matches" / "MISMATCH") plus the full server-side view (site_id, site_name, last_check, plugin version on file, remote IP). The "Re-register & Reset Kuma" button only appears if there's a mismatch. - Re-register & Reset Kuma — calls
/request-access(which is idempotent onsite_url— wpdeploy returns the canonical api_key for *this*home_url(), regardless of whatever wrong key we were holding), wipesauctionforge_kuma_push_urland any leftover heartbeat cron events, then immediately fires/check-updatesso the api_request interceptor catches the freshly-correct kuma_push_url for *this* site. The keepalive mu-plugin's heartbeat picks it up on the next 60s tick.
Internal
AuctionForge_Auto_Updater::whoami()(new) — thin wrapper overapi_request('whoami', [], 'GET'). Returns the parsed wpdeploy/whoamipayload.AuctionForge_Auto_Updater::reset_connection()(new) — three-step heal: re-register → wipe Kuma cached state → re-fetch via/check-updates. Returns shape suitable forwp_send_json_success.- Two new AJAX actions:
auctionforge_diagnose_connection,auctionforge_fix_connection. Both behindmanage_optionscapability + nonce check. auctionforge_normalize_host()helper for case/scheme/www-tolerant URL comparison.
Server-side companion
Requires the wpdeploy
/api.php/whoamiendpoint added in the same release window. The endpoint is gated behind whatever api_key the caller already presented (a master key returnstype=masterwith no site row; a per-site key returns the site row that key resolves to). Reveals nothing the caller doesn't already implicitly authenticate against.---
[3.7.0.20] — 2026-05-04
Changed
- Kuma heartbeat path is now wpdeploy-independent. The actual heartbeat firing — wp-cron schedule, page-load piggy-back, outbound HTTP — moved out of AF and into the AuctionForge Keepalive mu-plugin (
wp-content/mu-plugins/auctionforge-keepalive.php, bumped to v1.2.1). Once a site has been provisioned once andauctionforge_kuma_push_urlis cached locally, the heartbeat keeps reporting forever even if AF is deactivated, mid-upgrade, deleted from disk, or wpdeploy is offline. The mu-plugin can't be deactivated by WordPress, can't be broken by an AF release, and depends on AF for nothing. - AF only writes the
kuma_push_urloption now. It no longer schedules the cron, no longer fires the heartbeat, and no longer holds any heartbeat-related state. Theapi_request()interceptor still catcheskuma_push_urlfrom/check-updatesand/report-healthresponses and persists it; the mu-plugin reads it from there. - Migration block.
init_hooks()clears any leftoverauctionforge_kuma_heartbeatwp-cron event from AF ≤ 3.7.0.19 on first plugin load after upgrade. The mu-plugin runs the same clear on its ownplugins_loadedhook — belt and braces. The new event the mu-plugin schedules iskuma_heartbeat_tickon a newkuma_60sschedule. AuctionForge_Auto_Updater::kuma_heartbeat()no-opped. Kept as a safety landing pad so any staleauctionforge_kuma_heartbeatwp-cron event that fires one last time before migration kicks in doesn't trip a "no callback registered" warning. Real heartbeat work happens in the mu-plugin.
Added — operator override
AUCTIONFORGE_KUMA_PUSH_URLconstant. Define it inwp-config.phpto point a site directly at Kuma without ever involving wpdeploy:
```php
define('AUCTIONFORGE_KUMA_PUSH_URL', 'https://kuma.example.com/api/push/<token>?status=up&msg=&ping=');
```
The constant takes precedence over the
auctionforge_kuma_push_urloption. With it set, the site can be operated fully outside wpdeploy — useful for one-off installs, customer-managed sites, or disaster-recovery scenarios where wpdeploy is unreachable for an extended period.Internal — Keepalive mu-plugin v1.2.1
The bundled keepalive template at
inc/keepalive/auctionforge-keepalive.template.phpnow also handles Kuma heartbeats. Three layered triggers ensure cadence stays tight regardless of site traffic:1. Dedicated wp-cron event
kuma_heartbeat_tickon a new 60-secondkuma_60sschedule2. Page-load piggy-back on
init: any request older than 50s since the last beat fires another, transient-guarded so concurrent hits coalesce. Free on busy sites; rescues low-traffic sites the moment any visitor arrives.3. Self-heal inside the cron callback: if the event somehow gets cleared, re-arm it before sending the beat.
HTTP is
timeout=2, blocking=false— fire-and-forget, never slows page loads. External IP introspection (ipify → ifconfig.me → icanhazip fallback chain, 24h transient cache) is duplicated in the mu-plugin so it doesn't depend on AF being loaded for the IP lookup.The keepalive installer (
class-keepalive-installer.php) detects theKEEPALIVE_VERSION: 1.2.1header bump and idempotently overwrites every site's mu-plugin on the next AF load. No manual reactivation needed.Failure modes after this release
| State | Heartbeat? |
|---|---|
| AF active and healthy | ✅ via mu-plugin |
| AF deactivated by an operator | ✅ mu-plugin runs anyway |
| AF deleted from disk | ✅ as long as
auctionforge_kuma_push_urloption exists OR constant is set || AF mid-upgrade / partial install | ✅ mu-plugin doesn't depend on AF code paths |
| wpdeploy down (existing site) | ✅ option is cached locally, no wpdeploy round-trip |
| wpdeploy down (brand-new site, never provisioned) | ❌ first-time URL provisioning still needs wpdeploy. One-shot, accepted trade-off. |
| Site has zero traffic AND wp-cron isn't firing | ❌ truly nothing to trigger a beat. Mitigation is OS-level cron / DISABLE_WP_CRON, out of scope here. |
---
[3.7.0.19] — 2026-05-04
Changed
- Reverted Kuma heartbeat cadence: 3 min → 1 min.
auctionforge_kuma_heartbeatis back on thebp_minutelyschedule. With Kuma's 310s silence window, 60s cadence tolerates 4 missed wp-cron ticks before flipping a monitor red — the original "give-it-leeway" semantics. The 3-min cadence introduced in 3.7.0.18 was strictly less chatty but tighter (a single missed tick would trip Kuma), and the trade-off wasn't worth it operationally. - Migration block updated.
init_hooks()now detects any existing event scheduled on a non-bp_minutelycadence (e.g. thebp_3_minutelyset by 3.7.0.18) and re-schedules ontobp_minutely. Sites that took 3.7.0.18 roll back automatically on first plugin load after upgrade. - First-fire jitter narrowed back to 0–60s to match the new cadence.
Notes
- The
bp_3_minutelycron schedule registration inauctionforge.phpis kept (harmless; available for future use). Only the heartbeat scheduling sites have been reverted. - Apparent latency in Kuma flipping a monitor red after a real outage is governed by Kuma's per-monitor
interval(silence-detection window, currently 310s on the wpdeploy/Kuma side), not by this cron's cadence. To detect outages faster, lower the Kumainterval; to add more tolerance to wp-cron drift, raise it.
---
[3.7.0.18] — 2026-05-04
Changed
- Kuma heartbeat cron cadence: 1 min → 3 min.
auctionforge_kuma_heartbeatnow runs on a newbp_3_minutelyschedule (180s) instead ofbp_minutely(60s). This pairs with Kuma's 310s silence-detection window — a single late wp-cron tick (3 min + drift) still lands inside the threshold, but normal traffic to Kuma drops by ~3×. - Random first-fire offset widened from 60s → 180s. With ~50 sites on a single Kuma instance, spreading first-fires across the full 3-min window prevents synchronised bursts every 3 minutes.
- Auto-migration of existing scheduled events. On plugin upgrade
init_hooks()callswp_get_scheduled_event('auctionforge_kuma_heartbeat')and, if the existing event is onbp_minutely, clears it and reschedules ontobp_3_minutely. Idempotent — a no-op once migrated.
Internal
- New cron schedule registered:
bp_3_minutely(BP_MINUTELY * 3= 180s). Defined alongside the existingbp_minutely/bp_ten_minutely/bp_hourly/bp_24_hourlyschedules inauctionforge.php.
---
[3.7.0.17] — 2026-05-04
Fix: "Install Now" always returned
up_to_dateThe
/sync/versionREST endpoint (called by wpdeploy's Install Now button) was always respondingsuccess: false, reason: up_to_dateregardless of the actual installed version, defeating the button's whole purpose.Root cause
rest_force_plugin_update()did:```
delete_site_transient('update_plugins');
$this->check_pending_updates();
$updates = get_site_transient('update_plugins');
```
check_pending_updates()calls the/pending-updatesendpoint on wpdeploy (which returns the deploy queue), but it does not touch the WPupdate_pluginstransient. The transient just got deleted on the line above and never gets re-populated, so theisset($updates->response[…])check always failed → always returnedup_to_date.Fix
- Use
wp_update_plugins()instead — that's what fires thepre_set_site_transient_update_pluginsfilter chain that AF's owncheck_for_updates()callback hooks into to seed the transient. Same pattern is already used correctly ininstall_update()at line 280. - Belt-and-braces: when the request body carries an explicit target
versionand that version is strictly greater than the on-disk plugin version (perversion_compare), bypass the transient check and force the install. This handles edge cases where the transient seeding fails (network blip during the wp-cron cycle, hosting-side cache, etc.) so the explicit Install Now command still does what it says.
---
[3.7.0.16] — 2026-05-04
Server external IP reported to wpdeploy + included in Kuma heartbeats
Each AF install now reports its server's outbound external IP. Two channels:
1. Kuma heartbeat
msg— the per-minute push payload now reads<wp_version>|<plugin_version>|<external_ip>(e.g.6.9.4|3.7.0.16|152.53.187.82) so you can spot at a glance which physical box each site lives on. When a host goes dark, multiple monitors flip red simultaneously with the same IP — instant root-cause hint.2.
/report-healthpayload — a newexternal_ipfield that wpdeploy stores onsites.external_ip. Visible in the Sites admin view as a tappable IP chip; clicking it filters the table to all sites sharing that IP.How the IP is detected
AuctionForge_Auto_Updater::get_external_ip()calls one of three public echo services in turn —api.ipify.org,ifconfig.me/ip,icanhazip.com— with a 4-second timeout each. The first one that returns a valid IP wins. Result is cached in a transient for 24 hours so we don't hit external services on every report cycle.Wpdeploy fallback for older plugins
/report-healthnow also capturesREMOTE_ADDR(orHTTP_CF_CONNECTING_IPwhen behind Cloudflare) when the payload doesn't includeexternal_ip. So sites still on 3.7.0.15 or older start populating their IP today via the same route, no plugin update needed for that data path.---
[3.7.0.15] — 2026-05-04
Kuma monitors land in an "Unallocated" group by default
New AuctionForge installs are auto-enrolled into Kuma under a new top-level group called Unallocated so the operator can review them and drag-drop into the right home (Bidspirit Auction Sites, Stephen's Auctions, Misc Websites, etc.) before classification. Without this every new monitor would appear at the dashboard top level alongside whatever group hierarchy already exists.
What changed (wpdeploy-side, no plugin behaviour change)
KumaProvisionernow ensures an "Unallocated" group exists in Kuma (find-or-create via the same Socket.IOaddpath used for monitors, withtype='group') before provisioning each new push monitor, and passesparent=<group_id>in theaddpayload so the new monitor lands inside it.- The Node helper accepts arbitrary
typeandparentfields on the input payload (previously hardcoded totype='push'). - The group itself gets the same
user_id=1reassignment so it shows up on the human admin's dashboard.
Existing Clumber monitor (id=71) was migrated into the new group as part of this release.
This is a wpdeploy infrastructure change — the AF plugin code is unchanged from 3.7.0.14. Version bumped only to mark a clean snapshot point.
---
[3.7.0.14] — 2026-05-04
Uptime Kuma push heartbeat with automatic enrolment
Each AF install now appears as its own monitor on the central Uptime Kuma at https://kuma.smoothbyteit.dev, beating every minute and flipping red within ~90 seconds when a site goes dark. Auto-enrolled — no manual Kuma UI clicks per site.
How it works
1. First contact: when the plugin calls
/request-accessor/report-health, wpdeploy's newKumaProvisioneropens a Socket.IO connection to Kuma as a dedicatedauctionforge-botuser, calls the officialaddevent, and gets back a fresh push token. The resulting URL is shipped to the plugin in the JSON response askuma_push_url.2. Plugin-side: a generic interceptor on
api_request()persistskuma_push_urlto the optionauctionforge_kuma_push_urlwhenever it appears in any wpdeploy response, and immediately schedules the newauctionforge_kuma_heartbeatcron on the existingbp_minutelyinterval.3. Heartbeat: the cron handler sends a single
wp_remote_getto the push URL each minute withmsg=<wp_version>|<plugin_version>so Kuma's dashboard shows the running stack at a glance. Kuma's silence-detection window is 90s, so a single missed wp-cron tick won't false-DOWN.4. Idempotency: the Kuma monitor's
descriptionfield storeswpdeploy_site_id:<id>. Re-registering the same site short-circuits at the wpdeploy layer (existingkuma_monitor_idonsites), or — if that's missing — Kuma's existing row is reattached via the description anchor. Re-installing the plugin never duplicates monitors.Why Socket.IO and not direct DB insert
Kuma's monitor scheduler does not poll its own DB for new rows —
startMonitors()only loads monitors at boot, and new ones are armed via the authenticated Socket.IOaddevent handler (which callsstartMonitor()and arms the per-monitorsafeBeatsetTimeout). A direct INSERT would create a "phantom" monitor that accepts pushes but never flips DOWN on silence, defeating the whole feature.Notifications + monitor ownership
Provisioner reads existing Kuma notification rows whose channel is Telegram (or marked default-for-new-monitors) and attaches them to every new monitor via the
addevent'snotificationIDList. After the Socket.IOaddsucceeds, the provisioner direct-writesuser_id=1to the new monitor row in Kuma's SQLite — Kuma'saddhandler hardcodesuser_id=socket.userID(the bot at user 2), which causes the frontend's real-time monitor-list updates to skip the admin's session. The follow-up UPDATE stamps ownership to the human admin so monitors render with their full name on the admin dashboard immediately.Failure modes
Every call boundary degrades safely. If Kuma is unreachable when a site registers, registration completes without
kuma_push_url; the next twicedaily/report-healthretries. Heartbeat HTTP failures are silent (which is the desired outcome — Kuma's silence-detection is the signal). Kuma reset / monitor row deleted: provisioner's pre-flight check NULLs the wpdeploy anchors and falls through to fresh provisioning on the next call.Operator setup
Already done: the
auctionforge-botKuma user has been created and credentials populated in wpdeploy'sconfig.php. To rotate or migrate Kuma later, editKUMA_URL/KUMA_BOT_USER/KUMA_BOT_PASSinconfig.php— no plugin redeploy needed.---
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
[3.7.0.12] — 2026-05-03
Stop-word denylist for BidSpirit category-tag concatenations
BidSpirit's
wp_bp_meta_data.tagsfield arrives as PascalCase category names lowercased without separators (uscollectibles,firearmsaccessoriesammunition,usdecorativearts,printsandmultiples). They look like keywords but they're really categories, and on sites where the tags field is populated they completely swamp the top-5 chips for power users (e.g. one buyer's profile was led byuscollectibles 453 viewsdespite that being a label rather than a real interest signal).29 known patterns added to
stop-words.phpso the tokenizer drops them at extraction time. New ones surfacing from sites we haven't seen yet should be added here as they appear; the wpdeploy?tab=keywordscorpus page makes them easy to spot (sort by most users with low lots count and look for non-word concatenations).---
[3.7.0.11] — 2026-05-03
Keyword extractor — capture model years + hyphenated calibers, drop more auction noise
The People view's top-5 keyword chips were filling up with generic descriptors (
household,model,original,assortment) instead of the brand/maker terms (mauser,whitworth,colt) that actually distinguish one buyer from another. Live-data review on the new?tab=keywordsKeyword Corpus page on wpdeploy confirmed the cause: real signal was being lost at the tokenizer stage.Tokenizer changes (
inc/stats/class-keyword-extractor.php)Three pre-passes now run before the standard split-on-non-alpha:
1. Hyphenated / x-separated calibers —
.45-70,.30-06,7.62x54r,8x57. Without this they're split into pure-digit fragments and dropped, losing the actual model identifier.2. Caliber-with-suffix —
9mm,.22lr,.45acp,.380acp. Previously only survived by accident because of the trailing letters; now extracted explicitly.3. 4-digit model years 1700–2099 —
1873,1903,1911,1971. Pure-digit tokens were unconditionally dropped; now retained when year-shaped.Standard pass relaxed to allow 4-digit pure-digit tokens between 1700 and 2099.
Stop-word additions (
inc/stats/stop-words.php)Conservative — only universally empty auction descriptors:
original,marked,accessories,decorative,household,goods,made,condition,assortmentDeliberately NOT added:
antique,vintage,rare,fine— those carry buyer-segmentation signal (antiquarian buyers vs. modern-collectibles buyers). Borderline cases (metal,wooden,american,model) are left to the wpdeploy-side IDF re-rank.Companion change on wpdeploy (no plugin update needed)
refresh_people_rollup.phpnow multiplies each keyword's score by an IDF factor pulled fromkeyword_idf(refreshed hourly byrefresh_lot_keyword_index.php). Generic keywords that appear in many lots get demoted automatically; brand/maker words that only appear on a handful get lifted into the top-5 chips.After deploying this version, reset the
auctionforge_lots_sync_watermarkoption on each site and clear thewp_af_stats_lot_keywordstable to force re-extraction of every lot through the new tokenizer.---
[3.7.0.10] — 2026-05-03
Lots sync — ship resolved CDN image URL, not bare BidSpirit filename
class-lots-sync.phpwas sendingimagesListStr's first comma-token verbatim asimage_url— that's the raw BidSpirit storage value (e.g.001.jpg). Wpdeploy received a bare filename, the browser resolved it against the wpdeploy host, and every lot thumbnail 404'd.The lots-sync now instantiates
LotImageand pulls the resolved CDN URL (LABEL_MEDIUMthumbnail), with fallback toLABEL_ORIGINALand a final defensive check that drops anything that doesn't start withhttp(s)://. Resolution is wrapped in try/catch — a missing class or BidSpirit settings issue leavesimage_urlempty rather than blowing up the sync batch.After deploying this version on a site, reset its
auctionforge_lots_sync_watermarkoption (delete the row) to re-push existing lots through the new resolver. The wpdeploy view skips<img>rendering whenimage_urlisn't a real URL, so dirty rows degrade silently while the watermark catches up.---
[3.7.0.9] — 2026-05-03
Keepalive sweep — auto-clears WP upgrader debris that produces silent "Could not create directory" failures
WordPress 6.x's atomic-rollback flow leaves
wp-content/upgrade-temp-backup/lying around when its post-install cleanup misfires (the user-facing symptom is "Update failed: Could not create directory" after an update that has actually already succeeded). Stalewp-content/upgrade/<plugin>-tmp/and.maintenancelockfiles from crashed upgrades cause the *next* upgrade attempt to fail the same way. None of WP's own scheduled cleanup catches these reliably.The keepalive mu-plugin (
KEEPALIVE_VERSION1.1.0) now sweeps all three classes of debris on every page load, transient-guarded to once per hour. Safety thresholds prevent it touching anything actually in flight:.maintenancefiles less than 5 min old,wp-content/upgrade/*entries less than 60 min old, andupgrade-temp-backup/less than 30 min old are left alone. Wrapped in try/catch with no throws, so the sweep can never block a request.The keepalive installer detects the version bump and rewrites
wp-content/mu-plugins/auctionforge-keepalive.phpautomatically on the next AF load, no manual reactivation needed.---
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
[3.7.0.7] — 2026-05-02
Identity stitch on bidder login — keyword interest tallying now actually fires
Why
The keyword extractor was filling
wp_af_stats_lot_keywordscorrectly (197 lots, 734 distinct keywords on Clumber), butwp_af_user_interestsandwp_af_user_recent_activitywere both empty — zero rows of any dimension. Same on every site running the new sync streams.Root cause: identity stitching (visitor_hash → email) only fired in the tracker AJAX when
is_user_logged_in()returned true. On these auction sites the actual bidders authenticate via BidSpirit, never aswp_users— so the WP login check was always false, the stitch table stayed empty, andmaybe_record_lot_view($url, $email, …)got called with an empty email and bailed out before tallying. Lot views by real bidders never landed in the user-side interest profile.What changed (no auth-path intervention)
The fix piggybacks on the *existing* fire-and-forget tracking AJAX that already runs after a successful BidSpirit login (
afLogIP("login", bidspiritUser)). It does not touch the BidSpirit auth path itself.inc/ip-logger.phpauctionforge_ajax_log_ip()— after the existingauctionforge_user_trackinginsert, if the request carries avisitor_hashand the email is valid, callsAF_Stats_Visitor_Email_Map::record($vh, $email, $ip). Wrapped in try/catch so any failure here can never break the existing tracking response.assets/js/app.js+src/js/app.jsafLogIP()— readslocalStorage['af_vid'](defensively, ITP/private-mode safe) and includes it asvisitor_hashin the POST body.inc/stats/class-stats-init.phpmaybe_record_lot_view()— when$emailis empty (the common case for BidSpirit-only bidders) but avisitor_hashis present, falls back toAF_Stats_Visitor_Email_Map::lookup($visitor_hash). If a stitch exists, the view tallies; if not, it stays anonymous.
Net effect: a bidder logs in (BidSpirit), the post-login tracker AJAX fires as it always has, and now also writes one extra row into
af_stats_visitor_emails. From that point onward, every page they view on the same browser/device tallies intoaf_user_interestskeyed on their email. The BidSpirit transaction itself is untouched.---
[3.7.0.6] — 2026-05-02
Critical fix —
X-API-Keyheader collision with sibling SmoothByte pluginsWhat was wrong
On any site running both AuctionForge and another SmoothByte-stack plugin that also points its updater at
wpdeploy.smoothbyteit.dev(e.g.seo-aeo-optimizer), the second plugin'shttp_request_argsfilter overwrote AuctionForge'sX-API-Keyheader on every outbound API call. Wpdeploy then 401'd because the key it received belonged to a different plugin instance — usually aseo_aeo_update_api_keythat wpdeploy had no record of.The symptom was a quiet, total auth failure:
report-health,check-updates, everysync-*route, all 401. The watchdog flagged streams as "gone" and updates stopped flowing. Affected sites: any install where the AuctionForge key and the sibling plugin's key happen to differ. Sites where both plugins shared a key were silently safe (the overwrite was a no-op).Fix
AuctionForge_Auto_Updater::add_download_auth()now refuses to clobber an existingX-API-Keyand, for download URLs, only stamps when the URL slug matches our own. That keeps targeted calls fromapi_request()intact regardless of filter ordering, and prevents either plugin from writing its key onto the other's download URL.The same fix has been applied to
seo-aeo-optimizerso both ends of the collision are defensive.---
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds.---
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
---
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
---
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell.---
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
---
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
---
[3.6.4.0] — 2026-05-02
Identity stitching: visitor_hash → user_email
Why
The
af_stats_*tables (pageviews,sessions,searches,outbound) and the lot-engagement tables (af_lot_analytics,af_lot_shares) currently store activity keyed only on the anonymousaf_vidlocalStorage hash — there is no link from any browsing event to the user's email even when they're logged in. Without that link, the central wpdeploy server can never aggregate behavioural data on a per-person basis, which is the prerequisite for the cross-site user-data lake being built on wpdeploy (see/root/.claude/plans/i-want-to-add-synthetic-goose.mdPhase 1).This release adds the bridge: every authenticated tracker hit records a
(visitor_hash, user_email)pair into a new table. From here on, anonymous browsing can be resolved to an email via JOIN for any session the user was logged in.Changes
- New table
wp_af_stats_visitor_emails—(visitor_hash, user_email, ip_address, first_seen, last_seen)withUNIQUE(visitor_hash, user_email)+ index onuser_email. Created viadbDeltainside the existingAF_Stats_Database::create_tables()so it ships through the standardplugins_loadedmigration path — no manual reactivation needed after auto-update. af_stats_db_versionbumped 1 → 2.AF_Stats_Init::maybe_create_tables()now compares against the newAF_Stats_Database::DB_VERSIONconstant rather than a hard-coded< 1, so future schema bumps (Phase 2 onwards) just require constant edits + new dbDelta calls.- Identity record on every authenticated tracker hit.
AF_Stats_Ajax::handle_track()now callsAF_Stats_Visitor_Email_Map::record($visitor_hash, $email, $ip)wheneveris_user_logged_in()is true. Trust boundary: the email is read server-side fromwp_get_current_user()— clients cannot assert who they are. - Defensive table-exists guard. The new helper short-circuits silently if the migration hasn't run yet on a given site (race window on the very first request after auto-update); the next request finds the table and proceeds. No fatals, no missing rows.
- Email normalised at write time — lowercase + trim +
is_email()validation before storage. Marketing audience match-rates downstream depend on consistent casing.
Files Changed
- Created:
inc/stats/class-visitor-email-map.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION constant; new table; drop_tables list) - Modified:
inc/stats/class-stats-init.php(require new helper; migration check uses constant) - Modified:
inc/stats/class-stats-ajax.php(record pair inhandle_track) - Modified:
auctionforge.php(Version header bump)
Verification on Clumber dev
1. After auto-update, load any
wp-admin/*page once →wp_af_stats_visitor_emailsexists,af_stats_db_version = 2.2. Log in as a test user, browse 5 lots → exactly one row in
wp_af_stats_visitor_emailsfor that(visitor_hash, email)pair, withlast_seenupdating on each hit.3.
SELECT email FROM wp_af_stats_visitor_emails WHERE visitor_hash IN (SELECT visitor_hash FROM wp_af_stats_pageviews WHERE created_at > NOW() - INTERVAL 1 HOUR)returns the test email — proving the JOIN works.Privacy / scope
- This change writes a
(visitor_hash, email)pair to a single new table on the AF site. Nothing is sent to wpdeploy yet — the sync stream lands in Phase 2. - Anonymous (logged-out) browsing is unaffected: no row is created until the user authenticates.
- Bots are still skipped via
AF_Stats_Tracker::is_bot()before any tracking writes.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Run Identity Check — calls the new wpdeploy
-
v3.7.0.21 Stable May 05, 2026Download v3.7.0.21📦 9.26 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.7
Adds Connection Diagnostics card on Lots → Updates with Run Identity Check + Re-register & Reset Kuma buttons. Detects and one-click-heals identity mismatches caused by wp_options carried over from a clone/template.
-
v3.7.0.20 Stable May 04, 2026Download v3.7.0.20📦 9.25 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.7
Kuma heartbeat moved into the keepalive mu-plugin (v1.2.0). Site keeps reporting to Kuma even when AF is deactivated/missing or wpdeploy is offline. Adds AUCTIONFORGE_KUMA_PUSH_URL constant override for fully-decoupled operation.
-
v3.7.0.19 Stable May 04, 2026Download v3.7.0.19📦 9.25 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.7
Reverted Kuma heartbeat cadence to bp_minutely (60s). Auto-migrates any bp_3_minutely event from 3.7.0.18 back to bp_minutely on plugin load.
-
v3.7.0.18 Stable May 04, 2026Download v3.7.0.18📦 9.25 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.7
Kuma heartbeat cron cadence: 1 min → 3 min (new bp_3_minutely schedule). Includes auto-migration of existing scheduled events from bp_minutely on plugin upgrade. Pairs with Kuma 310s silence-detection window — single late wp-cron tick still lands inside threshold while reducing network chatter ~3x.
-
v3.7.0.17 Stable May 04, 2026Download v3.7.0.17📦 9.25 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
Fix: Install Now button always returned up_to_date. The /sync/version endpoint was deleting the WP update_plugins transient then calling check_pending_updates (which hits wpdeploys /pending-updates queue endpoint, not the WP transient seed path). Transient stayed empty -> up_to_date check always tripped. Replaced with wp_update_plugins() + a belt-and-braces explicit-version-compare fallback that forces install when the request body carries a target greater than the on-disk version.
-
v3.7.0.16 Stable May 04, 2026Download v3.7.0.16📦 9.25 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
Server external IP reported to wpdeploy + included in Kuma heartbeat msg as <wp_version>|<plugin_version>|<external_ip>. Plugin introspects via api.ipify.org with 24h transient cache and 3-endpoint fallback. Wpdeploy /report-health captures the IP from the payload OR falls back to REMOTE_ADDR for older plugin versions. Sites view shows the IP chip; click filters to other sites on the same physical host.
-
v3.7.0.15 Stable May 04, 2026Download v3.7.0.15📦 9.25 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
New Kuma monitors land in an Unallocated group by default. Operator can drag-drop into the right group post-hoc. wpdeploy provisioner now ensures the group exists (Socket.IO add with type=group) and passes parent=<id> in subsequent monitor add payloads. Plugin code unchanged from 3.7.0.14 — clean snapshot point.
-
v3.7.0.14 Stable May 04, 2026Download v3.7.0.14📦 9.24 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
Uptime Kuma push heartbeat with automatic enrolment. Each AF install becomes its own Kuma push monitor on first contact via wpdeploy provisioning a Socket.IO add as the dedicated auctionforge-bot user. Plugin-side bp_minutely cron beats every 60s with msg=<wp_version>|<af_version>; Kuma silence-detection window is 90s. Idempotency via wpdeploy.sites.kuma_monitor_id + monitor.description=wpdeploy_site_id:<id>. Notifications: existing Telegram channel auto-attached. Monitor ownership re-stamped to user_id=1 after add so admin dashboard renders the name immediately.
-
v3.7.0.13 Stable May 04, 2026Download v3.7.0.13📦 8.98 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.0.13] — 2026-05-04
Wide shortcode: hide Register button for logged-in users
- When a user is logged in to BidSpirit (
body.bp-login), the Register button is now automatically hidden on wide-format auction cards (bp_upcoming_wideandbp_auction_list layout="wide"). - The View Catalog / Catalog Coming Soon button expands to full width when the Register button is hidden.
- Uses a lightweight
MutationObserverto react to BidSpirit's async session init. - Applied to both
UpcomingAuctionWide.phpandAuctionList.phpshortcode CSS blocks.
---
- When a user is logged in to BidSpirit (
-
v3.7.0.12 Stable May 03, 2026Download v3.7.0.12📦 9.24 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
Stop-word denylist for BidSpirit category-tag concatenations (uscollectibles, firearmsaccessoriesammunition, usdecorativearts, etc.). 29 known patterns added to stop-words.php so the tokenizer drops them at extraction time. New ones surfacing should be added as observed via the wpdeploy ?tab=keywords corpus page.
-
v3.7.0.11 Stable May 03, 2026Download v3.7.0.11📦 9.24 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
Keyword extractor + IDF rerank — capture model years (1700-2099), hyphenated calibers (.45-70, 7.62x54r), and caliber-with-suffix (9mm, .45acp). Drop universal auction descriptors (original, marked, accessories, decorative, household, goods, made, condition, assortment). Companion wpdeploy change multiplies each keyword score by an IDF factor pulled from keyword_idf so generic words get demoted automatically and brand/maker terms rise to the top-5 chips. Reset auctionforge_lots_sync_watermark + truncate wp_af_stats_lot_keywords on each site after upgrade to force re-extraction.
-
v3.7.0.10 Stable May 03, 2026Download v3.7.0.10📦 9.24 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
Lots sync — ship resolved CDN image URL via LotImage helper instead of bare BidSpirit filename. Wpdeploy was receiving "001.jpg" verbatim and the browser 404'd. Now uses LABEL_MEDIUM (~400px CDN URL) with ORIGINAL fallback and a defensive drop for anything that does not start with http(s)://. Wrapped in try/catch so a missing class or settings issue leaves image_url empty rather than failing the batch. Reset auctionforge_lots_sync_watermark on each site after upgrade to re-push existing lots through the resolver.
-
v3.7.0.9 Stable May 03, 2026Download v3.7.0.9📦 9.24 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
Keepalive sweep — auto-clears WP upgrader debris (upgrade-temp-backup/, stale .maintenance, wp-content/upgrade/<plugin>-tmp/) that produce silent "Could not create directory" failures. KEEPALIVE_VERSION bumped to 1.1.0; installer rewrites the mu-plugin automatically on next AF load. Sweep is transient-guarded to once per hour with safety thresholds (.maintenance >5min, upgrade/* >60min, upgrade-temp-backup >30min) so anything in flight is left alone. Wrapped in try/catch, never blocks a request.
-
v3.7.0.8 Stable May 03, 2026Download v3.7.0.8📦 8.97 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.7.0.8] — 2026-05-03
Shop auction UI refinements & share button settings
Shop (Buy-It-Now) catalog improvements
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
buyoutPricemetadata. Regular auction catalogs retain the original sorting options. - Removed "Buyout price:" label: On shop lot pages, the redundant "Buyout price:" text before the price is now hidden. Only the price and "Buy Now" button are displayed. Regular auction buyout lots still show the label.
- Removed increments table & terms links: On shop lot pages, the "Increments table" and "Terms and conditions" links are hidden since they don't apply to buy-it-now items. The "Make an inquiry" link remains.
Buyout confirmation modal overhaul
- Sales tax logic: The buyout confirmation modal now follows the same sales tax detection as the regular bid modal. When the auction house has a US state configured, displays "Sales Tax" instead of "VAT", with the correct percentage for same-state buyers and 0% for out-of-state.
- Hide 0% buyer's premium: The Buyer's Premium line is hidden when the commission is 0%.
- Shop-specific text: For shop buyouts, all disclaimer text is replaced with: *"You will receive an email with your invoice, including shipping, and a secure payment link."*
- Shop-specific buttons: "I don't Agree" → "Cancel", "I Agree" → "Buy". The "Terms of sale" button is removed for shop buyouts. Non-shop buyouts retain the original layout.
Purchase confirmed screen
- Title changed from "Confirm purchase:" to "Purchase Confirmed".
- Thank you message updated to: *"Thank you for your order. We will confirm your order within 1-2 business days and email you an invoice, including shipping, and a secure payment link."*
Share button settings (Appearance tab)
- Added a Share Buttons section to the BidSpirit Appearance settings tab with individual checkboxes for each sharing platform: Copy Link, WhatsApp, Facebook, Telegram, X (Twitter), VK, Pinterest, and Email.
- All platforms default to enabled (shown). Unchecking a platform hides that share button on all lot pages (both shop and regular auctions).
---
- Shop sorting dropdown: Added a dedicated sorting dropdown for shop catalogs with options: Recently Added, Price: High, and Price: Low. Sorts by
-
v3.7.0.7 Stable May 02, 2026Download v3.7.0.7📦 9.24 MB⚙️ WP +🔧 PHP +
Identity stitch on bidder login — keyword interest tallying now actually fires. The keyword extractor was filling lot_keywords correctly but user_interests stayed empty because identity stitching only fired when is_user_logged_in() was true on the tracker AJAX. On these auction sites bidders authenticate via BidSpirit, never as wp_users. Fix piggybacks on the existing post-login afLogIP tracker AJAX (no auth-path intervention): JS now sends localStorage[af_vid] as visitor_hash, PHP records the stitch via AF_Stats_Visitor_Email_Map::record after the existing tracking insert, and maybe_record_lot_view falls back to the stitch map when wp_get_current_user is empty. Wrapped in try/catch so any failure can never affect the BidSpirit transaction or the existing tracking response.
-
v3.7.0.6 Stable May 02, 2026Download v3.7.0.6📦 9.23 MB⚙️ WP +🔧 PHP +
Critical fix — X-API-Key header collision with sibling SmoothByte plugins (e.g. seo-aeo-optimizer). The second plugins http_request_args filter overwrote AuctionForges X-API-Key header on every outbound API call, causing wpdeploy to 401 every sync, report-health, and check-update on affected sites. Filter is now defensive: refuses to clobber an existing X-API-Key, and for download URLs only stamps when the URL slug matches our own. Same fix applied to seo-aeo-optimizer.
-
v3.7.0.5 Stable May 02, 2026Download v3.7.0.5📦 9.23 MB⚙️ WP +🔧 PHP +
[3.7.0.5] — 2026-05-02
Sync Status admin panel — diagnostic + manual triggers in wp-admin
Why
Diagnosing sync issues used to require shell access on the site or piecing together state from wpdeploy's app.log. This release surfaces the same diagnostics inside wp-admin so a site admin (or anyone with
manage_options) can see exactly what's happening and trigger fixes in one click.Lives inside the existing Sync Status menu (
Lots → Sync Status) — appended below the existing content as a "Sync Diagnostics & Manual Controls" section, no duplicate menu entry.What's on the panel
Connection summary — plugin version,
af_stats_db_version, wpdeploy server URL, last 8 chars of API key, keepalive install state + auto-reactivation count.Sync streams table — for each of the 10 cron hooks (user-tracking, user-profile, user-interests, user-activity, lots, bids+wins, vulnerabilities, report-health, check-updates, fraud-enforcement):
- Configured schedule
- Last fired timestamp + status colour-coded (green=success, blue=idle/empty, red=error)
- Record count from last run
- Next scheduled run
- Current watermark (where applicable)
- "Fire now" button — clears doing_cron, calls
do_action($hook)synchronously, shows the result
One-shot utilities
Test wpdeploy connection— calls/api.php/infowith the site's API keyClear stuck cron lock— deletes thedoing_crontransient (the classic fix when scheduled events stop firing)Reinstall keepalive— overwritesmu-plugins/auctionforge-keepalive.phpfrom the bundled templateBackfill bids from legacy tracking— replayslast_event_type='bid'rows fromwp_auctionforge_user_trackingintowp_af_bid_eventsso the bids-wins stream picks them up. Useful for sites that have years of bid history in the legacy table but nothing in the new queueRun all due events now— clears doing_cron and fires everyauctionforge_*cron event whose timestamp is past due, in one go
Live response output panel shows the JSON returned from each action so you can see what just happened without leaving the page.
Files Changed
- Created:
inc/admin/class-sync-status-page.php - Modified:
inc/stats/class-stats-init.php(load + init the panel) - Modified:
admin/view/sync-status.php(appendAF_Sync_Status_Page::render_panel()call at the bottom) - Modified:
auctionforge.php(Version 3.7.0.4 → 3.7.0.5)
Verification
Navigate to Lots → Sync Status in wp-admin on any 3.7.0.5 site. Scroll past the existing content to the new "Sync Diagnostics & Manual Controls" section. Click "Test wpdeploy connection" — you should see
{"success": true, "auth_type": "site"}. Click "Fire now" on any stream — the row's last-fired timestamp should refresh after 1–2 seconds. -
v3.7.0.4 Stable May 02, 2026Download v3.7.0.4📦 9.23 MB⚙️ WP +🔧 PHP +
[3.7.0.4] — 2026-05-02
Self-healing: failed plugin updates can no longer take a site offline
Why
WordPress's plugin updater isn't transactional — when an update fails partway (corrupt unzip, fatal during the new version's
plugins_loaded, disk pressure, theme conflict, etc.) WP often deactivates the plugin and leaves the site without it. Once AuctionForge is dropped fromactive_plugins, no AF code runs again — including its sync handlers — until someone notices and reactivates manually. On the AuctionForge site fleet, this had quietly happened to ~5 sites we found; almost certainly more on production sites we don't have filesystem access to.This release ships an autonomous watchdog that lives outside the AuctionForge plugin entirely so it survives even when AF gets knocked out.
What changed
1. Self-installing keepalive (mu-plugin)
inc/keepalive/auctionforge-keepalive.template.php— a 50-line WordPress must-use plugin. Lives inwp-content/mu-plugins/, which WordPress can't deactivate. On everyplugins_loaded(priority 1, before any regular plugin), it checks: isauctionforge/auctionforge.phpon disk but missing fromactive_plugins? If yes AND the file has a validPlugin Name:header (smoke check — don't reactivate a corrupt zip), it re-adds AF andincludes the file in the same request.inc/keepalive/class-keepalive-installer.php— runs on every successful AF load. Copies the template intowp-content/mu-plugins/auctionforge-keepalive.phpif missing or version-bumped. Idempotent: comparesKEEPALIVE_VERSIONheaders, only writes on change. Refuses to overwrite a foreign mu-plugin (defensive against name collisions). Failure is non-fatal — install errors are recorded inwp_optionsfor wpdeploy visibility but never throw.
Net effect. Any site that successfully loads AF 3.7.0.4 or later will have the keepalive permanently installed in mu-plugins. From that point forward, even if a future update hard-fails, the keepalive recovers AF on the next pageview without any manual intervention or filesystem access. Crucially: this works on sites we don't own — the keepalive ships inside the AF zip, gets installed during the first successful update, and lives in mu-plugins thereafter.
2. Forensic markers
The keepalive writes three options on each recovery:
auctionforge_auto_reactivated_at— most recent recovery timestampauctionforge_auto_reactivated_count— total recoveriesauctionforge_auto_reactivated_failed_at— set when the keepalive saw AF on disk but with no Plugin Name header (genuinely corrupt — needs operator attention)
These get picked up by the AF sync streams and surfaced on the wpdeploy dashboard so you can see which sites have been quietly recovering themselves and whether a particular release is failing more than it should.
3. Companion: post-update-silence alerts on wpdeploy (server side)
Wpdeploy's
report-healthroute now compares incomingplugin_versionagainst the row and writeslast_version_changed_at+previous_plugin_versionon change. TheSyncWatchdogcron then alerts if a site has reported a version change but not sent any heartbeat for 2+ hours since — likely a failed install. Emitssync_incidentsrows withkind=post_update_failureso the existing Telegram fan-out picks them up. Each version-change event alerts at most once.Files Changed
- Created:
inc/keepalive/auctionforge-keepalive.template.php,inc/keepalive/class-keepalive-installer.php - Modified:
inc/stats/class-stats-init.php(callAF_Keepalive_Installer::maybe_install()on every plugin load) - Modified:
auctionforge.php(Version 3.7.0.3 → 3.7.0.4)
Companion server-side changes (already deployed on wpdeploy.smoothbyteit.dev — not in this zip):
sitestable:last_version_changed_at TIMESTAMP NULL,previous_plugin_version VARCHAR(20) NULLapi.php/report-health route detects version changesSyncWatchdog::maybe_alert_post_update_failure()emits the new alert kind
Verification
1. Drop a 3.7.0.4 install on any site. Confirm
wp-content/mu-plugins/auctionforge-keepalive.phpexists post-load.2. Manually deactivate AF from the database (
DELETE FROM wp_options ...active_plugins...). Hit the homepage. AF reappears inactive_plugins;auctionforge_auto_reactivated_countincrements.3. Delete
mu-plugins/auctionforge-keepalive.php. Trigger AF load (any pageview). The file reappears.4. Bump KEEPALIVE_VERSION in the source template, ship a new release, observe the installed mu-plugin update on next AF load.
Operator notes
- Sites currently in the broken state (AF deactivated, no keepalive yet) are NOT auto-rescued by this release — they need a one-time manual reactivation in wp-admin OR a fresh AF install via wp-admin's Upload Plugin. After that, the keepalive is in place and future failures self-heal.
- The post-update-failure alert won't fire for upgrades that happened before this release (no
last_version_changed_atwas recorded). It activates from the first version change wpdeploy sees from each site after this update lands.
-
v3.7.0.3 Stable May 02, 2026Download v3.7.0.3📦 9.22 MB⚙️ WP +🔧 PHP +
[3.7.0.3] — 2026-05-02
Fix: profile re-sync on login / profile-edit / register (no more empty names)
Why
The hourly profile sync uses a
user_registeredwatermark — once a user has been pushed once, the watermark advances past them and they're never re-pulled. That meant:1. Existing users whose first_name/last_name became populated AFTER their first sync stayed empty on wpdeploy forever.
2. BidSpirit-driven sites that don't write WP's standard
first_name/last_namefields shipped empty rows even though the canonical name was visible indisplay_name.The People view was showing
—for the name on rows that absolutely had a name available — just not where the sync handler was looking.Fixes (
inc/sync/class-profile-sync.php+inc/stats/class-stats-init.php)AF_Profile_Sync::push_for_user_id($user_id, $blocking=false)— new public method that builds a single user's profile row, normalises it, and POSTs to wpdeploy bypassing the watermark. Non-blocking by default (wp_remote_postwithblocking=false, timeout=2s) so login latency is unaffected.build_user_row($user_id)extracted as a reusable helper. Both the hourly batch and the on-demand push call it. Centralises role-filtering + display_name fallback + usermeta probing.display_namefallback. Whenwp_usermeta.first_name/last_nameare both empty (typical on BidSpirit-only sites),build_user_row()parsesdisplay_name("John Smith" → first=John, last=Smith) so we don't ship a row with a blank name when the data is sitting on the user record itself.- Three new action hooks registered in
class-stats-init.php::init(): wp_login→push_for_user_id($user->ID)— every login refreshes the rowprofile_update→ same — profile edits in wp-admin refreshuser_register→ same — new registrations get pushed instantly (instead of waiting up to 60 min for the next cron tick)
Files Changed
- Modified:
inc/sync/class-profile-sync.php(refactor + new method + display_name fallback) - Modified:
inc/stats/class-stats-init.php(three new action hooks) - Modified:
auctionforge.php(Version 3.7.0.2 → 3.7.0.3)
Verification
1. On a 3.7.0.3 site, find a user whose
wpdeploy.user_profiles.first_nameis empty:SELECT site_id, email, first_name FROM user_profiles WHERE email='something@example.com'2. Log in as that user (or have them log in).
3. Within ~2 seconds, re-query —
first_nameshould now be populated, either from the standard meta key OR parsed fromdisplay_name.4. The non-blocking POST means the user's login experience is unchanged — no spinner, no extra latency.
Operator note
Existing wpdeploy rows with empty
first_name/last_namewill refresh automatically as users log in or edit their profile. To force a one-shot re-walk of every user on a site (regardless of watermark), reset the watermark there:DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';Next hourly cron will re-pull every non-admin user with the new fallback applied.
-
v3.7.0.2 Stable May 02, 2026Download v3.7.0.2📦 9.22 MB⚙️ WP +🔧 PHP +
[3.7.0.2] — 2026-05-02
Fix: sync-lots datetime mismatch + idle syncs now heartbeat
Why
Two issues surfaced in the wpdeploy Sync Health dashboard:
1. Every site's
sync-lotswas stuck onapi_error. The plugin sendslast_modifiedstraight fromwp_bp_meta_data.lastUpdate, which is a Unix epoch string ("1756919754"). wpdeploy'slots.last_modifiedcolumn is aDATETIME, so MariaDB rejects the insert withSQLSTATE[22007]: Incorrect datetime value. The handler recordsapi_errorand the watermark never advances, so every cron tick re-fails on the same batch.2. Sites with nothing to sync looked dead. When a sync handler had an empty batch (e.g. no non-admin users yet) it was returning early without POSTing anything. wpdeploy's
update_sync_health()only fires on a successful POST, sosites.sync_health_jsonstayed NULL and the dashboard showed grey dots — indistinguishable from a broken cron. WP-Cron was actually firing fine; the watchdog just couldn't tell.Fixes
inc/sync/class-lots-sync.php— newepoch_to_datetime()helper converts the BidSpirit epoch field to UTCY-m-d H:i:sbefore shipping. Idempotent: already-formatted datetimes pass through untouched, invalid values becomeNULL.inc/sync/class-sync-base.php::run()— always POST, even when the batch is empty. Empty payload is{records: []}and wpdeploy's existing handlers handle it gracefully (loop is skipped,update_sync_health()still fires withsynced=0). This makes the dashboard reflect cron liveness, not just data flow.
Files Changed
- Modified:
inc/sync/class-lots-sync.php - Modified:
inc/sync/class-sync-base.php - Modified:
auctionforge.php(Version 3.7.0.1 → 3.7.0.2)
Verification
1. On Clumber:
do_action('auctionforge_sync_lots')— confirm the previousauctionforge_last_sync-lots_status = api_errorflips tosuccess(N)and thatwpdeploy.lotsnow contains rows.2. On a site with no non-admin users:
do_action('auctionforge_sync_user_profile')— confirmauctionforge_last_sync-user-profile_status = idle(0)AND that wpdeploy's Sync Health page now shows a green dot for that cell. -
v3.7.0.1 Beta May 02, 2026Download v3.7.0.1📦 9.22 MB⚙️ WP +🔧 PHP +
[3.7.0.1] — 2026-05-02
Fix: marketing data leaks WordPress admins into the people list
Why
After the 3.7.0.0 rollout the central
peopleview on wpdeploy was showing site administrators alongside real auction-house users — thesync-user-profilehandler read straight fromwp_userswith no role filter, so any account with administrator/editor/author/contributor/shop_manager capabilities was getting shipped as if it were a buyer. Marketing audiences must contain buyers, not staff.Fixes (
inc/sync/class-profile-sync.php)- The
read_batch()SQL now LEFT JOINs{$wpdb->usermeta}on the prefix-aware capabilities key ({$wpdb->prefix}capabilities) and filters out any user whose serialized capabilities meta_value containsadministrator,editor,author,contributor,shop_manager, orsuper_admin. Users withIS NULL(no roles registered) are still allowed through — those are typically subscribers/buyers on this fleet. - Watermark on
auctionforge_profile_sync_watermarkis unchanged; the next run after upgrade picks up the same batch with the new filter applied. Operationally the wpdeploy team also wiped the existing admin rows fromuser_profiles,user_interests,user_recent_activity,bids_wins, andpeopleto clear the leak server-side — total of 35 rows scrubbed.
Files Changed
- Modified:
inc/sync/class-profile-sync.php - Modified:
auctionforge.php(Version 3.7.0.0 → 3.7.0.1)
Verification on Clumber dev
1. Find a logged-in admin:
SELECT u.user_email FROM wp_users u JOIN wp_usermeta um ON um.user_id=u.ID AND um.meta_key='wp_capabilities' WHERE um.meta_value LIKE '%administrator%';2. Reset the watermark:
DELETE FROM wp_options WHERE option_name='auctionforge_profile_sync_watermark';3. Trigger sync:
do_action('auctionforge_sync_user_profile');4. On wpdeploy: confirm those admin emails do NOT appear in
user_profiles(SELECT * FROM user_profiles WHERE email='admin@…';returns zero rows).Privacy / scope
- No API contract changes. The wpdeploy
/api.php/sync-user-profileroute accepts the same payload shape; this is purely a source-side filter narrowing what gets sent.
- The
-
v3.7.0.0 Beta May 02, 2026Download v3.7.0.0📦 9.22 MB⚙️ WP +🔧 PHP +
[3.7.0.0] — 2026-05-02
Cross-site user data lake — full Phase 2 + Phase 3 build
Why
AuctionForge sites were exposing per-lot engagement (views, favourites, bids, wins) keyed only on the anonymous
af_vidlocalStorage hash, with no central aggregation. Marketing — even the "collect data now, send later" phase — needs a per-person view across the whole AF network: name + phone + postcode + country + IP + device fingerprint + the keywords they engage with + the bids they place + the lots they win, all rolled up across every site they've used. This release wires every piece needed for that, and keeps it sustainable: per-site aggregates rather than raw event firehose, watermarked syncs with retry, 14/30-day site-side retention, watchdog + Telegram alerts for dead streams.See
/root/.claude/plans/i-want-to-add-synthetic-goose.mdfor the full architecture.New tables (idempotent dbDelta on the existing
plugins_loadedmigration path)wp_af_stats_lot_keywords— per-lot keyword cache. Title + tags + author tokenized (lowercase → drop stop-words → drop ≤2-char → drop pure-digit), one row per (post_id, keyword). Populated onsave_post_lot.wp_af_user_interests— running tally per(user_email, dimension, dimension_value)where dimension ∈keyword | category | author | price_band. UPSERT-keyed so increments are atomic. Bounded by users × distinct interests (typically a few dozen rows per user).wp_af_user_recent_activity— capped per-user activity log (last 50 events per email). Seeds the drill-down timeline on the wpdeploy person view.wp_af_bid_events— bid + win events (auto-created by the bids sync handler). Drained and shipped to wpdeploy hourly.synced_atcolumns added towp_af_stats_pageviews,wp_af_stats_sessions,wp_af_stats_searches,wp_af_stats_outbound. Powers the durable sync-and-purge lifecycle.af_stats_db_versionbumped 2 → 3. The existingAF_Stats_Init::maybe_create_tables()picks up the new tables on the next admin page load after auto-update.
New helper classes
AF_Keyword_Extractor(inc/stats/class-keyword-extractor.php) — pure tokenizer + cache writer + read-through helper. Hookssave_post_lotto refresh on lot changes. Stop-words tunable viaapply_filters('af_keyword_stop_words', ...).AF_User_Interests(inc/stats/class-user-interests.php) —record_view / record_favorite / record_bid / record_win. All idempotent UPSERTs. CallsAF_Stats_Tracker::should_skip()first so admins/bots never pollute interest profiles.AF_Sync_Base(inc/sync/class-sync-base.php) — abstract durability backbone. Readssynced_at IS NULLbatches, POSTs, marks rows synced only on a confirmed 200, retries on next cron run if the POST fails. Idempotent UNIQUE keys on the wpdeploy side mean retries never duplicate.AF_Profile_Sync/AF_Interests_Sync/AF_Activity_Sync/AF_Lots_Sync/AF_Bids_Sync— five concrete sync streams.AF_Data_Purge(inc/maintenance/class-data-purge.php) — daily cron: 14-day floor on synced rows, 30-day hard cap with CRITICAL alert if any unsynced row hits the cap (which would mean wpdeploy has been unreachable for a month).
New cron schedules (auto-registered on plugins_loaded if
auctionforge_update_server_url+_api_keyare configured)| Hook | Schedule | Endpoint |
|---|---|---|
|
auctionforge_sync_user_profile| hourly |POST /api.php/sync-user-profile||
auctionforge_sync_user_interests| hourly |POST /api.php/sync-user-interests||
auctionforge_sync_user_activity| hourly |POST /api.php/sync-user-activity||
auctionforge_sync_lots| every 10 min |POST /api.php/sync-lots||
auctionforge_sync_bids_wins| hourly |POST /api.php/sync-bids-wins||
auctionforge_purge_synced_data| daily | (purge cron — local) |All sync handlers are watermarked or
synced_at-gated; cron mid-run failure is harmless because the next run picks up the same rows.Hooks fired
af_stats_pageview_recorded($url, $email, $visitor_hash)— fires insideAF_Stats_Ajax::handle_track()after a logged-in pageview is recorded. Default listener resolves URL → lot post →AF_User_Interests::record_view().af_stats_lot_favorited($email, $post_id)— fires from the existinglot_analytics_record_favoriteAJAX handler when a watchlist add happens. Default listener callsrecord_favorite().
Server-side companion (wpdeploy)
This release is paired with wpdeploy changes (handled out-of-band; not in the plugin zip):
- 5 new API routes matching the sync streams above. All return
synced+ watermark metadata so the plugin can advance / mark rows synced. update_sync_health()helper in wpdeploy — every successful sync POST writes(stream, last_seen_at, last_synced_count, state)intosites.sync_health_json.SyncWatchdog— 5-minute cron on wpdeploy that scans every (site × stream) cell and runs anok → overdue → gone → recovering → okstate machine. Emits alerts via the existingsync_incidentsTelegram fan-out.- People section in the wpdeploy admin —
?tab=people(cross-site rollup with top keywords / lifetime spend),?tab=person&email=...(drill-down with timeline + bids),?tab=people-export(Google Customer Match + Meta Custom Audience CSV with sha256 hashing per spec). ?tab=sync-health— site × stream traffic-light grid showing every sync's state.
Files Changed
- Created:
inc/stats/class-keyword-extractor.php,inc/stats/class-user-interests.php,inc/stats/stop-words.php - Created:
inc/sync/class-sync-base.php,inc/sync/class-profile-sync.php,inc/sync/class-interests-sync.php,inc/sync/class-activity-sync.php,inc/sync/class-lots-sync.php,inc/sync/class-bids-sync.php - Created:
inc/maintenance/class-data-purge.php - Modified:
inc/stats/class-stats-database.php(DB_VERSION = 3, new dbDelta calls, synced_at column adds) - Modified:
inc/stats/class-stats-init.php(require new classes, register sync crons, wireaf_stats_pageview_recordedandaf_stats_lot_favoritedlisteners) - Modified:
inc/stats/class-stats-ajax.php(fireaf_stats_pageview_recordedafter pageview record; fireaf_stats_lot_favoritedon watchlist add) - Modified:
auctionforge.php(Version: 3.6.4.0 → 3.7.0.0)
Verification on Clumber dev
1. After auto-update + first wp-admin page load:
wp_af_stats_lot_keywords,wp_af_user_interests,wp_af_user_recent_activityall exist;af_stats_db_version = 3.2. Save a test lot titled
"Pair of Victorian silver candlesticks by Mappin & Webb"→wp_af_stats_lot_keywordshas rows forvictorian, silver, candlesticks, mappin, webb(and notpair, of, by).3. Log in as throwaway test user, view 3 of those lots →
wp_af_user_interestsshows(test@…, keyword, 'victorian', view_count=3).4. Wait one hourly cron, confirm
wpdeploy.user_interestsmirror via the wpdeploy admin →?tab=people→ search for the test email.5. Confirm
wpdeploy.sites.sync_health_jsonhasuser-interests.state = 'ok'for the Clumber site.6. Pause the cron, wait 2h, watch the watchdog flip the cell to
overdueand emit a Telegram alert.Privacy / scope
- Admins and bots are filtered out at write time via
AF_Stats_Tracker::should_skip(). - Email is normalized lowercase + trim before storage; phone normalized to E.164 (UK-aware) on the sync side.
- No outbound activation yet — wpdeploy stores everything but doesn't send anything to anyone. Marketing send mechanics (consent flag, suppression list, sender) explicitly deferred.
-
v3.6.3.2 Stable May 01, 2026Download v3.6.3.2📦 8.93 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
[3.6.2.9] — 2026-05-01
Public Auctions REST API Endpoint
Features
- New REST endpoint
GET /wp-json/auctionforge/v1/public/auctions— Exposes auction list data (title, date, image, state, catalog link) for consumption by the AuctionForge Hub plugin on parent/main sites. - Parameters:
type(upcoming or past, default: upcoming),limit(max results, default: 50). - Handles multi-day auctions with correct part numbers and catalog links.
- Returns auction metadata including
auction_id,source_site, andday_indexfor deduplication.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php - Created:
inc/api/PublicAuctions.php
---
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- InsertLotsBS loop break condition — The stage 5 insert loop in
-
v3.6.3.1 Stable May 01, 2026Download v3.6.3.1📦 9.19 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.6.3.1] — 2026-05-01
Fix: New lots never processed + Live console improvements
Why
When BidSpirit adds new lots to an auction (e.g. items 683-692), the sync pipeline correctly staged all 709 lots into
import_historybut the InsertLotsBS processing loop exited after the first batch of 500 unchanged lots, never reaching the genuinely new lots at the tail of the queue. This caused new lots to remain permanently stuck in staging — synced but never created as WordPress posts.Separately, the admin Live Console was replaying the entire log history from the beginning on every page load, and showed long silent gaps during API calls with no indication of what the worker was doing.
Fixes
- InsertLotsBS loop break condition — The stage 5 insert loop in
CatalogSyncWorker::process()andprocessDayEnded()previously broke whenupdated lots === 0, even if the batch had cleared hundreds of unchanged ("unupdated") lots from the queue. New lots with higher IDs were never reached. Now breaks only when bothupdated lotsandunupdated lotsare 0, meaning the staging queue is truly empty. - Live Console starts from current position —
consoleSinceIdwas hardcoded to0, causing the console to page through the entireaf_sync_logtable (50 rows at a time) before reaching real-time entries. Now initializes toSyncLogger::getLatestId()so only entries created after page load appear. queueIsActivedeclaration order — Variable was declared after functions that referenced it, causing the console to always start at the slow 3s polling rate even when a worker was active. Moved declaration before first use.- Progress logging during API calls —
LotList::auctionProcessing()made 2-3 BidSpirit API calls with zeroSyncLoggerentries, creating long silent gaps in the console. Added log entries for API fetch start, response received (with duration), data staged, and API errors. - Per-auction progress in bulk runs —
LotList::run()now logsProcessing auction X/Y: {id}for each auction in the bulk loop, and logs skipped auctions. processDayEndedinsert loop logging — Added batch progress logging and timeout guard matching theprocess()method pattern. Previously this loop was completely silent.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/LotList.php,admin/view/sync-status.php
---
- InsertLotsBS loop break condition — The stage 5 insert loop in
-
v3.6.3.0 Stable May 01, 2026Download v3.6.3.0📦 8.97 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.6.3.0] — 2026-05-01
Public Auctions API — Token Authentication
Why
The
/wp-json/auctionforge/v1/public/auctionsendpoint added in v3.6.2.9 was publicly accessible. Since auction data should only be served to authorized aggregator sites, the endpoint now requires a valid auth token.Changes
- Token verification — The
PublicAuctionsREST endpoint now validates anX-AF-Tokenheader (or?token=query parameter) against the site's existing Webhook auth token from AuctionForge settings. - Returns
401 Unauthorizedfor missing/invalid tokens and403 Forbiddenif no auth token is configured on the site. - Uses
hash_equals()for timing-safe comparison.
Files Changed
- Modified:
auctionforge.php,inc/api/PublicAuctions.php
---
- Token verification — The
-
v3.6.2.9 Stable May 01, 2026Download v3.6.2.9📦 8.97 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — 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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield 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_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon 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_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso 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_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
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 toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor 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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- New private helper
-
v3.6.2.8 Stable Apr 30, 2026Download v3.6.2.8📦 9.23 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[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 intoaf_sync_log(so the in-app sync console still shows the retry chain) but theauctionforge_sync_error_loggedfan-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_patternsfilter — themes/MU plugins can add their own substrings.
Files Changed
- Modified:
auctionforge.php,inc/sync/SyncLogger.php
---
- New private helper
-
v3.6.2.7 Stable Apr 30, 2026Download v3.6.2.7📦 9.23 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[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 readshttps://my-account.bidspirit.com/.
Files Changed
- Modified:
auctionforge.php,inc/controllers/Settings.php
---
- Settings → Connections tab: the placeholder/help text under the Server field showed
-
v3.6.2.6 Stable Apr 30, 2026Download v3.6.2.6📦 9.23 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.6.2.6] — 2026-04-30
Fix stale
plugin_versionin reportsWhy
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_VERSIONis read from the plugin file'sVersion:header *once at plugin load*, then frozen into$this->current_versionon 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 (viaget_file_data()— uncached). report_health,push_sync_error_immediate, andretry_sync_errorsnow sendget_live_version()instead of the cached constant.- The pre-existing iteration through active plugins inside
report_healthalready usedget_plugin_data()per plugin, so secondary plugin entries were already accurate; only the top-levelplugin_versionfield was stale.
Files Changed
- Modified:
auctionforge.php,inc/class-auto-updater.php
---
- New helper
-
v3.6.2.5 Stable Apr 30, 2026Download v3.6.2.5📦 9.22 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.6.2.5] — 2026-04-30
Real diagnostics for plugin self-updates
Why
The wpdeploy
update_queuetable 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: theinstall_update()method in both clients (AF auto-updater and wpd-client) treatedPlugin_Upgrader::upgrade()as a boolean, throwing awayWP_Errorobjects and skin-emitted error messages.Fixes (
inc/class-auto-updater.phpinstall_update())- Refreshes the
update_pluginstransient 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_Errorwas 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->resultwhich holds the finalWP_Errorfor 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 fromclient-plugin/wpd-client.phpon the wpdeploy server).Files Changed
- Modified:
auctionforge.php,inc/class-auto-updater.php
---
- Refreshes the
-
v3.6.2.4 Stable Apr 30, 2026Download v3.6.2.4📦 9.22 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.6.2.4] — 2026-04-30
Bulk-sync worker dispatch fix
Fixes
- The HTTP-loopback worker (
auctionforge_rest_sync_workerinauctionforge.php) had its own switch statement for job-type dispatch, distinct from the one incrontasks/syncWorker.php. The newfull_sync_upcomingandfull_sync_pastjob types were only added to the CLI worker, so the loopback path produced "Unknown job type" errors. Both switches now route those types toBulkSyncWorker::process. inc/coreIncludes.phpnowrequire_oncesBulkSyncWorker.phpso the class is available wherever sync code runs.
Files Changed
- Modified:
auctionforge.php,inc/coreIncludes.php
---
- The HTTP-loopback worker (
-
v3.6.2.3 Stable Apr 30, 2026Download v3.6.2.3📦 9.22 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.6.2.3] — 2026-04-30
Fixes for end-to-end Telegram round-trip
Fixes
verify_api_keyreads the option fresh on every call (hash_equalsagainstget_option('auctionforge_update_api_key')) instead of relying on the cached$this->api_keyfrom 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 existingtrigger-installflow with a JSON response (always HTTP 200 withsuccessflag). The original endpoint returnedWP_Error('no_update', …, status: 404)when the plugin was already on the latest version, which nginx'serror_page 404directive 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
---
-
v3.6.2.2 Stable Apr 29, 2026Download v3.6.2.2📦 9.22 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.6.2.2] — 2026-04-29
Catalog-sync kind disambiguation
Fix
catalog_syncjobs target a single auction that may be either active or archived. Until now their alerts were always classified askind=upcoming. The classifier now resolves the auction'sauction_stateterm 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
---
-
v3.6.2.1 Stable Apr 29, 2026Download v3.6.2.1📦 9.22 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[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
kindfield classified at log-write time from the queue'sjob_type. - Plugin version in alerts + Update button. Each push now includes the live
plugin_version. wpdeploy compares it to the latestis_stable=1row in itsversionstable and only renders an Update plugin button when the site is behind. Tapping Update calls the existingPOST /wp-json/auctionforge/v1/trigger-installendpoint to force the upgrade.
Architecture
- New private helper
AuctionForge_Auto_Updater::classify_error_kind($jobId)looks up the job'sjob_typeand maps it to'upcoming'/'past'/'other'. Used by both the immediate push and the 1-min retry cron. - Outbound payloads now include
plugin_versionat the top level; wpdeploy refreshessites.plugin_versionon every/sync-errorsPOST.
Files Changed
- Modified:
auctionforge.php,inc/class-auto-updater.php
---
- 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
-
v3.6.2.0 Stable Apr 29, 2026Download v3.6.2.0📦 9.22 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.6.2.0] — 2026-04-29
Telegram Sync Monitoring & Remote Control
Features
- Instant Telegram alerts on sync errors. Any
level='error'row written toaf_sync_logis 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=importBidSpiritUpcomingflow 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 viaUNIQUE(site_id, af_log_id)so duplicates are silent.
Architecture
SyncLogger::log()now firesdo_action('auctionforge_sync_error_logged', …)onlevel='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) — runsSyncQueue::cancelAll()+ SIGTERM + watchdog. Logs every action throughSyncLogger. POST /sync/startextended with amodeparameter (upcomingdefault,pastfor archived). Bulk re-sync is now a queued job, not a synchronous block.- New job types
full_sync_upcomingandfull_sync_pastonSyncQueue, dispatched to a newBulkSyncWorkerthat wraps the existingAuctionList::init()+LotList::init($upcoming)pipeline. syncWorker.phpinstalls a SIGTERM handler that flips the in-flight job tofailedwith 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
---
- Instant Telegram alerts on sync errors. Any
-
v3.6.1.6 Stable Apr 29, 2026Download v3.6.1.6📦 9.22 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.6.1.6] — 2026-04-29
High-Value Lot Modes for
[bp_lot_list]Features
high_value_pastmode — Displays past auction lots ordered by their catalog estimate (estimatedPriceInt), highest first. Useful for "Top Lots" or "Highlights" sections.high_value_soldmode — 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 asrandom_pastto skip lots wheresoldBidwas carried over fromstartPricewithout real bidding activity.- Admin documentation updated — The Shortcodes reference page in WP Admin now lists both new modes in the
modeattribute description and includes example shortcode usage.
Architecture
- New constants
MODE_HIGH_VALUE_PASTandMODE_HIGH_VALUE_SOLDonLotsWidget. - New method
LotsWidget::getLotListHighValue($mode, $limit)builds the query, applies the past-auction tax filter, setsbp_orderbyto the relevant meta field withDESCorder, and (for the sold variant) filters out unsold-but-stale lots in PHP. - New filter hook
bp_lot_list_high_value_args_filterfor downstream customization of the WP_Query args.
Files Changed
- Modified:
inc/widgets/LotsWidget.php,inc/controllers/ShortcodesPage.php
---
-
v3.6.1.5 Stable Apr 29, 2026Download v3.6.1.5📦 8.95 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.6.1.3 Stable Apr 29, 2026Download v3.6.1.3📦 8.95 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.6.1.2 Stable Apr 29, 2026Download v3.6.1.2📦 8.95 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.6.1.1 Stable Apr 27, 2026Download v3.6.1.1📦 8.95 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.6.1.0 Stable Apr 27, 2026Download v3.6.1.0📦 8.95 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.6.0.20 Stable Apr 26, 2026Download v3.6.0.20📦 8.95 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.6.0.19 Stable Apr 26, 2026Download v3.6.0.19📦 8.95 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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_subscriberstable 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.phpmethods:leading_bid_surpassed(),item_live_soon(),auction_starts_soon(), andauctionPublishedPush().
AJAX Handlers
auctionforge_sms_save— Save SMS preferences from profile pageauctionforge_sms_load— Load SMS preferences for profile pageauctionforge_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_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.6.0.18 Stable Apr 25, 2026Download v3.6.0.18📦 8.94 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.6.0.7] — 2026-04-14
Hotfix
Fixes
- Namespace resolution — Fixed fatal error in
wp_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Namespace resolution — Fixed fatal error in
-
v3.6.0.17 Stable Apr 23, 2026Download v3.6.0.17📦 8.94 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.6.0.7] — 2026-04-14
Hotfix
Fixes
- Namespace resolution — Fixed fatal error in
wp_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Namespace resolution — Fixed fatal error in
-
v3.6.0.16 Stable Apr 21, 2026Download v3.6.0.16📦 8.94 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.6.0.7] — 2026-04-14
Hotfix
Fixes
- Namespace resolution — Fixed fatal error in
wp_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Namespace resolution — Fixed fatal error in
-
v3.6.0.15 Stable Apr 20, 2026Download v3.6.0.15📦 8.94 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.6.0.7] — 2026-04-14
Hotfix
Fixes
- Namespace resolution — Fixed fatal error in
wp_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Namespace resolution — Fixed fatal error in
-
v3.6.0.13 Stable Apr 20, 2026Download v3.6.0.13📦 8.94 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.6.0.7] — 2026-04-14
Hotfix
Fixes
- Namespace resolution — Fixed fatal error in
wp_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Namespace resolution — Fixed fatal error in
-
v3.6.0.12 Stable Apr 18, 2026Download v3.6.0.12📦 8.94 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.6.0.7] — 2026-04-14
Hotfix
Fixes
- Namespace resolution — Fixed fatal error in
wp_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Namespace resolution — Fixed fatal error in
-
v3.6.0.10 Stable Apr 15, 2026Download v3.6.0.10📦 8.93 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.6.0.7] — 2026-04-14
Hotfix
Fixes
- Namespace resolution — Fixed fatal error in
wp_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Namespace resolution — Fixed fatal error in
-
v3.6.0.8 Stable Apr 14, 2026Download v3.6.0.8📦 8.93 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.6.0.7] — 2026-04-14
Hotfix
Fixes
- Namespace resolution — Fixed fatal error in
wp_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass 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.phptemplate intoauctionforge.phpviawp_footerhook (priority 99). Sites with theme-level template overrides (e.g.themes/*/auctionforge/single-lot.php) were not picking up the inline script. Thewp_footerapproach 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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Namespace resolution — Fixed fatal error in
-
v3.6.0.7 Stable Apr 14, 2026Download v3.6.0.7📦 8.92 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.6.0.7] — 2026-04-14
Hotfix
Fixes
- Namespace resolution — Fixed fatal error in
wp_footercommission label hook caused by unqualifiedSettingsHelperandSettingsclass references. Now uses fully qualified namespaces.
Files Changed
- Modified:
auctionforge.php
---
- Namespace resolution — Fixed fatal error in
-
v3.6.0.5 Stable Apr 14, 2026Download v3.6.0.5📦 8.92 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[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
---
-
v3.6.0.4 Stable Apr 12, 2026Download v3.6.0.4📦 8.92 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[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
---
-
v3.6.0.3 Stable Apr 11, 2026Download v3.6.0.3📦 8.92 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
-
v3.6.0.2 Stable Apr 10, 2026Download v3.6.0.2📦 8.91 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[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
---
-
v3.6.0.0 Stable Apr 10, 2026Download v3.6.0.0📦 8.89 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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
bidto the allowedevent_typevalues 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_collisionerror. 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_collisionerrors, 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-indexstacks.
---
[3.5.0.1] — 2026-04-08
Hotfix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Bid Tracking — All successful bids (regular, catalog, pre-sale buyout, post-sale purchase) now fire
-
v3.5.0.5 Stable Apr 08, 2026Download v3.5.0.5📦 9.15 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.6.1
Self-Healing Database Schema + Bid Event Tracking — Auto-migrates missing tracking columns on upgrade and tracks all user bids (regular, pre-sale buyout, post-sale purchase) in the user tracking system.
-
v3.5.0.4 Stable Apr 08, 2026Download v3.5.0.4📦 9.15 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.6.1
API Key Auto-Healing — 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.
-
v3.5.0.6 Stable Apr 08, 2026Download v3.5.0.6📦 9.15 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.6.1
Bid Event Tracking + Self-Healing Schema — All bids now tracked with full user fingerprint identity. Auto-migrates missing DB columns on upgrade.
-
v3.5.0.3 Stable Apr 08, 2026Download v3.5.0.3📦 9.15 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.6.1
API Key Collision Defense — Implemented an strict identity verification system to prevent cloned testing sites from polluting the central WPDeploy tracking network.
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.
-
v3.5.0.2 Stable Apr 08, 2026Download v3.5.0.2📦 9.15 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.2.9.2] — 2026-03-30
Timed Auction Archive Fix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
-
v3.5.0.1 Stable Apr 08, 2026Download v3.5.0.1📦 9.15 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.2.9.2] — 2026-03-30
Timed Auction Archive Fix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
-
v3.5.0.0 Stable Apr 08, 2026Download v3.5.0.0📦 9.15 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.2.9.2] — 2026-03-30
Timed Auction Archive Fix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
-
v3.4.0.0 Stable Apr 08, 2026Download v3.4.0.0📦 9.08 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.2.9.2] — 2026-03-30
Timed Auction Archive Fix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
-
v3.3.0.0 Stable Apr 08, 2026Download v3.3.0.0📦 9.08 MB⚙️ WP 6.0+🔧 PHP 8.0+✓ Tested 6.4.3
Added Central Site Statistics Proxy
Added Remote Live Auction Sync Hooks
-
v3.2.9.5 Stable Apr 02, 2026Download v3.2.9.5📦 8.82 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.2.9.2] — 2026-03-30
Timed Auction Archive Fix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
-
v3.2.9.4 Stable Apr 02, 2026Download v3.2.9.4📦 8.82 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.2.9.2] — 2026-03-30
Timed Auction Archive Fix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
-
v3.2.9.3 Stable Apr 02, 2026Download v3.2.9.3📦 8.82 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.2.9.2] — 2026-03-30
Timed Auction Archive Fix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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_wideshortcodes 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.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips 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
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
-
v3.2.9.2 Stable Mar 30, 2026Download v3.2.9.2📦 8.82 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.2.9.2] — 2026-03-30
Timed Auction Archive Fix
Fixes
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
auction_list.phpcore 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
---
- Timed Auction Badge Visibility — Fixed a strict type-matching bug in the main
-
v3.2.9.0 Stable Mar 30, 2026Download v3.2.9.0📦 8.82 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.2.9.0] — 2026-03-29
Billing & Invoices Polish
Fixes
- Profile Invoice ID Tracking — Prevented internal auction codes (e.g.
002orai4) from breaking into the clean hyphenated titles inside theprofile-templates.phpview. - Dynamic Payment Statuses — Replaced Bidspirit's default/cached
NOT PAIDHTML payloads by wiring thedompdfgenerator to directly respect the current$b.statusstring 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.
---
- Profile Invoice ID Tracking — Prevented internal auction codes (e.g.
-
v3.2.6.0 Stable Mar 28, 2026Download v3.2.6.0📦 4.85 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[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
itemIndexcolumn inbp_meta_dataisvarchar(55), but the day-range filter inBidspiritQuery::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 usingCAST(bpMeta.itemIndex AS UNSIGNED)for numeric comparison andintval()on the PHP-side boundary values. Letter lots (e.g.14A,1025A) are handled correctly —CASTstrips the suffix, keeping them grouped with their base lot number.
Files Changed
- Modified:
inc/controllers/BidspiritQuery.php
---
- Day 2+ showing lots from Day 1 in multi-day auctions — The
-
v3.2.5.9 Stable Mar 27, 2026Download v3.2.5.9📦 4.85 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.2.5.9] — 2026-03-27
Catalog Page Fix
Fixes
- Duplicate preview hours on catalog pages — The
displayHoursfield was rendered twice: once via thebp_after_description_auctionhook inBidspiritViews.phpand 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
---
- Duplicate preview hours on catalog pages — The
-
v3.2.5.8 Stable Mar 26, 2026Download v3.2.5.8📦 4.85 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- 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
-
v3.2.5.7 Stable Mar 24, 2026Download v3.2.5.7📦 4.85 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- 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
-
v3.2.5.6 Stable Mar 24, 2026Download v3.2.5.6📦 4.85 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- 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
-
v3.2.5.5 Stable Mar 24, 2026Download v3.2.5.5📦 4.85 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- 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
-
v3.2.5.4 Stable Mar 24, 2026Download v3.2.5.4📦 4.85 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- 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
-
v3.2.5.2 Stable Mar 23, 2026Download v3.2.5.2📦 4.85 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- 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
-
v3.2.5.1 Stable Mar 23, 2026Download v3.2.5.1📦 4.85 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- 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
-
v3.2.5 Stable Mar 19, 2026Download v3.2.5📦 5.07 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[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.comsubdomains and BidSpirit image CDNs (fastly.net,cloudfront.net). Previously onlybidspirit.comandwww.bidspirit.comwere 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 keepssyncFinished=0so 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 intofixResult()to keep the lot retryable. - Hourly orphan detection in safety cron — New step in
syncSafety.phpdetects lot posts that exist inbp_meta_databut are missing their auction taxonomy assignment (caused by workers killed mid-batch) and auto-repairs them withwp_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
---
- 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
-
v3.2.4.1 Stable Mar 19, 2026Download v3.2.4.1📦 4.84 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob 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_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Sync workers hanging at InsertLotsBS stage — Third-party plugins (Smush Pro, SmartCrawl SEO, WPMU DEV Updates) hook into
-
v3.2.4 Stable Mar 18, 2026Download v3.2.4📦 5.07 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[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). AddedWP_IMPORTINGconstant at the REST worker entry point and apre_http_requestfilter inCatalogSyncWorkerthat 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_historywere left withsyncstart=1and never unblocked. Subsequent sync runs could not see these rows (filtered out byLot::getRawLots()), causing the InsertLotsBS loop to exit immediately with 0 posts processed. Combined with job deduplication (SyncQueue::enqueue()skips if aprocessingjob exists), this created a deadlock where no new sync could proceed.
Files Changed
- Modified:
auctionforge.php,inc/sync/CatalogSyncWorker.php
---
- Sync workers hanging at InsertLotsBS stage — Third-party plugins (Smush Pro, SmartCrawl SEO, WPMU DEV Updates) hook into
-
v3.2.3.1 Stable Mar 18, 2026Download v3.2.3.1📦 4.85 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
AuctionForge Changelog
All notable changes to the AuctionForge plugin are documented in this file.
---
[3.2.2] — 2026-03-17
Full Sync Auction Discovery Fix
Fixes
- Full Sync finds no auctions on fresh sites — The v3
full_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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 existingLotList::singleRun()logic. - Item Update Worker (
inc/sync/ItemUpdateWorker.php) — Processes individual item_update jobs with the same logic as the webhookitemsUpdatedhandler. - Background Worker (
crontasks/syncWorker.php) — Async CLI process spawned by webhooks viaexec()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_logevery 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
migratecommand 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 consoleauctionforge_sync_queue_status— Queue counts + recent jobs for dashboardauctionforge_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 inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js.
[2.7.1.1]
Fixes
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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.phprequire 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 onaf_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.
- Full Sync finds no auctions on fresh sites — The v3
-
v3.2.2 Stable Mar 17, 2026Download v3.2.2📦 5.07 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.2.2] — 2026-03-17
Full Sync Auction Discovery Fix
Fixes
- Full Sync finds no auctions on fresh sites — The v3
full_syncaction only queried the WordPress database for existing auction terms, but on a freshly deployed site the database has zero auctions. AddedAuctionList::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
---
- Full Sync finds no auctions on fresh sites — The v3
-
v3.2.1 Stable Mar 16, 2026Download v3.2.1📦 5.07 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.2.1] — 2026-03-16
Sync Worker Fix & Lot Analytics Default
Fixes
- Critical: Autocommit rollback bug —
InsertLotsBS::init()setsautocommit=0(whenAUTO_COMMIT_DISABLEDis enabled) but never restores it. After the InsertLotsBS processing loop, all subsequent DB writes —SyncLogger::log(),SyncQueue::complete()— were in an uncommitted transaction that got silently rolled back when the PHP-FPM process exited. This caused sync jobs to appear permanently stuck in "processing" status despite actually completing. Fixed by addingCOMMIT; SET autocommit = 1;after the InsertLotsBS loop in bothCatalogSyncWorker::process()andprocessDayEnded(). - Lot analytics enabled by default — Changed the fallback default for
enable_lot_analyticsfrom'off'to'on'across all code paths (settings, templates, AJAX handlers, shortcodes, stats tracker). New installs now have lot analytics active without requiring manual configuration.
Files Changed
- Modified:
inc/sync/CatalogSyncWorker.php,inc/import/InsertLotsBS.php,inc/controllers/Settings.php,inc/controllers/BidspiritViews.php,templates/single-lot.php,admin/view/stats/dashboard.php,inc/stats/class-stats-ajax.php,inc/stats/class-stats-tracker.php,inc/shortcodes/PopularLots.php,ajax/LotAnalytics.php
---
- Critical: Autocommit rollback bug —
-
v3.2.0 Stable Mar 14, 2026Download v3.2.0📦 5.07 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[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(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring 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
---
-
v3.1.0 Stable Mar 14, 2026Download v3.1.0📦 5.07 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
[3.1.0] — 2026-03-14
Webhook Pipeline Fixes & Archive Sync
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.
Fixes
- Dual-write removed — Removed legacy handlers from
auctionCatalogUpdated(),auctionDayEnded(), anditemsUpdated()webhook methods. The old handlers were racing with the v3 worker, creating orphaned staging rows inimport_historythat were never processed. All three methods now exclusively use the v3 enqueue → spawn path. - ITEMS_UPDATED webhook — Fixed
itemsUpdated()failing to enqueue jobs. TheITEMS_UPDATEDpayload hasauctionIdnested initemsByLanguage.{lang}[0].item.auctionId, not at the top level. Added extraction logic to resolve it. - ItemUpdateWorker incomplete —
ItemUpdateWorker::process()was only callingLot::bulkSave()(staging table) without runningInsertLotsBSto process staged data into WP posts andbp_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.1with explicitHostheader, 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_TOKENinwp-config.php(renamed fromUCO_API_TOKENduring the plugin rename). Documented in readme.
Files Changed
- Modified:
inc/controllers/AuctionsAPI.php,inc/sync/ItemUpdateWorker.php,inc/sync/SyncQueue.php,auctionforge.php,admin/view/sync-status.php
---
-
v2.7.3.0 Stable Mar 14, 2026Download v2.7.3.0📦 4.82 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.1.2
Fixes:
- Added missing
vatAndCommission()function inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js. Also added missingsetAuctionItemInfo()call.
2.7.1.1
Fixes:
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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 — added missing
auction-commission-fieldpopulation inauctionInfoInit()from upstream reference.
2.7.0.9
Fixes:
- Fixed fatal error during plugin install/upgrade:
class-stats-database.phprequire failure. Addedfile_existsguard inAF_Stats_Init::activate()and plugin-specific guard inupgrader_process_completehook to prevent running upgrade logic for unrelated plugin updates.
2.7.0.8
Fixes:
- Synced
formattedPrice()across bothapp.jsandapp.min.js— added second currency support, better null/NaN handling, and always 2 decimal places on all prices. Fixes sites where minification plugins may loadapp.min.jsinstead ofapp.js.
2.7.0.7
Fixes:
- All prices now display with 2 decimal places (e.g. $10.00 instead of $10) via
formattedPrice()usingtoLocaleStringwithminimumFractionDigits: 2.
2.7.0.6
Fixes:
- Added missing
estimatedPrice,content, andisShoptemplate variables to widget and shortcodelot-content-templatecalls, preventing potential ReferenceErrors on catalogue pages with ended auctions or shop items.
2.7.0.5
Fixes:
- Fixed Stripe payment form not visible in Add Payment Method modal —
#stripePaymentElementhadhiddenclass that was never removed. Now unhides form on Stripereadyevent and re-hides on modal close.
2.7.0.4
Fixes:
- Fixed
bidspiritApi.getAuctionSettingsData is not a functionerror in getFullPriceInfo — use local auctionSettingsData variable instead of non-existent API method.
2.7.0.3
Fixes:
- Fixed
fullPriceInfo is not definedReferenceError that prevented bid confirmation modal from opening on single lot pages and catalogue pages. - Added missing
getFullPriceInfo(),getCommissonInfoForPrice(), androundToCents()functions ported from upstream. - All three bid submission paths (submitBid, submitBidCatalog, submitBidCatalog3) now correctly pass fullPriceInfo to the modal-bid-template.
2.7.0.0
New Features: Advanced Lot Analytics
- Conversion Tracking — View-to-bid ratio table showing lot views, catalogue/external clicks, favorites, shares, bid status, and sold price with sell-through rate summary cards
- Peak Viewing Times — Hourly bar chart showing when lots get the most views (last 30 days) with peak hour identification
- Pre-Sale vs Live Comparison — Per-auction breakdown of views before auction day vs during/after, with pre-sale percentage
- Category Performance — Analytics grouped by lot category: views, lots, favorites, sold count, average views per lot
- Share Analytics — Track shares by channel (Facebook, Twitter, WhatsApp, Email, Pinterest, Copy Link) with per-channel badges and most-shared lots table
- Search → View Funnel — Correlates internal search queries with subsequent lot page views to show which searches lead to browsing
- Click-Through Tracking — Distinguishes catalogue clicks (user came from auction catalogue) vs external clicks (direct/search/referral)
- CSV Export — One-click export of all lot analytics data, filterable by catalogue
- Hot Lot Badge — Automatic "🔥 Popular" badge on catalogue page thumbnails for lots exceeding view threshold (default: 5 views)
- All new panels respect the catalogue filter dropdown
- New DB table: af_lot_shares (per-channel share events)
- New columns: catalogue_clicks, external_clicks, share_count on af_lot_analytics
- DB version bumped to 20
2.6.3.0
Fixes:
- View counter now shows on lot pages even with 0 views
- Fixed backfill and trending queries failing on URLs with trailing slashes
2.6.2.0
Fixes:
- Fixed 404 errors on auction/lot pages after DB version update by flushing rewrite rules automatically
2.6.1.0
Fixes:
- Renamed shortcodes to use bp_ prefix for consistency: [bp_popular_lots], [bp_most_favorited], [bp_trending_lots]
2.6.0.0
New Feature: Lot Analytics
- Added comprehensive lot view and favorite tracking system
- Two new settings in AuctionForge > Settings (both disabled by default):
- Enable Lot Analytics — master toggle for the entire tracking system
- Show View Counter on Lots — display a public view count on each lot page
- New admin dashboard tab: Lot Analytics (under Site Statistics) with:
- Auction Performance comparison table
- Most Viewed Lots (all time)
- Most Favorited Lots
- Trending Lots (last 7 days)
- Backfill from existing pageview data button
- Local favorites tracking mirrors BidSpirit favorites for analytics
- Three new shortcodes for displaying popular lots:
- — most viewed lots grid
- — most favorited lots grid
- — trending lots (configurable time window)
- All shortcodes support limit, columns, auction_id, and period filtering
- New DB tables: af_lot_analytics, af_lot_favorites (DB version 19)
2.5.9.0
Improvements:
- Redesigned Recently Viewed items as square card grid (rows/columns) instead of horizontal list.
- Changed lot thumbnail to contain (show full image) instead of cover (crop to fill).
2.5.8.0
Fixes:
- Fixed Recently Viewed cloned image thumbnails using non-existent resize prefix. Now uses raw image filename for reliable CDN loading.
2.5.7.0
Fixes:
- Fixed Recently Viewed thumbnail URLs — now builds correct BidSpirit CDN URLs for both cloned (Fastly) and non-cloned (Cloudinary) images instead of broken relative paths.
2.5.6.0
Fixes:
- Fixed placeholder image fallback path still referencing old uco-auctions directory.
- Added background color for empty thumbnail containers in Recently Viewed.
2.5.5.0
Improvements:
- Removed PAST/Upcoming badges from Recently Viewed items display.
- Added lot thumbnail images to Recently Viewed items with proper card layout.
2.5.4.0
Fixes:
- Fixed item variable not being passed to auctionIsLive-template (was lost during previous file edits).
2.5.3.0
Fixes:
- Fixed
item is not definedindisplayBidFormData— removed strayitem: itemfrom house-info-template call where item variable does not exist in scope.
2.5.2.0
Fixes:
- Fixed
item is not definedJavaScript error inauctionIsLive-templateon single lot pages. - Fixed statistics table auto-creation running on frontend requests (restricted to admin only).
2.5.1.0
Fixes:
- Fixed
hideleadingBidSection is not definedJavaScript error on single lot pages. - Fixed statistics database tables not auto-creating on plugin update (only activated on fresh install).
- Added self-healing table creation on
plugins_loadedfor sites that update without re-activating. - Cleaned up duplicate
AF_Stats_Init::activate()calls in DB update function. - Bumped DB version to 18.
2.5.0.0
New Features:
- Site Statistics System — Full website analytics built into the plugin admin (Lots → Site Statistics).
- Dashboard with KPI cards: visitors, sessions, pageviews, bounce rate, avg duration, pages per session.
- Period comparison: current vs previous period with percentage change indicators.
- Traffic over time line chart (pageviews, visitors, sessions).
- Traffic channels doughnut chart (organic search, direct, referral, social, email, paid).
- Hourly distribution bar chart.
- Device type pie chart (desktop, mobile, tablet).
- Top Pages tab with views, unique visitors, avg time on page, search, pagination, CSV export.
- Referrers tab with traffic channel badges, sessions, bounce rate, avg duration, CSV export.
- Countries tab with horizontal bar chart and visitor/pageview counts.
- Technology tab with browser, OS, device breakdown and screen resolutions.
- Searches tab tracking internal WordPress and auction search queries.
- 404 Errors tab tracking broken URLs with hit counts and referrers.
- Real-Time visitors view (last 5 minutes, auto-refreshes every 10 seconds).
- Settings tab: enable/disable tracking, exclude IPs and roles, IP anonymization (GDPR), data retention.
- Date range picker with presets (7, 14, 30, 90, 365 days) and custom range.
- Lightweight frontend tracker (~3KB JS) with session management, heartbeat, and beacon API.
- GeoIP country/city lookup via ip-api.com with 24-hour cache.
- Bot detection (30+ crawler patterns filtered).
- UTM parameter tracking (source, medium, campaign, term, content).
- Traffic channel auto-classification.
- Outbound link click tracking.
- Time-on-page tracking via 30-second heartbeat + page unload beacon.
- Daily cron for automatic data retention purge.
- Recently Viewed Items — New profile page tab showing the last 20 viewed lots per user.
- Admin setting to enable/disable (Lots → Settings → Enable user viewing history).
- Tracks lot views with timestamp, stores in custom DB table.
- Clear history button for users.
- Disabled by default.
Fixes:
- Fixed
settings is not definedJavaScript error on auction catalog and single lot pages. - Fixed
postAuctionSaleOpened is not definedJavaScript error on auction pages. - Fixed missing
vendor.min.cssandpassword-validation.min.css(404 errors). - Added missing template variables to all
loadTemplatecalls forlot-content-template,leadingBid-template.
2.4.4.0
Fixes:
- Fixed the display of purchase prices on shop pages for unsold items. Items with a buyout price now correctly show the "Purchase Price" in both ended and active auction states.
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added missing
-
v2.7.2.9 Stable Mar 14, 2026Download v2.7.2.9📦 4.82 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.1.2
Fixes:
- Added missing
vatAndCommission()function inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js. Also added missingsetAuctionItemInfo()call.
2.7.1.1
Fixes:
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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 — added missing
auction-commission-fieldpopulation inauctionInfoInit()from upstream reference.
2.7.0.9
Fixes:
- Fixed fatal error during plugin install/upgrade:
class-stats-database.phprequire failure. Addedfile_existsguard inAF_Stats_Init::activate()and plugin-specific guard inupgrader_process_completehook to prevent running upgrade logic for unrelated plugin updates.
2.7.0.8
Fixes:
- Synced
formattedPrice()across bothapp.jsandapp.min.js— added second currency support, better null/NaN handling, and always 2 decimal places on all prices. Fixes sites where minification plugins may loadapp.min.jsinstead ofapp.js.
2.7.0.7
Fixes:
- All prices now display with 2 decimal places (e.g. $10.00 instead of $10) via
formattedPrice()usingtoLocaleStringwithminimumFractionDigits: 2.
2.7.0.6
Fixes:
- Added missing
estimatedPrice,content, andisShoptemplate variables to widget and shortcodelot-content-templatecalls, preventing potential ReferenceErrors on catalogue pages with ended auctions or shop items.
2.7.0.5
Fixes:
- Fixed Stripe payment form not visible in Add Payment Method modal —
#stripePaymentElementhadhiddenclass that was never removed. Now unhides form on Stripereadyevent and re-hides on modal close.
2.7.0.4
Fixes:
- Fixed
bidspiritApi.getAuctionSettingsData is not a functionerror in getFullPriceInfo — use local auctionSettingsData variable instead of non-existent API method.
2.7.0.3
Fixes:
- Fixed
fullPriceInfo is not definedReferenceError that prevented bid confirmation modal from opening on single lot pages and catalogue pages. - Added missing
getFullPriceInfo(),getCommissonInfoForPrice(), androundToCents()functions ported from upstream. - All three bid submission paths (submitBid, submitBidCatalog, submitBidCatalog3) now correctly pass fullPriceInfo to the modal-bid-template.
2.7.0.0
New Features: Advanced Lot Analytics
- Conversion Tracking — View-to-bid ratio table showing lot views, catalogue/external clicks, favorites, shares, bid status, and sold price with sell-through rate summary cards
- Peak Viewing Times — Hourly bar chart showing when lots get the most views (last 30 days) with peak hour identification
- Pre-Sale vs Live Comparison — Per-auction breakdown of views before auction day vs during/after, with pre-sale percentage
- Category Performance — Analytics grouped by lot category: views, lots, favorites, sold count, average views per lot
- Share Analytics — Track shares by channel (Facebook, Twitter, WhatsApp, Email, Pinterest, Copy Link) with per-channel badges and most-shared lots table
- Search → View Funnel — Correlates internal search queries with subsequent lot page views to show which searches lead to browsing
- Click-Through Tracking — Distinguishes catalogue clicks (user came from auction catalogue) vs external clicks (direct/search/referral)
- CSV Export — One-click export of all lot analytics data, filterable by catalogue
- Hot Lot Badge — Automatic "🔥 Popular" badge on catalogue page thumbnails for lots exceeding view threshold (default: 5 views)
- All new panels respect the catalogue filter dropdown
- New DB table: af_lot_shares (per-channel share events)
- New columns: catalogue_clicks, external_clicks, share_count on af_lot_analytics
- DB version bumped to 20
2.6.3.0
Fixes:
- View counter now shows on lot pages even with 0 views
- Fixed backfill and trending queries failing on URLs with trailing slashes
2.6.2.0
Fixes:
- Fixed 404 errors on auction/lot pages after DB version update by flushing rewrite rules automatically
2.6.1.0
Fixes:
- Renamed shortcodes to use bp_ prefix for consistency: [bp_popular_lots], [bp_most_favorited], [bp_trending_lots]
2.6.0.0
New Feature: Lot Analytics
- Added comprehensive lot view and favorite tracking system
- Two new settings in AuctionForge > Settings (both disabled by default):
- Enable Lot Analytics — master toggle for the entire tracking system
- Show View Counter on Lots — display a public view count on each lot page
- New admin dashboard tab: Lot Analytics (under Site Statistics) with:
- Auction Performance comparison table
- Most Viewed Lots (all time)
- Most Favorited Lots
- Trending Lots (last 7 days)
- Backfill from existing pageview data button
- Local favorites tracking mirrors BidSpirit favorites for analytics
- Three new shortcodes for displaying popular lots:
- — most viewed lots grid
- — most favorited lots grid
- — trending lots (configurable time window)
- All shortcodes support limit, columns, auction_id, and period filtering
- New DB tables: af_lot_analytics, af_lot_favorites (DB version 19)
2.5.9.0
Improvements:
- Redesigned Recently Viewed items as square card grid (rows/columns) instead of horizontal list.
- Changed lot thumbnail to contain (show full image) instead of cover (crop to fill).
2.5.8.0
Fixes:
- Fixed Recently Viewed cloned image thumbnails using non-existent resize prefix. Now uses raw image filename for reliable CDN loading.
2.5.7.0
Fixes:
- Fixed Recently Viewed thumbnail URLs — now builds correct BidSpirit CDN URLs for both cloned (Fastly) and non-cloned (Cloudinary) images instead of broken relative paths.
2.5.6.0
Fixes:
- Fixed placeholder image fallback path still referencing old uco-auctions directory.
- Added background color for empty thumbnail containers in Recently Viewed.
2.5.5.0
Improvements:
- Removed PAST/Upcoming badges from Recently Viewed items display.
- Added lot thumbnail images to Recently Viewed items with proper card layout.
2.5.4.0
Fixes:
- Fixed item variable not being passed to auctionIsLive-template (was lost during previous file edits).
2.5.3.0
Fixes:
- Fixed
item is not definedindisplayBidFormData— removed strayitem: itemfrom house-info-template call where item variable does not exist in scope.
2.5.2.0
Fixes:
- Fixed
item is not definedJavaScript error inauctionIsLive-templateon single lot pages. - Fixed statistics table auto-creation running on frontend requests (restricted to admin only).
2.5.1.0
Fixes:
- Fixed
hideleadingBidSection is not definedJavaScript error on single lot pages. - Fixed statistics database tables not auto-creating on plugin update (only activated on fresh install).
- Added self-healing table creation on
plugins_loadedfor sites that update without re-activating. - Cleaned up duplicate
AF_Stats_Init::activate()calls in DB update function. - Bumped DB version to 18.
2.5.0.0
New Features:
- Site Statistics System — Full website analytics built into the plugin admin (Lots → Site Statistics).
- Dashboard with KPI cards: visitors, sessions, pageviews, bounce rate, avg duration, pages per session.
- Period comparison: current vs previous period with percentage change indicators.
- Traffic over time line chart (pageviews, visitors, sessions).
- Traffic channels doughnut chart (organic search, direct, referral, social, email, paid).
- Hourly distribution bar chart.
- Device type pie chart (desktop, mobile, tablet).
- Top Pages tab with views, unique visitors, avg time on page, search, pagination, CSV export.
- Referrers tab with traffic channel badges, sessions, bounce rate, avg duration, CSV export.
- Countries tab with horizontal bar chart and visitor/pageview counts.
- Technology tab with browser, OS, device breakdown and screen resolutions.
- Searches tab tracking internal WordPress and auction search queries.
- 404 Errors tab tracking broken URLs with hit counts and referrers.
- Real-Time visitors view (last 5 minutes, auto-refreshes every 10 seconds).
- Settings tab: enable/disable tracking, exclude IPs and roles, IP anonymization (GDPR), data retention.
- Date range picker with presets (7, 14, 30, 90, 365 days) and custom range.
- Lightweight frontend tracker (~3KB JS) with session management, heartbeat, and beacon API.
- GeoIP country/city lookup via ip-api.com with 24-hour cache.
- Bot detection (30+ crawler patterns filtered).
- UTM parameter tracking (source, medium, campaign, term, content).
- Traffic channel auto-classification.
- Outbound link click tracking.
- Time-on-page tracking via 30-second heartbeat + page unload beacon.
- Daily cron for automatic data retention purge.
- Recently Viewed Items — New profile page tab showing the last 20 viewed lots per user.
- Admin setting to enable/disable (Lots → Settings → Enable user viewing history).
- Tracks lot views with timestamp, stores in custom DB table.
- Clear history button for users.
- Disabled by default.
Fixes:
- Fixed
settings is not definedJavaScript error on auction catalog and single lot pages. - Fixed
postAuctionSaleOpened is not definedJavaScript error on auction pages. - Fixed missing
vendor.min.cssandpassword-validation.min.css(404 errors). - Added missing template variables to all
loadTemplatecalls forlot-content-template,leadingBid-template.
2.4.4.0
Fixes:
- Fixed the display of purchase prices on shop pages for unsold items. Items with a buyout price now correctly show the "Purchase Price" in both ended and active auction states.
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added missing
-
v2.7.2.7 Stable Mar 13, 2026Download v2.7.2.7📦 4.82 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.1.2
Fixes:
- Added missing
vatAndCommission()function inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js. Also added missingsetAuctionItemInfo()call.
2.7.1.1
Fixes:
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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 — added missing
auction-commission-fieldpopulation inauctionInfoInit()from upstream reference.
2.7.0.9
Fixes:
- Fixed fatal error during plugin install/upgrade:
class-stats-database.phprequire failure. Addedfile_existsguard inAF_Stats_Init::activate()and plugin-specific guard inupgrader_process_completehook to prevent running upgrade logic for unrelated plugin updates.
2.7.0.8
Fixes:
- Synced
formattedPrice()across bothapp.jsandapp.min.js— added second currency support, better null/NaN handling, and always 2 decimal places on all prices. Fixes sites where minification plugins may loadapp.min.jsinstead ofapp.js.
2.7.0.7
Fixes:
- All prices now display with 2 decimal places (e.g. $10.00 instead of $10) via
formattedPrice()usingtoLocaleStringwithminimumFractionDigits: 2.
2.7.0.6
Fixes:
- Added missing
estimatedPrice,content, andisShoptemplate variables to widget and shortcodelot-content-templatecalls, preventing potential ReferenceErrors on catalogue pages with ended auctions or shop items.
2.7.0.5
Fixes:
- Fixed Stripe payment form not visible in Add Payment Method modal —
#stripePaymentElementhadhiddenclass that was never removed. Now unhides form on Stripereadyevent and re-hides on modal close.
2.7.0.4
Fixes:
- Fixed
bidspiritApi.getAuctionSettingsData is not a functionerror in getFullPriceInfo — use local auctionSettingsData variable instead of non-existent API method.
2.7.0.3
Fixes:
- Fixed
fullPriceInfo is not definedReferenceError that prevented bid confirmation modal from opening on single lot pages and catalogue pages. - Added missing
getFullPriceInfo(),getCommissonInfoForPrice(), androundToCents()functions ported from upstream. - All three bid submission paths (submitBid, submitBidCatalog, submitBidCatalog3) now correctly pass fullPriceInfo to the modal-bid-template.
2.7.0.0
New Features: Advanced Lot Analytics
- Conversion Tracking — View-to-bid ratio table showing lot views, catalogue/external clicks, favorites, shares, bid status, and sold price with sell-through rate summary cards
- Peak Viewing Times — Hourly bar chart showing when lots get the most views (last 30 days) with peak hour identification
- Pre-Sale vs Live Comparison — Per-auction breakdown of views before auction day vs during/after, with pre-sale percentage
- Category Performance — Analytics grouped by lot category: views, lots, favorites, sold count, average views per lot
- Share Analytics — Track shares by channel (Facebook, Twitter, WhatsApp, Email, Pinterest, Copy Link) with per-channel badges and most-shared lots table
- Search → View Funnel — Correlates internal search queries with subsequent lot page views to show which searches lead to browsing
- Click-Through Tracking — Distinguishes catalogue clicks (user came from auction catalogue) vs external clicks (direct/search/referral)
- CSV Export — One-click export of all lot analytics data, filterable by catalogue
- Hot Lot Badge — Automatic "🔥 Popular" badge on catalogue page thumbnails for lots exceeding view threshold (default: 5 views)
- All new panels respect the catalogue filter dropdown
- New DB table: af_lot_shares (per-channel share events)
- New columns: catalogue_clicks, external_clicks, share_count on af_lot_analytics
- DB version bumped to 20
2.6.3.0
Fixes:
- View counter now shows on lot pages even with 0 views
- Fixed backfill and trending queries failing on URLs with trailing slashes
2.6.2.0
Fixes:
- Fixed 404 errors on auction/lot pages after DB version update by flushing rewrite rules automatically
2.6.1.0
Fixes:
- Renamed shortcodes to use bp_ prefix for consistency: [bp_popular_lots], [bp_most_favorited], [bp_trending_lots]
2.6.0.0
New Feature: Lot Analytics
- Added comprehensive lot view and favorite tracking system
- Two new settings in AuctionForge > Settings (both disabled by default):
- Enable Lot Analytics — master toggle for the entire tracking system
- Show View Counter on Lots — display a public view count on each lot page
- New admin dashboard tab: Lot Analytics (under Site Statistics) with:
- Auction Performance comparison table
- Most Viewed Lots (all time)
- Most Favorited Lots
- Trending Lots (last 7 days)
- Backfill from existing pageview data button
- Local favorites tracking mirrors BidSpirit favorites for analytics
- Three new shortcodes for displaying popular lots:
- — most viewed lots grid
- — most favorited lots grid
- — trending lots (configurable time window)
- All shortcodes support limit, columns, auction_id, and period filtering
- New DB tables: af_lot_analytics, af_lot_favorites (DB version 19)
2.5.9.0
Improvements:
- Redesigned Recently Viewed items as square card grid (rows/columns) instead of horizontal list.
- Changed lot thumbnail to contain (show full image) instead of cover (crop to fill).
2.5.8.0
Fixes:
- Fixed Recently Viewed cloned image thumbnails using non-existent resize prefix. Now uses raw image filename for reliable CDN loading.
2.5.7.0
Fixes:
- Fixed Recently Viewed thumbnail URLs — now builds correct BidSpirit CDN URLs for both cloned (Fastly) and non-cloned (Cloudinary) images instead of broken relative paths.
2.5.6.0
Fixes:
- Fixed placeholder image fallback path still referencing old uco-auctions directory.
- Added background color for empty thumbnail containers in Recently Viewed.
2.5.5.0
Improvements:
- Removed PAST/Upcoming badges from Recently Viewed items display.
- Added lot thumbnail images to Recently Viewed items with proper card layout.
2.5.4.0
Fixes:
- Fixed item variable not being passed to auctionIsLive-template (was lost during previous file edits).
2.5.3.0
Fixes:
- Fixed
item is not definedindisplayBidFormData— removed strayitem: itemfrom house-info-template call where item variable does not exist in scope.
2.5.2.0
Fixes:
- Fixed
item is not definedJavaScript error inauctionIsLive-templateon single lot pages. - Fixed statistics table auto-creation running on frontend requests (restricted to admin only).
2.5.1.0
Fixes:
- Fixed
hideleadingBidSection is not definedJavaScript error on single lot pages. - Fixed statistics database tables not auto-creating on plugin update (only activated on fresh install).
- Added self-healing table creation on
plugins_loadedfor sites that update without re-activating. - Cleaned up duplicate
AF_Stats_Init::activate()calls in DB update function. - Bumped DB version to 18.
2.5.0.0
New Features:
- Site Statistics System — Full website analytics built into the plugin admin (Lots → Site Statistics).
- Dashboard with KPI cards: visitors, sessions, pageviews, bounce rate, avg duration, pages per session.
- Period comparison: current vs previous period with percentage change indicators.
- Traffic over time line chart (pageviews, visitors, sessions).
- Traffic channels doughnut chart (organic search, direct, referral, social, email, paid).
- Hourly distribution bar chart.
- Device type pie chart (desktop, mobile, tablet).
- Top Pages tab with views, unique visitors, avg time on page, search, pagination, CSV export.
- Referrers tab with traffic channel badges, sessions, bounce rate, avg duration, CSV export.
- Countries tab with horizontal bar chart and visitor/pageview counts.
- Technology tab with browser, OS, device breakdown and screen resolutions.
- Searches tab tracking internal WordPress and auction search queries.
- 404 Errors tab tracking broken URLs with hit counts and referrers.
- Real-Time visitors view (last 5 minutes, auto-refreshes every 10 seconds).
- Settings tab: enable/disable tracking, exclude IPs and roles, IP anonymization (GDPR), data retention.
- Date range picker with presets (7, 14, 30, 90, 365 days) and custom range.
- Lightweight frontend tracker (~3KB JS) with session management, heartbeat, and beacon API.
- GeoIP country/city lookup via ip-api.com with 24-hour cache.
- Bot detection (30+ crawler patterns filtered).
- UTM parameter tracking (source, medium, campaign, term, content).
- Traffic channel auto-classification.
- Outbound link click tracking.
- Time-on-page tracking via 30-second heartbeat + page unload beacon.
- Daily cron for automatic data retention purge.
- Recently Viewed Items — New profile page tab showing the last 20 viewed lots per user.
- Admin setting to enable/disable (Lots → Settings → Enable user viewing history).
- Tracks lot views with timestamp, stores in custom DB table.
- Clear history button for users.
- Disabled by default.
Fixes:
- Fixed
settings is not definedJavaScript error on auction catalog and single lot pages. - Fixed
postAuctionSaleOpened is not definedJavaScript error on auction pages. - Fixed missing
vendor.min.cssandpassword-validation.min.css(404 errors). - Added missing template variables to all
loadTemplatecalls forlot-content-template,leadingBid-template.
2.4.4.0
Fixes:
- Fixed the display of purchase prices on shop pages for unsold items. Items with a buyout price now correctly show the "Purchase Price" in both ended and active auction states.
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added missing
-
v2.7.2.6 Stable Mar 13, 2026Download v2.7.2.6📦 4.82 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.1.2
Fixes:
- Added missing
vatAndCommission()function inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js. Also added missingsetAuctionItemInfo()call.
2.7.1.1
Fixes:
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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 — added missing
auction-commission-fieldpopulation inauctionInfoInit()from upstream reference.
2.7.0.9
Fixes:
- Fixed fatal error during plugin install/upgrade:
class-stats-database.phprequire failure. Addedfile_existsguard inAF_Stats_Init::activate()and plugin-specific guard inupgrader_process_completehook to prevent running upgrade logic for unrelated plugin updates.
2.7.0.8
Fixes:
- Synced
formattedPrice()across bothapp.jsandapp.min.js— added second currency support, better null/NaN handling, and always 2 decimal places on all prices. Fixes sites where minification plugins may loadapp.min.jsinstead ofapp.js.
2.7.0.7
Fixes:
- All prices now display with 2 decimal places (e.g. $10.00 instead of $10) via
formattedPrice()usingtoLocaleStringwithminimumFractionDigits: 2.
2.7.0.6
Fixes:
- Added missing
estimatedPrice,content, andisShoptemplate variables to widget and shortcodelot-content-templatecalls, preventing potential ReferenceErrors on catalogue pages with ended auctions or shop items.
2.7.0.5
Fixes:
- Fixed Stripe payment form not visible in Add Payment Method modal —
#stripePaymentElementhadhiddenclass that was never removed. Now unhides form on Stripereadyevent and re-hides on modal close.
2.7.0.4
Fixes:
- Fixed
bidspiritApi.getAuctionSettingsData is not a functionerror in getFullPriceInfo — use local auctionSettingsData variable instead of non-existent API method.
2.7.0.3
Fixes:
- Fixed
fullPriceInfo is not definedReferenceError that prevented bid confirmation modal from opening on single lot pages and catalogue pages. - Added missing
getFullPriceInfo(),getCommissonInfoForPrice(), androundToCents()functions ported from upstream. - All three bid submission paths (submitBid, submitBidCatalog, submitBidCatalog3) now correctly pass fullPriceInfo to the modal-bid-template.
2.7.0.0
New Features: Advanced Lot Analytics
- Conversion Tracking — View-to-bid ratio table showing lot views, catalogue/external clicks, favorites, shares, bid status, and sold price with sell-through rate summary cards
- Peak Viewing Times — Hourly bar chart showing when lots get the most views (last 30 days) with peak hour identification
- Pre-Sale vs Live Comparison — Per-auction breakdown of views before auction day vs during/after, with pre-sale percentage
- Category Performance — Analytics grouped by lot category: views, lots, favorites, sold count, average views per lot
- Share Analytics — Track shares by channel (Facebook, Twitter, WhatsApp, Email, Pinterest, Copy Link) with per-channel badges and most-shared lots table
- Search → View Funnel — Correlates internal search queries with subsequent lot page views to show which searches lead to browsing
- Click-Through Tracking — Distinguishes catalogue clicks (user came from auction catalogue) vs external clicks (direct/search/referral)
- CSV Export — One-click export of all lot analytics data, filterable by catalogue
- Hot Lot Badge — Automatic "🔥 Popular" badge on catalogue page thumbnails for lots exceeding view threshold (default: 5 views)
- All new panels respect the catalogue filter dropdown
- New DB table: af_lot_shares (per-channel share events)
- New columns: catalogue_clicks, external_clicks, share_count on af_lot_analytics
- DB version bumped to 20
2.6.3.0
Fixes:
- View counter now shows on lot pages even with 0 views
- Fixed backfill and trending queries failing on URLs with trailing slashes
2.6.2.0
Fixes:
- Fixed 404 errors on auction/lot pages after DB version update by flushing rewrite rules automatically
2.6.1.0
Fixes:
- Renamed shortcodes to use bp_ prefix for consistency: [bp_popular_lots], [bp_most_favorited], [bp_trending_lots]
2.6.0.0
New Feature: Lot Analytics
- Added comprehensive lot view and favorite tracking system
- Two new settings in AuctionForge > Settings (both disabled by default):
- Enable Lot Analytics — master toggle for the entire tracking system
- Show View Counter on Lots — display a public view count on each lot page
- New admin dashboard tab: Lot Analytics (under Site Statistics) with:
- Auction Performance comparison table
- Most Viewed Lots (all time)
- Most Favorited Lots
- Trending Lots (last 7 days)
- Backfill from existing pageview data button
- Local favorites tracking mirrors BidSpirit favorites for analytics
- Three new shortcodes for displaying popular lots:
- — most viewed lots grid
- — most favorited lots grid
- — trending lots (configurable time window)
- All shortcodes support limit, columns, auction_id, and period filtering
- New DB tables: af_lot_analytics, af_lot_favorites (DB version 19)
2.5.9.0
Improvements:
- Redesigned Recently Viewed items as square card grid (rows/columns) instead of horizontal list.
- Changed lot thumbnail to contain (show full image) instead of cover (crop to fill).
2.5.8.0
Fixes:
- Fixed Recently Viewed cloned image thumbnails using non-existent resize prefix. Now uses raw image filename for reliable CDN loading.
2.5.7.0
Fixes:
- Fixed Recently Viewed thumbnail URLs — now builds correct BidSpirit CDN URLs for both cloned (Fastly) and non-cloned (Cloudinary) images instead of broken relative paths.
2.5.6.0
Fixes:
- Fixed placeholder image fallback path still referencing old uco-auctions directory.
- Added background color for empty thumbnail containers in Recently Viewed.
2.5.5.0
Improvements:
- Removed PAST/Upcoming badges from Recently Viewed items display.
- Added lot thumbnail images to Recently Viewed items with proper card layout.
2.5.4.0
Fixes:
- Fixed item variable not being passed to auctionIsLive-template (was lost during previous file edits).
2.5.3.0
Fixes:
- Fixed
item is not definedindisplayBidFormData— removed strayitem: itemfrom house-info-template call where item variable does not exist in scope.
2.5.2.0
Fixes:
- Fixed
item is not definedJavaScript error inauctionIsLive-templateon single lot pages. - Fixed statistics table auto-creation running on frontend requests (restricted to admin only).
2.5.1.0
Fixes:
- Fixed
hideleadingBidSection is not definedJavaScript error on single lot pages. - Fixed statistics database tables not auto-creating on plugin update (only activated on fresh install).
- Added self-healing table creation on
plugins_loadedfor sites that update without re-activating. - Cleaned up duplicate
AF_Stats_Init::activate()calls in DB update function. - Bumped DB version to 18.
2.5.0.0
New Features:
- Site Statistics System — Full website analytics built into the plugin admin (Lots → Site Statistics).
- Dashboard with KPI cards: visitors, sessions, pageviews, bounce rate, avg duration, pages per session.
- Period comparison: current vs previous period with percentage change indicators.
- Traffic over time line chart (pageviews, visitors, sessions).
- Traffic channels doughnut chart (organic search, direct, referral, social, email, paid).
- Hourly distribution bar chart.
- Device type pie chart (desktop, mobile, tablet).
- Top Pages tab with views, unique visitors, avg time on page, search, pagination, CSV export.
- Referrers tab with traffic channel badges, sessions, bounce rate, avg duration, CSV export.
- Countries tab with horizontal bar chart and visitor/pageview counts.
- Technology tab with browser, OS, device breakdown and screen resolutions.
- Searches tab tracking internal WordPress and auction search queries.
- 404 Errors tab tracking broken URLs with hit counts and referrers.
- Real-Time visitors view (last 5 minutes, auto-refreshes every 10 seconds).
- Settings tab: enable/disable tracking, exclude IPs and roles, IP anonymization (GDPR), data retention.
- Date range picker with presets (7, 14, 30, 90, 365 days) and custom range.
- Lightweight frontend tracker (~3KB JS) with session management, heartbeat, and beacon API.
- GeoIP country/city lookup via ip-api.com with 24-hour cache.
- Bot detection (30+ crawler patterns filtered).
- UTM parameter tracking (source, medium, campaign, term, content).
- Traffic channel auto-classification.
- Outbound link click tracking.
- Time-on-page tracking via 30-second heartbeat + page unload beacon.
- Daily cron for automatic data retention purge.
- Recently Viewed Items — New profile page tab showing the last 20 viewed lots per user.
- Admin setting to enable/disable (Lots → Settings → Enable user viewing history).
- Tracks lot views with timestamp, stores in custom DB table.
- Clear history button for users.
- Disabled by default.
Fixes:
- Fixed
settings is not definedJavaScript error on auction catalog and single lot pages. - Fixed
postAuctionSaleOpened is not definedJavaScript error on auction pages. - Fixed missing
vendor.min.cssandpassword-validation.min.css(404 errors). - Added missing template variables to all
loadTemplatecalls forlot-content-template,leadingBid-template.
2.4.4.0
Fixes:
- Fixed the display of purchase prices on shop pages for unsold items. Items with a buyout price now correctly show the "Purchase Price" in both ended and active auction states.
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added missing
-
v2.7.2.5 Stable Mar 13, 2026Download v2.7.2.5📦 4.82 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.1.2
Fixes:
- Added missing
vatAndCommission()function inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js. Also added missingsetAuctionItemInfo()call.
2.7.1.1
Fixes:
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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 — added missing
auction-commission-fieldpopulation inauctionInfoInit()from upstream reference.
2.7.0.9
Fixes:
- Fixed fatal error during plugin install/upgrade:
class-stats-database.phprequire failure. Addedfile_existsguard inAF_Stats_Init::activate()and plugin-specific guard inupgrader_process_completehook to prevent running upgrade logic for unrelated plugin updates.
2.7.0.8
Fixes:
- Synced
formattedPrice()across bothapp.jsandapp.min.js— added second currency support, better null/NaN handling, and always 2 decimal places on all prices. Fixes sites where minification plugins may loadapp.min.jsinstead ofapp.js.
2.7.0.7
Fixes:
- All prices now display with 2 decimal places (e.g. $10.00 instead of $10) via
formattedPrice()usingtoLocaleStringwithminimumFractionDigits: 2.
2.7.0.6
Fixes:
- Added missing
estimatedPrice,content, andisShoptemplate variables to widget and shortcodelot-content-templatecalls, preventing potential ReferenceErrors on catalogue pages with ended auctions or shop items.
2.7.0.5
Fixes:
- Fixed Stripe payment form not visible in Add Payment Method modal —
#stripePaymentElementhadhiddenclass that was never removed. Now unhides form on Stripereadyevent and re-hides on modal close.
2.7.0.4
Fixes:
- Fixed
bidspiritApi.getAuctionSettingsData is not a functionerror in getFullPriceInfo — use local auctionSettingsData variable instead of non-existent API method.
2.7.0.3
Fixes:
- Fixed
fullPriceInfo is not definedReferenceError that prevented bid confirmation modal from opening on single lot pages and catalogue pages. - Added missing
getFullPriceInfo(),getCommissonInfoForPrice(), androundToCents()functions ported from upstream. - All three bid submission paths (submitBid, submitBidCatalog, submitBidCatalog3) now correctly pass fullPriceInfo to the modal-bid-template.
2.7.0.0
New Features: Advanced Lot Analytics
- Conversion Tracking — View-to-bid ratio table showing lot views, catalogue/external clicks, favorites, shares, bid status, and sold price with sell-through rate summary cards
- Peak Viewing Times — Hourly bar chart showing when lots get the most views (last 30 days) with peak hour identification
- Pre-Sale vs Live Comparison — Per-auction breakdown of views before auction day vs during/after, with pre-sale percentage
- Category Performance — Analytics grouped by lot category: views, lots, favorites, sold count, average views per lot
- Share Analytics — Track shares by channel (Facebook, Twitter, WhatsApp, Email, Pinterest, Copy Link) with per-channel badges and most-shared lots table
- Search → View Funnel — Correlates internal search queries with subsequent lot page views to show which searches lead to browsing
- Click-Through Tracking — Distinguishes catalogue clicks (user came from auction catalogue) vs external clicks (direct/search/referral)
- CSV Export — One-click export of all lot analytics data, filterable by catalogue
- Hot Lot Badge — Automatic "🔥 Popular" badge on catalogue page thumbnails for lots exceeding view threshold (default: 5 views)
- All new panels respect the catalogue filter dropdown
- New DB table: af_lot_shares (per-channel share events)
- New columns: catalogue_clicks, external_clicks, share_count on af_lot_analytics
- DB version bumped to 20
2.6.3.0
Fixes:
- View counter now shows on lot pages even with 0 views
- Fixed backfill and trending queries failing on URLs with trailing slashes
2.6.2.0
Fixes:
- Fixed 404 errors on auction/lot pages after DB version update by flushing rewrite rules automatically
2.6.1.0
Fixes:
- Renamed shortcodes to use bp_ prefix for consistency: [bp_popular_lots], [bp_most_favorited], [bp_trending_lots]
2.6.0.0
New Feature: Lot Analytics
- Added comprehensive lot view and favorite tracking system
- Two new settings in AuctionForge > Settings (both disabled by default):
- Enable Lot Analytics — master toggle for the entire tracking system
- Show View Counter on Lots — display a public view count on each lot page
- New admin dashboard tab: Lot Analytics (under Site Statistics) with:
- Auction Performance comparison table
- Most Viewed Lots (all time)
- Most Favorited Lots
- Trending Lots (last 7 days)
- Backfill from existing pageview data button
- Local favorites tracking mirrors BidSpirit favorites for analytics
- Three new shortcodes for displaying popular lots:
- — most viewed lots grid
- — most favorited lots grid
- — trending lots (configurable time window)
- All shortcodes support limit, columns, auction_id, and period filtering
- New DB tables: af_lot_analytics, af_lot_favorites (DB version 19)
2.5.9.0
Improvements:
- Redesigned Recently Viewed items as square card grid (rows/columns) instead of horizontal list.
- Changed lot thumbnail to contain (show full image) instead of cover (crop to fill).
2.5.8.0
Fixes:
- Fixed Recently Viewed cloned image thumbnails using non-existent resize prefix. Now uses raw image filename for reliable CDN loading.
2.5.7.0
Fixes:
- Fixed Recently Viewed thumbnail URLs — now builds correct BidSpirit CDN URLs for both cloned (Fastly) and non-cloned (Cloudinary) images instead of broken relative paths.
2.5.6.0
Fixes:
- Fixed placeholder image fallback path still referencing old uco-auctions directory.
- Added background color for empty thumbnail containers in Recently Viewed.
2.5.5.0
Improvements:
- Removed PAST/Upcoming badges from Recently Viewed items display.
- Added lot thumbnail images to Recently Viewed items with proper card layout.
2.5.4.0
Fixes:
- Fixed item variable not being passed to auctionIsLive-template (was lost during previous file edits).
2.5.3.0
Fixes:
- Fixed
item is not definedindisplayBidFormData— removed strayitem: itemfrom house-info-template call where item variable does not exist in scope.
2.5.2.0
Fixes:
- Fixed
item is not definedJavaScript error inauctionIsLive-templateon single lot pages. - Fixed statistics table auto-creation running on frontend requests (restricted to admin only).
2.5.1.0
Fixes:
- Fixed
hideleadingBidSection is not definedJavaScript error on single lot pages. - Fixed statistics database tables not auto-creating on plugin update (only activated on fresh install).
- Added self-healing table creation on
plugins_loadedfor sites that update without re-activating. - Cleaned up duplicate
AF_Stats_Init::activate()calls in DB update function. - Bumped DB version to 18.
2.5.0.0
New Features:
- Site Statistics System — Full website analytics built into the plugin admin (Lots → Site Statistics).
- Dashboard with KPI cards: visitors, sessions, pageviews, bounce rate, avg duration, pages per session.
- Period comparison: current vs previous period with percentage change indicators.
- Traffic over time line chart (pageviews, visitors, sessions).
- Traffic channels doughnut chart (organic search, direct, referral, social, email, paid).
- Hourly distribution bar chart.
- Device type pie chart (desktop, mobile, tablet).
- Top Pages tab with views, unique visitors, avg time on page, search, pagination, CSV export.
- Referrers tab with traffic channel badges, sessions, bounce rate, avg duration, CSV export.
- Countries tab with horizontal bar chart and visitor/pageview counts.
- Technology tab with browser, OS, device breakdown and screen resolutions.
- Searches tab tracking internal WordPress and auction search queries.
- 404 Errors tab tracking broken URLs with hit counts and referrers.
- Real-Time visitors view (last 5 minutes, auto-refreshes every 10 seconds).
- Settings tab: enable/disable tracking, exclude IPs and roles, IP anonymization (GDPR), data retention.
- Date range picker with presets (7, 14, 30, 90, 365 days) and custom range.
- Lightweight frontend tracker (~3KB JS) with session management, heartbeat, and beacon API.
- GeoIP country/city lookup via ip-api.com with 24-hour cache.
- Bot detection (30+ crawler patterns filtered).
- UTM parameter tracking (source, medium, campaign, term, content).
- Traffic channel auto-classification.
- Outbound link click tracking.
- Time-on-page tracking via 30-second heartbeat + page unload beacon.
- Daily cron for automatic data retention purge.
- Recently Viewed Items — New profile page tab showing the last 20 viewed lots per user.
- Admin setting to enable/disable (Lots → Settings → Enable user viewing history).
- Tracks lot views with timestamp, stores in custom DB table.
- Clear history button for users.
- Disabled by default.
Fixes:
- Fixed
settings is not definedJavaScript error on auction catalog and single lot pages. - Fixed
postAuctionSaleOpened is not definedJavaScript error on auction pages. - Fixed missing
vendor.min.cssandpassword-validation.min.css(404 errors). - Added missing template variables to all
loadTemplatecalls forlot-content-template,leadingBid-template.
2.4.4.0
Fixes:
- Fixed the display of purchase prices on shop pages for unsold items. Items with a buyout price now correctly show the "Purchase Price" in both ended and active auction states.
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added missing
-
v2.7.2.4 Stable Mar 12, 2026Download v2.7.2.4📦 4.82 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.1.2
Fixes:
- Added missing
vatAndCommission()function inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js. Also added missingsetAuctionItemInfo()call.
2.7.1.1
Fixes:
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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 — added missing
auction-commission-fieldpopulation inauctionInfoInit()from upstream reference.
2.7.0.9
Fixes:
- Fixed fatal error during plugin install/upgrade:
class-stats-database.phprequire failure. Addedfile_existsguard inAF_Stats_Init::activate()and plugin-specific guard inupgrader_process_completehook to prevent running upgrade logic for unrelated plugin updates.
2.7.0.8
Fixes:
- Synced
formattedPrice()across bothapp.jsandapp.min.js— added second currency support, better null/NaN handling, and always 2 decimal places on all prices. Fixes sites where minification plugins may loadapp.min.jsinstead ofapp.js.
2.7.0.7
Fixes:
- All prices now display with 2 decimal places (e.g. $10.00 instead of $10) via
formattedPrice()usingtoLocaleStringwithminimumFractionDigits: 2.
2.7.0.6
Fixes:
- Added missing
estimatedPrice,content, andisShoptemplate variables to widget and shortcodelot-content-templatecalls, preventing potential ReferenceErrors on catalogue pages with ended auctions or shop items.
2.7.0.5
Fixes:
- Fixed Stripe payment form not visible in Add Payment Method modal —
#stripePaymentElementhadhiddenclass that was never removed. Now unhides form on Stripereadyevent and re-hides on modal close.
2.7.0.4
Fixes:
- Fixed
bidspiritApi.getAuctionSettingsData is not a functionerror in getFullPriceInfo — use local auctionSettingsData variable instead of non-existent API method.
2.7.0.3
Fixes:
- Fixed
fullPriceInfo is not definedReferenceError that prevented bid confirmation modal from opening on single lot pages and catalogue pages. - Added missing
getFullPriceInfo(),getCommissonInfoForPrice(), androundToCents()functions ported from upstream. - All three bid submission paths (submitBid, submitBidCatalog, submitBidCatalog3) now correctly pass fullPriceInfo to the modal-bid-template.
2.7.0.0
New Features: Advanced Lot Analytics
- Conversion Tracking — View-to-bid ratio table showing lot views, catalogue/external clicks, favorites, shares, bid status, and sold price with sell-through rate summary cards
- Peak Viewing Times — Hourly bar chart showing when lots get the most views (last 30 days) with peak hour identification
- Pre-Sale vs Live Comparison — Per-auction breakdown of views before auction day vs during/after, with pre-sale percentage
- Category Performance — Analytics grouped by lot category: views, lots, favorites, sold count, average views per lot
- Share Analytics — Track shares by channel (Facebook, Twitter, WhatsApp, Email, Pinterest, Copy Link) with per-channel badges and most-shared lots table
- Search → View Funnel — Correlates internal search queries with subsequent lot page views to show which searches lead to browsing
- Click-Through Tracking — Distinguishes catalogue clicks (user came from auction catalogue) vs external clicks (direct/search/referral)
- CSV Export — One-click export of all lot analytics data, filterable by catalogue
- Hot Lot Badge — Automatic "🔥 Popular" badge on catalogue page thumbnails for lots exceeding view threshold (default: 5 views)
- All new panels respect the catalogue filter dropdown
- New DB table: af_lot_shares (per-channel share events)
- New columns: catalogue_clicks, external_clicks, share_count on af_lot_analytics
- DB version bumped to 20
2.6.3.0
Fixes:
- View counter now shows on lot pages even with 0 views
- Fixed backfill and trending queries failing on URLs with trailing slashes
2.6.2.0
Fixes:
- Fixed 404 errors on auction/lot pages after DB version update by flushing rewrite rules automatically
2.6.1.0
Fixes:
- Renamed shortcodes to use bp_ prefix for consistency: [bp_popular_lots], [bp_most_favorited], [bp_trending_lots]
2.6.0.0
New Feature: Lot Analytics
- Added comprehensive lot view and favorite tracking system
- Two new settings in AuctionForge > Settings (both disabled by default):
- Enable Lot Analytics — master toggle for the entire tracking system
- Show View Counter on Lots — display a public view count on each lot page
- New admin dashboard tab: Lot Analytics (under Site Statistics) with:
- Auction Performance comparison table
- Most Viewed Lots (all time)
- Most Favorited Lots
- Trending Lots (last 7 days)
- Backfill from existing pageview data button
- Local favorites tracking mirrors BidSpirit favorites for analytics
- Three new shortcodes for displaying popular lots:
- — most viewed lots grid
- — most favorited lots grid
- — trending lots (configurable time window)
- All shortcodes support limit, columns, auction_id, and period filtering
- New DB tables: af_lot_analytics, af_lot_favorites (DB version 19)
2.5.9.0
Improvements:
- Redesigned Recently Viewed items as square card grid (rows/columns) instead of horizontal list.
- Changed lot thumbnail to contain (show full image) instead of cover (crop to fill).
2.5.8.0
Fixes:
- Fixed Recently Viewed cloned image thumbnails using non-existent resize prefix. Now uses raw image filename for reliable CDN loading.
2.5.7.0
Fixes:
- Fixed Recently Viewed thumbnail URLs — now builds correct BidSpirit CDN URLs for both cloned (Fastly) and non-cloned (Cloudinary) images instead of broken relative paths.
2.5.6.0
Fixes:
- Fixed placeholder image fallback path still referencing old uco-auctions directory.
- Added background color for empty thumbnail containers in Recently Viewed.
2.5.5.0
Improvements:
- Removed PAST/Upcoming badges from Recently Viewed items display.
- Added lot thumbnail images to Recently Viewed items with proper card layout.
2.5.4.0
Fixes:
- Fixed item variable not being passed to auctionIsLive-template (was lost during previous file edits).
2.5.3.0
Fixes:
- Fixed
item is not definedindisplayBidFormData— removed strayitem: itemfrom house-info-template call where item variable does not exist in scope.
2.5.2.0
Fixes:
- Fixed
item is not definedJavaScript error inauctionIsLive-templateon single lot pages. - Fixed statistics table auto-creation running on frontend requests (restricted to admin only).
2.5.1.0
Fixes:
- Fixed
hideleadingBidSection is not definedJavaScript error on single lot pages. - Fixed statistics database tables not auto-creating on plugin update (only activated on fresh install).
- Added self-healing table creation on
plugins_loadedfor sites that update without re-activating. - Cleaned up duplicate
AF_Stats_Init::activate()calls in DB update function. - Bumped DB version to 18.
2.5.0.0
New Features:
- Site Statistics System — Full website analytics built into the plugin admin (Lots → Site Statistics).
- Dashboard with KPI cards: visitors, sessions, pageviews, bounce rate, avg duration, pages per session.
- Period comparison: current vs previous period with percentage change indicators.
- Traffic over time line chart (pageviews, visitors, sessions).
- Traffic channels doughnut chart (organic search, direct, referral, social, email, paid).
- Hourly distribution bar chart.
- Device type pie chart (desktop, mobile, tablet).
- Top Pages tab with views, unique visitors, avg time on page, search, pagination, CSV export.
- Referrers tab with traffic channel badges, sessions, bounce rate, avg duration, CSV export.
- Countries tab with horizontal bar chart and visitor/pageview counts.
- Technology tab with browser, OS, device breakdown and screen resolutions.
- Searches tab tracking internal WordPress and auction search queries.
- 404 Errors tab tracking broken URLs with hit counts and referrers.
- Real-Time visitors view (last 5 minutes, auto-refreshes every 10 seconds).
- Settings tab: enable/disable tracking, exclude IPs and roles, IP anonymization (GDPR), data retention.
- Date range picker with presets (7, 14, 30, 90, 365 days) and custom range.
- Lightweight frontend tracker (~3KB JS) with session management, heartbeat, and beacon API.
- GeoIP country/city lookup via ip-api.com with 24-hour cache.
- Bot detection (30+ crawler patterns filtered).
- UTM parameter tracking (source, medium, campaign, term, content).
- Traffic channel auto-classification.
- Outbound link click tracking.
- Time-on-page tracking via 30-second heartbeat + page unload beacon.
- Daily cron for automatic data retention purge.
- Recently Viewed Items — New profile page tab showing the last 20 viewed lots per user.
- Admin setting to enable/disable (Lots → Settings → Enable user viewing history).
- Tracks lot views with timestamp, stores in custom DB table.
- Clear history button for users.
- Disabled by default.
Fixes:
- Fixed
settings is not definedJavaScript error on auction catalog and single lot pages. - Fixed
postAuctionSaleOpened is not definedJavaScript error on auction pages. - Fixed missing
vendor.min.cssandpassword-validation.min.css(404 errors). - Added missing template variables to all
loadTemplatecalls forlot-content-template,leadingBid-template.
2.4.4.0
Fixes:
- Fixed the display of purchase prices on shop pages for unsold items. Items with a buyout price now correctly show the "Purchase Price" in both ended and active auction states.
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added missing
-
v2.7.2.2 Stable Mar 11, 2026Download v2.7.2.2📦 4.82 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.1.2
Fixes:
- Added missing
vatAndCommission()function inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js. Also added missingsetAuctionItemInfo()call.
2.7.1.1
Fixes:
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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 — added missing
auction-commission-fieldpopulation inauctionInfoInit()from upstream reference.
2.7.0.9
Fixes:
- Fixed fatal error during plugin install/upgrade:
class-stats-database.phprequire failure. Addedfile_existsguard inAF_Stats_Init::activate()and plugin-specific guard inupgrader_process_completehook to prevent running upgrade logic for unrelated plugin updates.
2.7.0.8
Fixes:
- Synced
formattedPrice()across bothapp.jsandapp.min.js— added second currency support, better null/NaN handling, and always 2 decimal places on all prices. Fixes sites where minification plugins may loadapp.min.jsinstead ofapp.js.
2.7.0.7
Fixes:
- All prices now display with 2 decimal places (e.g. $10.00 instead of $10) via
formattedPrice()usingtoLocaleStringwithminimumFractionDigits: 2.
2.7.0.6
Fixes:
- Added missing
estimatedPrice,content, andisShoptemplate variables to widget and shortcodelot-content-templatecalls, preventing potential ReferenceErrors on catalogue pages with ended auctions or shop items.
2.7.0.5
Fixes:
- Fixed Stripe payment form not visible in Add Payment Method modal —
#stripePaymentElementhadhiddenclass that was never removed. Now unhides form on Stripereadyevent and re-hides on modal close.
2.7.0.4
Fixes:
- Fixed
bidspiritApi.getAuctionSettingsData is not a functionerror in getFullPriceInfo — use local auctionSettingsData variable instead of non-existent API method.
2.7.0.3
Fixes:
- Fixed
fullPriceInfo is not definedReferenceError that prevented bid confirmation modal from opening on single lot pages and catalogue pages. - Added missing
getFullPriceInfo(),getCommissonInfoForPrice(), androundToCents()functions ported from upstream. - All three bid submission paths (submitBid, submitBidCatalog, submitBidCatalog3) now correctly pass fullPriceInfo to the modal-bid-template.
2.7.0.0
New Features: Advanced Lot Analytics
- Conversion Tracking — View-to-bid ratio table showing lot views, catalogue/external clicks, favorites, shares, bid status, and sold price with sell-through rate summary cards
- Peak Viewing Times — Hourly bar chart showing when lots get the most views (last 30 days) with peak hour identification
- Pre-Sale vs Live Comparison — Per-auction breakdown of views before auction day vs during/after, with pre-sale percentage
- Category Performance — Analytics grouped by lot category: views, lots, favorites, sold count, average views per lot
- Share Analytics — Track shares by channel (Facebook, Twitter, WhatsApp, Email, Pinterest, Copy Link) with per-channel badges and most-shared lots table
- Search → View Funnel — Correlates internal search queries with subsequent lot page views to show which searches lead to browsing
- Click-Through Tracking — Distinguishes catalogue clicks (user came from auction catalogue) vs external clicks (direct/search/referral)
- CSV Export — One-click export of all lot analytics data, filterable by catalogue
- Hot Lot Badge — Automatic "🔥 Popular" badge on catalogue page thumbnails for lots exceeding view threshold (default: 5 views)
- All new panels respect the catalogue filter dropdown
- New DB table: af_lot_shares (per-channel share events)
- New columns: catalogue_clicks, external_clicks, share_count on af_lot_analytics
- DB version bumped to 20
2.6.3.0
Fixes:
- View counter now shows on lot pages even with 0 views
- Fixed backfill and trending queries failing on URLs with trailing slashes
2.6.2.0
Fixes:
- Fixed 404 errors on auction/lot pages after DB version update by flushing rewrite rules automatically
2.6.1.0
Fixes:
- Renamed shortcodes to use bp_ prefix for consistency: [bp_popular_lots], [bp_most_favorited], [bp_trending_lots]
2.6.0.0
New Feature: Lot Analytics
- Added comprehensive lot view and favorite tracking system
- Two new settings in AuctionForge > Settings (both disabled by default):
- Enable Lot Analytics — master toggle for the entire tracking system
- Show View Counter on Lots — display a public view count on each lot page
- New admin dashboard tab: Lot Analytics (under Site Statistics) with:
- Auction Performance comparison table
- Most Viewed Lots (all time)
- Most Favorited Lots
- Trending Lots (last 7 days)
- Backfill from existing pageview data button
- Local favorites tracking mirrors BidSpirit favorites for analytics
- Three new shortcodes for displaying popular lots:
- — most viewed lots grid
- — most favorited lots grid
- — trending lots (configurable time window)
- All shortcodes support limit, columns, auction_id, and period filtering
- New DB tables: af_lot_analytics, af_lot_favorites (DB version 19)
2.5.9.0
Improvements:
- Redesigned Recently Viewed items as square card grid (rows/columns) instead of horizontal list.
- Changed lot thumbnail to contain (show full image) instead of cover (crop to fill).
2.5.8.0
Fixes:
- Fixed Recently Viewed cloned image thumbnails using non-existent resize prefix. Now uses raw image filename for reliable CDN loading.
2.5.7.0
Fixes:
- Fixed Recently Viewed thumbnail URLs — now builds correct BidSpirit CDN URLs for both cloned (Fastly) and non-cloned (Cloudinary) images instead of broken relative paths.
2.5.6.0
Fixes:
- Fixed placeholder image fallback path still referencing old uco-auctions directory.
- Added background color for empty thumbnail containers in Recently Viewed.
2.5.5.0
Improvements:
- Removed PAST/Upcoming badges from Recently Viewed items display.
- Added lot thumbnail images to Recently Viewed items with proper card layout.
2.5.4.0
Fixes:
- Fixed item variable not being passed to auctionIsLive-template (was lost during previous file edits).
2.5.3.0
Fixes:
- Fixed
item is not definedindisplayBidFormData— removed strayitem: itemfrom house-info-template call where item variable does not exist in scope.
2.5.2.0
Fixes:
- Fixed
item is not definedJavaScript error inauctionIsLive-templateon single lot pages. - Fixed statistics table auto-creation running on frontend requests (restricted to admin only).
2.5.1.0
Fixes:
- Fixed
hideleadingBidSection is not definedJavaScript error on single lot pages. - Fixed statistics database tables not auto-creating on plugin update (only activated on fresh install).
- Added self-healing table creation on
plugins_loadedfor sites that update without re-activating. - Cleaned up duplicate
AF_Stats_Init::activate()calls in DB update function. - Bumped DB version to 18.
2.5.0.0
New Features:
- Site Statistics System — Full website analytics built into the plugin admin (Lots → Site Statistics).
- Dashboard with KPI cards: visitors, sessions, pageviews, bounce rate, avg duration, pages per session.
- Period comparison: current vs previous period with percentage change indicators.
- Traffic over time line chart (pageviews, visitors, sessions).
- Traffic channels doughnut chart (organic search, direct, referral, social, email, paid).
- Hourly distribution bar chart.
- Device type pie chart (desktop, mobile, tablet).
- Top Pages tab with views, unique visitors, avg time on page, search, pagination, CSV export.
- Referrers tab with traffic channel badges, sessions, bounce rate, avg duration, CSV export.
- Countries tab with horizontal bar chart and visitor/pageview counts.
- Technology tab with browser, OS, device breakdown and screen resolutions.
- Searches tab tracking internal WordPress and auction search queries.
- 404 Errors tab tracking broken URLs with hit counts and referrers.
- Real-Time visitors view (last 5 minutes, auto-refreshes every 10 seconds).
- Settings tab: enable/disable tracking, exclude IPs and roles, IP anonymization (GDPR), data retention.
- Date range picker with presets (7, 14, 30, 90, 365 days) and custom range.
- Lightweight frontend tracker (~3KB JS) with session management, heartbeat, and beacon API.
- GeoIP country/city lookup via ip-api.com with 24-hour cache.
- Bot detection (30+ crawler patterns filtered).
- UTM parameter tracking (source, medium, campaign, term, content).
- Traffic channel auto-classification.
- Outbound link click tracking.
- Time-on-page tracking via 30-second heartbeat + page unload beacon.
- Daily cron for automatic data retention purge.
- Recently Viewed Items — New profile page tab showing the last 20 viewed lots per user.
- Admin setting to enable/disable (Lots → Settings → Enable user viewing history).
- Tracks lot views with timestamp, stores in custom DB table.
- Clear history button for users.
- Disabled by default.
Fixes:
- Fixed
settings is not definedJavaScript error on auction catalog and single lot pages. - Fixed
postAuctionSaleOpened is not definedJavaScript error on auction pages. - Fixed missing
vendor.min.cssandpassword-validation.min.css(404 errors). - Added missing template variables to all
loadTemplatecalls forlot-content-template,leadingBid-template.
2.4.4.0
Fixes:
- Fixed the display of purchase prices on shop pages for unsold items. Items with a buyout price now correctly show the "Purchase Price" in both ended and active auction states.
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added missing
-
v2.7.2.1 Stable Mar 11, 2026Download v2.7.2.1📦 4.81 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.1.2
Fixes:
- Added missing
vatAndCommission()function inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js. Also added missingsetAuctionItemInfo()call.
2.7.1.1
Fixes:
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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 — added missing
auction-commission-fieldpopulation inauctionInfoInit()from upstream reference.
2.7.0.9
Fixes:
- Fixed fatal error during plugin install/upgrade:
class-stats-database.phprequire failure. Addedfile_existsguard inAF_Stats_Init::activate()and plugin-specific guard inupgrader_process_completehook to prevent running upgrade logic for unrelated plugin updates.
2.7.0.8
Fixes:
- Synced
formattedPrice()across bothapp.jsandapp.min.js— added second currency support, better null/NaN handling, and always 2 decimal places on all prices. Fixes sites where minification plugins may loadapp.min.jsinstead ofapp.js.
2.7.0.7
Fixes:
- All prices now display with 2 decimal places (e.g. $10.00 instead of $10) via
formattedPrice()usingtoLocaleStringwithminimumFractionDigits: 2.
2.7.0.6
Fixes:
- Added missing
estimatedPrice,content, andisShoptemplate variables to widget and shortcodelot-content-templatecalls, preventing potential ReferenceErrors on catalogue pages with ended auctions or shop items.
2.7.0.5
Fixes:
- Fixed Stripe payment form not visible in Add Payment Method modal —
#stripePaymentElementhadhiddenclass that was never removed. Now unhides form on Stripereadyevent and re-hides on modal close.
2.7.0.4
Fixes:
- Fixed
bidspiritApi.getAuctionSettingsData is not a functionerror in getFullPriceInfo — use local auctionSettingsData variable instead of non-existent API method.
2.7.0.3
Fixes:
- Fixed
fullPriceInfo is not definedReferenceError that prevented bid confirmation modal from opening on single lot pages and catalogue pages. - Added missing
getFullPriceInfo(),getCommissonInfoForPrice(), androundToCents()functions ported from upstream. - All three bid submission paths (submitBid, submitBidCatalog, submitBidCatalog3) now correctly pass fullPriceInfo to the modal-bid-template.
2.7.0.0
New Features: Advanced Lot Analytics
- Conversion Tracking — View-to-bid ratio table showing lot views, catalogue/external clicks, favorites, shares, bid status, and sold price with sell-through rate summary cards
- Peak Viewing Times — Hourly bar chart showing when lots get the most views (last 30 days) with peak hour identification
- Pre-Sale vs Live Comparison — Per-auction breakdown of views before auction day vs during/after, with pre-sale percentage
- Category Performance — Analytics grouped by lot category: views, lots, favorites, sold count, average views per lot
- Share Analytics — Track shares by channel (Facebook, Twitter, WhatsApp, Email, Pinterest, Copy Link) with per-channel badges and most-shared lots table
- Search → View Funnel — Correlates internal search queries with subsequent lot page views to show which searches lead to browsing
- Click-Through Tracking — Distinguishes catalogue clicks (user came from auction catalogue) vs external clicks (direct/search/referral)
- CSV Export — One-click export of all lot analytics data, filterable by catalogue
- Hot Lot Badge — Automatic "🔥 Popular" badge on catalogue page thumbnails for lots exceeding view threshold (default: 5 views)
- All new panels respect the catalogue filter dropdown
- New DB table: af_lot_shares (per-channel share events)
- New columns: catalogue_clicks, external_clicks, share_count on af_lot_analytics
- DB version bumped to 20
2.6.3.0
Fixes:
- View counter now shows on lot pages even with 0 views
- Fixed backfill and trending queries failing on URLs with trailing slashes
2.6.2.0
Fixes:
- Fixed 404 errors on auction/lot pages after DB version update by flushing rewrite rules automatically
2.6.1.0
Fixes:
- Renamed shortcodes to use bp_ prefix for consistency: [bp_popular_lots], [bp_most_favorited], [bp_trending_lots]
2.6.0.0
New Feature: Lot Analytics
- Added comprehensive lot view and favorite tracking system
- Two new settings in AuctionForge > Settings (both disabled by default):
- Enable Lot Analytics — master toggle for the entire tracking system
- Show View Counter on Lots — display a public view count on each lot page
- New admin dashboard tab: Lot Analytics (under Site Statistics) with:
- Auction Performance comparison table
- Most Viewed Lots (all time)
- Most Favorited Lots
- Trending Lots (last 7 days)
- Backfill from existing pageview data button
- Local favorites tracking mirrors BidSpirit favorites for analytics
- Three new shortcodes for displaying popular lots:
- — most viewed lots grid
- — most favorited lots grid
- — trending lots (configurable time window)
- All shortcodes support limit, columns, auction_id, and period filtering
- New DB tables: af_lot_analytics, af_lot_favorites (DB version 19)
2.5.9.0
Improvements:
- Redesigned Recently Viewed items as square card grid (rows/columns) instead of horizontal list.
- Changed lot thumbnail to contain (show full image) instead of cover (crop to fill).
2.5.8.0
Fixes:
- Fixed Recently Viewed cloned image thumbnails using non-existent resize prefix. Now uses raw image filename for reliable CDN loading.
2.5.7.0
Fixes:
- Fixed Recently Viewed thumbnail URLs — now builds correct BidSpirit CDN URLs for both cloned (Fastly) and non-cloned (Cloudinary) images instead of broken relative paths.
2.5.6.0
Fixes:
- Fixed placeholder image fallback path still referencing old uco-auctions directory.
- Added background color for empty thumbnail containers in Recently Viewed.
2.5.5.0
Improvements:
- Removed PAST/Upcoming badges from Recently Viewed items display.
- Added lot thumbnail images to Recently Viewed items with proper card layout.
2.5.4.0
Fixes:
- Fixed item variable not being passed to auctionIsLive-template (was lost during previous file edits).
2.5.3.0
Fixes:
- Fixed
item is not definedindisplayBidFormData— removed strayitem: itemfrom house-info-template call where item variable does not exist in scope.
2.5.2.0
Fixes:
- Fixed
item is not definedJavaScript error inauctionIsLive-templateon single lot pages. - Fixed statistics table auto-creation running on frontend requests (restricted to admin only).
2.5.1.0
Fixes:
- Fixed
hideleadingBidSection is not definedJavaScript error on single lot pages. - Fixed statistics database tables not auto-creating on plugin update (only activated on fresh install).
- Added self-healing table creation on
plugins_loadedfor sites that update without re-activating. - Cleaned up duplicate
AF_Stats_Init::activate()calls in DB update function. - Bumped DB version to 18.
2.5.0.0
New Features:
- Site Statistics System — Full website analytics built into the plugin admin (Lots → Site Statistics).
- Dashboard with KPI cards: visitors, sessions, pageviews, bounce rate, avg duration, pages per session.
- Period comparison: current vs previous period with percentage change indicators.
- Traffic over time line chart (pageviews, visitors, sessions).
- Traffic channels doughnut chart (organic search, direct, referral, social, email, paid).
- Hourly distribution bar chart.
- Device type pie chart (desktop, mobile, tablet).
- Top Pages tab with views, unique visitors, avg time on page, search, pagination, CSV export.
- Referrers tab with traffic channel badges, sessions, bounce rate, avg duration, CSV export.
- Countries tab with horizontal bar chart and visitor/pageview counts.
- Technology tab with browser, OS, device breakdown and screen resolutions.
- Searches tab tracking internal WordPress and auction search queries.
- 404 Errors tab tracking broken URLs with hit counts and referrers.
- Real-Time visitors view (last 5 minutes, auto-refreshes every 10 seconds).
- Settings tab: enable/disable tracking, exclude IPs and roles, IP anonymization (GDPR), data retention.
- Date range picker with presets (7, 14, 30, 90, 365 days) and custom range.
- Lightweight frontend tracker (~3KB JS) with session management, heartbeat, and beacon API.
- GeoIP country/city lookup via ip-api.com with 24-hour cache.
- Bot detection (30+ crawler patterns filtered).
- UTM parameter tracking (source, medium, campaign, term, content).
- Traffic channel auto-classification.
- Outbound link click tracking.
- Time-on-page tracking via 30-second heartbeat + page unload beacon.
- Daily cron for automatic data retention purge.
- Recently Viewed Items — New profile page tab showing the last 20 viewed lots per user.
- Admin setting to enable/disable (Lots → Settings → Enable user viewing history).
- Tracks lot views with timestamp, stores in custom DB table.
- Clear history button for users.
- Disabled by default.
Fixes:
- Fixed
settings is not definedJavaScript error on auction catalog and single lot pages. - Fixed
postAuctionSaleOpened is not definedJavaScript error on auction pages. - Fixed missing
vendor.min.cssandpassword-validation.min.css(404 errors). - Added missing template variables to all
loadTemplatecalls forlot-content-template,leadingBid-template.
2.4.4.0
Fixes:
- Fixed the display of purchase prices on shop pages for unsold items. Items with a buyout price now correctly show the "Purchase Price" in both ended and active auction states.
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added missing
-
v2.7.2.0 Stable Mar 11, 2026Download v2.7.2.0📦 4.81 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.1.2
Fixes:
- Added missing
vatAndCommission()function inloadBid()— the sales tax display on single lot pages was never being rendered because the code to callloadTemplateforvat-and-commission-templatewas missing fromapp.js. Also added missingsetAuctionItemInfo()call.
2.7.1.1
Fixes:
- 2 decimal places now only appear in the bid modal (Your max bid, Total Price) via
showDecimalsflag. 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 — added missing
auction-commission-fieldpopulation inauctionInfoInit()from upstream reference.
2.7.0.9
Fixes:
- Fixed fatal error during plugin install/upgrade:
class-stats-database.phprequire failure. Addedfile_existsguard inAF_Stats_Init::activate()and plugin-specific guard inupgrader_process_completehook to prevent running upgrade logic for unrelated plugin updates.
2.7.0.8
Fixes:
- Synced
formattedPrice()across bothapp.jsandapp.min.js— added second currency support, better null/NaN handling, and always 2 decimal places on all prices. Fixes sites where minification plugins may loadapp.min.jsinstead ofapp.js.
2.7.0.7
Fixes:
- All prices now display with 2 decimal places (e.g. $10.00 instead of $10) via
formattedPrice()usingtoLocaleStringwithminimumFractionDigits: 2.
2.7.0.6
Fixes:
- Added missing
estimatedPrice,content, andisShoptemplate variables to widget and shortcodelot-content-templatecalls, preventing potential ReferenceErrors on catalogue pages with ended auctions or shop items.
2.7.0.5
Fixes:
- Fixed Stripe payment form not visible in Add Payment Method modal —
#stripePaymentElementhadhiddenclass that was never removed. Now unhides form on Stripereadyevent and re-hides on modal close.
2.7.0.4
Fixes:
- Fixed
bidspiritApi.getAuctionSettingsData is not a functionerror in getFullPriceInfo — use local auctionSettingsData variable instead of non-existent API method.
2.7.0.3
Fixes:
- Fixed
fullPriceInfo is not definedReferenceError that prevented bid confirmation modal from opening on single lot pages and catalogue pages. - Added missing
getFullPriceInfo(),getCommissonInfoForPrice(), androundToCents()functions ported from upstream. - All three bid submission paths (submitBid, submitBidCatalog, submitBidCatalog3) now correctly pass fullPriceInfo to the modal-bid-template.
2.7.0.0
New Features: Advanced Lot Analytics
- Conversion Tracking — View-to-bid ratio table showing lot views, catalogue/external clicks, favorites, shares, bid status, and sold price with sell-through rate summary cards
- Peak Viewing Times — Hourly bar chart showing when lots get the most views (last 30 days) with peak hour identification
- Pre-Sale vs Live Comparison — Per-auction breakdown of views before auction day vs during/after, with pre-sale percentage
- Category Performance — Analytics grouped by lot category: views, lots, favorites, sold count, average views per lot
- Share Analytics — Track shares by channel (Facebook, Twitter, WhatsApp, Email, Pinterest, Copy Link) with per-channel badges and most-shared lots table
- Search → View Funnel — Correlates internal search queries with subsequent lot page views to show which searches lead to browsing
- Click-Through Tracking — Distinguishes catalogue clicks (user came from auction catalogue) vs external clicks (direct/search/referral)
- CSV Export — One-click export of all lot analytics data, filterable by catalogue
- Hot Lot Badge — Automatic "🔥 Popular" badge on catalogue page thumbnails for lots exceeding view threshold (default: 5 views)
- All new panels respect the catalogue filter dropdown
- New DB table: af_lot_shares (per-channel share events)
- New columns: catalogue_clicks, external_clicks, share_count on af_lot_analytics
- DB version bumped to 20
2.6.3.0
Fixes:
- View counter now shows on lot pages even with 0 views
- Fixed backfill and trending queries failing on URLs with trailing slashes
2.6.2.0
Fixes:
- Fixed 404 errors on auction/lot pages after DB version update by flushing rewrite rules automatically
2.6.1.0
Fixes:
- Renamed shortcodes to use bp_ prefix for consistency: [bp_popular_lots], [bp_most_favorited], [bp_trending_lots]
2.6.0.0
New Feature: Lot Analytics
- Added comprehensive lot view and favorite tracking system
- Two new settings in AuctionForge > Settings (both disabled by default):
- Enable Lot Analytics — master toggle for the entire tracking system
- Show View Counter on Lots — display a public view count on each lot page
- New admin dashboard tab: Lot Analytics (under Site Statistics) with:
- Auction Performance comparison table
- Most Viewed Lots (all time)
- Most Favorited Lots
- Trending Lots (last 7 days)
- Backfill from existing pageview data button
- Local favorites tracking mirrors BidSpirit favorites for analytics
- Three new shortcodes for displaying popular lots:
- — most viewed lots grid
- — most favorited lots grid
- — trending lots (configurable time window)
- All shortcodes support limit, columns, auction_id, and period filtering
- New DB tables: af_lot_analytics, af_lot_favorites (DB version 19)
2.5.9.0
Improvements:
- Redesigned Recently Viewed items as square card grid (rows/columns) instead of horizontal list.
- Changed lot thumbnail to contain (show full image) instead of cover (crop to fill).
2.5.8.0
Fixes:
- Fixed Recently Viewed cloned image thumbnails using non-existent resize prefix. Now uses raw image filename for reliable CDN loading.
2.5.7.0
Fixes:
- Fixed Recently Viewed thumbnail URLs — now builds correct BidSpirit CDN URLs for both cloned (Fastly) and non-cloned (Cloudinary) images instead of broken relative paths.
2.5.6.0
Fixes:
- Fixed placeholder image fallback path still referencing old uco-auctions directory.
- Added background color for empty thumbnail containers in Recently Viewed.
2.5.5.0
Improvements:
- Removed PAST/Upcoming badges from Recently Viewed items display.
- Added lot thumbnail images to Recently Viewed items with proper card layout.
2.5.4.0
Fixes:
- Fixed item variable not being passed to auctionIsLive-template (was lost during previous file edits).
2.5.3.0
Fixes:
- Fixed
item is not definedindisplayBidFormData— removed strayitem: itemfrom house-info-template call where item variable does not exist in scope.
2.5.2.0
Fixes:
- Fixed
item is not definedJavaScript error inauctionIsLive-templateon single lot pages. - Fixed statistics table auto-creation running on frontend requests (restricted to admin only).
2.5.1.0
Fixes:
- Fixed
hideleadingBidSection is not definedJavaScript error on single lot pages. - Fixed statistics database tables not auto-creating on plugin update (only activated on fresh install).
- Added self-healing table creation on
plugins_loadedfor sites that update without re-activating. - Cleaned up duplicate
AF_Stats_Init::activate()calls in DB update function. - Bumped DB version to 18.
2.5.0.0
New Features:
- Site Statistics System — Full website analytics built into the plugin admin (Lots → Site Statistics).
- Dashboard with KPI cards: visitors, sessions, pageviews, bounce rate, avg duration, pages per session.
- Period comparison: current vs previous period with percentage change indicators.
- Traffic over time line chart (pageviews, visitors, sessions).
- Traffic channels doughnut chart (organic search, direct, referral, social, email, paid).
- Hourly distribution bar chart.
- Device type pie chart (desktop, mobile, tablet).
- Top Pages tab with views, unique visitors, avg time on page, search, pagination, CSV export.
- Referrers tab with traffic channel badges, sessions, bounce rate, avg duration, CSV export.
- Countries tab with horizontal bar chart and visitor/pageview counts.
- Technology tab with browser, OS, device breakdown and screen resolutions.
- Searches tab tracking internal WordPress and auction search queries.
- 404 Errors tab tracking broken URLs with hit counts and referrers.
- Real-Time visitors view (last 5 minutes, auto-refreshes every 10 seconds).
- Settings tab: enable/disable tracking, exclude IPs and roles, IP anonymization (GDPR), data retention.
- Date range picker with presets (7, 14, 30, 90, 365 days) and custom range.
- Lightweight frontend tracker (~3KB JS) with session management, heartbeat, and beacon API.
- GeoIP country/city lookup via ip-api.com with 24-hour cache.
- Bot detection (30+ crawler patterns filtered).
- UTM parameter tracking (source, medium, campaign, term, content).
- Traffic channel auto-classification.
- Outbound link click tracking.
- Time-on-page tracking via 30-second heartbeat + page unload beacon.
- Daily cron for automatic data retention purge.
- Recently Viewed Items — New profile page tab showing the last 20 viewed lots per user.
- Admin setting to enable/disable (Lots → Settings → Enable user viewing history).
- Tracks lot views with timestamp, stores in custom DB table.
- Clear history button for users.
- Disabled by default.
Fixes:
- Fixed
settings is not definedJavaScript error on auction catalog and single lot pages. - Fixed
postAuctionSaleOpened is not definedJavaScript error on auction pages. - Fixed missing
vendor.min.cssandpassword-validation.min.css(404 errors). - Added missing template variables to all
loadTemplatecalls forlot-content-template,leadingBid-template.
2.4.4.0
Fixes:
- Fixed the display of purchase prices on shop pages for unsold items. Items with a buyout price now correctly show the "Purchase Price" in both ended and active auction states.
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added missing
-
v2.7.1.2 Stable Mar 10, 2026Download v2.7.1.2📦 5.05 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.1.2
Fixes:
- Added missing vatAndCommission() function in loadBid() — sales tax display on single lot pages was never rendering because the loadTemplate call for vat-and-commission-template was missing from app.js.
-
v2.7.1.1 Stable Mar 10, 2026Download v2.7.1.1📦 5.05 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
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.
-
v2.7.1.0 Stable Mar 10, 2026Download v2.7.1.0📦 5.05 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.1.0
Fixes:
- Fixed Buyers Premium value not showing on single lot pages — added missing auction-commission-field population in auctionInfoInit() from upstream reference.
-
v2.7.0.9 Stable Mar 10, 2026Download v2.7.0.9📦 5.05 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.0.9
Fixes:
- Fixed fatal error during plugin install/upgrade: class-stats-database.php require failure. Added file_exists guard and plugin-specific guard in upgrader_process_complete hook.
-
v2.7.0.8 Stable Mar 10, 2026Download v2.7.0.8📦 5.05 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.0.8
Fixes:
- Synced formattedPrice() across both app.js and app.min.js with second currency support, better null/NaN handling, and always 2 decimal places on all prices.
-
v2.7.0.7 Stable Mar 10, 2026Download v2.7.0.7📦 5.05 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.0.7
Fixes:
- All prices now display with 2 decimal places (e.g. $10.00 instead of $10) via formattedPrice() using toLocaleString with minimumFractionDigits: 2.
-
v2.7.0.6 Stable Mar 10, 2026Download v2.7.0.6📦 5.05 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.0.6
Fixes:
- Added missing estimatedPrice, content, and isShop template variables to widget and shortcode lot-content-template calls, preventing potential ReferenceErrors on catalogue pages with ended auctions or shop items.
-
v2.7.0.5 Stable Mar 10, 2026Download v2.7.0.5📦 5.05 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.0.5
Fixes:
- Fixed Stripe payment form not visible in Add Payment Method modal — #stripePaymentElement had hidden class that was never removed. Now unhides form on Stripe ready event and re-hides on modal close.
-
v2.7.0.4 Stable Mar 10, 2026Download v2.7.0.4📦 5.05 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.0.4
Fixes:
- Fixed
bidspiritApi.getAuctionSettingsData is not a functionerror in getFullPriceInfo — use local auctionSettingsData variable instead of non-existent API method.
- Fixed
-
v2.7.0.3 Stable Mar 10, 2026Download v2.7.0.3📦 5.05 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.0.3
Fixes:
- Fixed
fullPriceInfo is not definedReferenceError that prevented bid confirmation modal from opening on single lot pages and catalogue pages. - Added missing
getFullPriceInfo(),getCommissonInfoForPrice(), androundToCents()functions ported from upstream. - All three bid submission paths (submitBid, submitBidCatalog, submitBidCatalog3) now correctly pass fullPriceInfo to the modal-bid-template.
- Fixed
-
v2.7.0.2 Stable Mar 09, 2026Download v2.7.0.2📦 4.83 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.0.1
Fixes:
- Fixed catalog sorting (price, most active) not working on the regular (banner) template — sort_by parameter was not being applied to the main WordPress query
- Fixed user tracking (login/registration) not recording data — tracking functions were missing from the loaded JavaScript file
2.7.0.0
New Features: Advanced Lot Analytics
- Conversion Tracking — View-to-bid ratio table showing lot views, catalogue/external clicks, favorites, shares, bid status, and sold price with sell-through rate summary cards
- Peak Viewing Times — Hourly bar chart showing when lots get the most views (last 30 days) with peak hour identification
- Pre-Sale vs Live Comparison — Per-auction breakdown of views before auction day vs during/after, with pre-sale percentage
- Category Performance — Analytics grouped by lot category: views, lots, favorites, sold count, average views per lot
- Share Analytics — Track shares by channel (Facebook, Twitter, WhatsApp, Email, Pinterest, Copy Link) with per-channel badges and most-shared lots table
- Search → View Funnel — Correlates internal search queries with subsequent lot page views to show which searches lead to browsing
- Click-Through Tracking — Distinguishes catalogue clicks (user came from auction catalogue) vs external clicks (direct/search/referral)
- CSV Export — One-click export of all lot analytics data, filterable by catalogue
- Hot Lot Badge — Automatic "🔥 Popular" badge on catalogue page thumbnails for lots exceeding view threshold (default: 5 views)
- All new panels respect the catalogue filter dropdown
- New DB table: af_lot_shares (per-channel share events)
- New columns: catalogue_clicks, external_clicks, share_count on af_lot_analytics
- DB version bumped to 20
2.6.3.0
Fixes:
- View counter now shows on lot pages even with 0 views
- Fixed backfill and trending queries failing on URLs with trailing slashes
2.6.2.0
Fixes:
- Fixed 404 errors on auction/lot pages after DB version update by flushing rewrite rules automatically
2.6.1.0
Fixes:
- Renamed shortcodes to use bp_ prefix for consistency: [bp_popular_lots], [bp_most_favorited], [bp_trending_lots]
2.6.0.0
New Feature: Lot Analytics
- Added comprehensive lot view and favorite tracking system
- Two new settings in AuctionForge > Settings (both disabled by default):
- Enable Lot Analytics — master toggle for the entire tracking system
- Show View Counter on Lots — display a public view count on each lot page
- New admin dashboard tab: Lot Analytics (under Site Statistics) with:
- Auction Performance comparison table
- Most Viewed Lots (all time)
- Most Favorited Lots
- Trending Lots (last 7 days)
- Backfill from existing pageview data button
- Local favorites tracking mirrors BidSpirit favorites for analytics
- Three new shortcodes for displaying popular lots:
- — most viewed lots grid
- — most favorited lots grid
- — trending lots (configurable time window)
- All shortcodes support limit, columns, auction_id, and period filtering
- New DB tables: af_lot_analytics, af_lot_favorites (DB version 19)
2.5.9.0
Improvements:
- Redesigned Recently Viewed items as square card grid (rows/columns) instead of horizontal list.
- Changed lot thumbnail to contain (show full image) instead of cover (crop to fill).
2.5.8.0
Fixes:
- Fixed Recently Viewed cloned image thumbnails using non-existent resize prefix. Now uses raw image filename for reliable CDN loading.
2.5.7.0
Fixes:
- Fixed Recently Viewed thumbnail URLs — now builds correct BidSpirit CDN URLs for both cloned (Fastly) and non-cloned (Cloudinary) images instead of broken relative paths.
2.5.6.0
Fixes:
- Fixed placeholder image fallback path still referencing old uco-auctions directory.
- Added background color for empty thumbnail containers in Recently Viewed.
2.5.5.0
Improvements:
- Removed PAST/Upcoming badges from Recently Viewed items display.
- Added lot thumbnail images to Recently Viewed items with proper card layout.
2.5.4.0
Fixes:
- Fixed item variable not being passed to auctionIsLive-template (was lost during previous file edits).
2.5.3.0
Fixes:
- Fixed
item is not definedindisplayBidFormData— removed strayitem: itemfrom house-info-template call where item variable does not exist in scope.
2.5.2.0
Fixes:
- Fixed
item is not definedJavaScript error inauctionIsLive-templateon single lot pages. - Fixed statistics table auto-creation running on frontend requests (restricted to admin only).
2.5.1.0
Fixes:
- Fixed
hideleadingBidSection is not definedJavaScript error on single lot pages. - Fixed statistics database tables not auto-creating on plugin update (only activated on fresh install).
- Added self-healing table creation on
plugins_loadedfor sites that update without re-activating. - Cleaned up duplicate
AF_Stats_Init::activate()calls in DB update function. - Bumped DB version to 18.
2.5.0.0
New Features:
- Site Statistics System — Full website analytics built into the plugin admin (Lots → Site Statistics).
- Dashboard with KPI cards: visitors, sessions, pageviews, bounce rate, avg duration, pages per session.
- Period comparison: current vs previous period with percentage change indicators.
- Traffic over time line chart (pageviews, visitors, sessions).
- Traffic channels doughnut chart (organic search, direct, referral, social, email, paid).
- Hourly distribution bar chart.
- Device type pie chart (desktop, mobile, tablet).
- Top Pages tab with views, unique visitors, avg time on page, search, pagination, CSV export.
- Referrers tab with traffic channel badges, sessions, bounce rate, avg duration, CSV export.
- Countries tab with horizontal bar chart and visitor/pageview counts.
- Technology tab with browser, OS, device breakdown and screen resolutions.
- Searches tab tracking internal WordPress and auction search queries.
- 404 Errors tab tracking broken URLs with hit counts and referrers.
- Real-Time visitors view (last 5 minutes, auto-refreshes every 10 seconds).
- Settings tab: enable/disable tracking, exclude IPs and roles, IP anonymization (GDPR), data retention.
- Date range picker with presets (7, 14, 30, 90, 365 days) and custom range.
- Lightweight frontend tracker (~3KB JS) with session management, heartbeat, and beacon API.
- GeoIP country/city lookup via ip-api.com with 24-hour cache.
- Bot detection (30+ crawler patterns filtered).
- UTM parameter tracking (source, medium, campaign, term, content).
- Traffic channel auto-classification.
- Outbound link click tracking.
- Time-on-page tracking via 30-second heartbeat + page unload beacon.
- Daily cron for automatic data retention purge.
- Recently Viewed Items — New profile page tab showing the last 20 viewed lots per user.
- Admin setting to enable/disable (Lots → Settings → Enable user viewing history).
- Tracks lot views with timestamp, stores in custom DB table.
- Clear history button for users.
- Disabled by default.
Fixes:
- Fixed
settings is not definedJavaScript error on auction catalog and single lot pages. - Fixed
postAuctionSaleOpened is not definedJavaScript error on auction pages. - Fixed missing
vendor.min.cssandpassword-validation.min.css(404 errors). - Added missing template variables to all
loadTemplatecalls forlot-content-template,leadingBid-template.
2.4.4.0
Fixes:
- Fixed the display of purchase prices on shop pages for unsold items. Items with a buyout price now correctly show the "Purchase Price" in both ended and active auction states.
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
-
v2.7.0.0 Stable Mar 08, 2026Download v2.7.0.0📦 5.05 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.7.0.0
New Features: Advanced Lot Analytics
- Conversion Tracking — View-to-bid ratio table showing lot views, catalogue/external clicks, favorites, shares, bid status, and sold price with sell-through rate summary cards
- Peak Viewing Times — Hourly bar chart showing when lots get the most views (last 30 days) with peak hour identification
- Pre-Sale vs Live Comparison — Per-auction breakdown of views before auction day vs during/after, with pre-sale percentage
- Category Performance — Analytics grouped by lot category: views, lots, favorites, sold count, average views per lot
- Share Analytics — Track shares by channel (Facebook, Twitter, WhatsApp, Email, Pinterest, Copy Link) with per-channel badges and most-shared lots table
- Search → View Funnel — Correlates internal search queries with subsequent lot page views to show which searches lead to browsing
- Click-Through Tracking — Distinguishes catalogue clicks (user came from auction catalogue) vs external clicks (direct/search/referral)
- CSV Export — One-click export of all lot analytics data, filterable by catalogue
- Hot Lot Badge — Automatic "🔥 Popular" badge on catalogue page thumbnails for lots exceeding view threshold (default: 5 views)
- All new panels respect the catalogue filter dropdown
- New DB table: af_lot_shares (per-channel share events)
- New columns: catalogue_clicks, external_clicks, share_count on af_lot_analytics
- DB version bumped to 20
-
v2.6.3.0 Beta Mar 08, 2026Download v2.6.3.0📦 5.04 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.6.3.0
Fixes:
- View counter now shows on lot pages even with 0 views
- Fixed backfill and trending queries failing on URLs with trailing slashes
-
v2.6.2.0 Beta Mar 08, 2026Download v2.6.2.0📦 5.04 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.6.2.0
Fixes:
- Fixed 404 errors on auction/lot pages after DB version update by flushing rewrite rules automatically
-
v2.6.0.0 Beta Mar 08, 2026Download v2.6.0.0📦 5.04 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.6.0.0
New Feature: Lot Analytics
- Added comprehensive lot view and favorite tracking system
- Two new settings in AuctionForge > Settings (both disabled by default):
- Enable Lot Analytics — master toggle for the entire tracking system
- Show View Counter on Lots — display a public view count on each lot page
- New admin dashboard tab: Lot Analytics (under Site Statistics) with:
- Auction Performance comparison table
- Most Viewed Lots (all time)
- Most Favorited Lots
- Trending Lots (last 7 days)
- Backfill from existing pageview data button
- Local favorites tracking mirrors BidSpirit favorites for analytics
- Three new shortcodes for displaying popular lots:
- — most viewed lots grid
- — most favorited lots grid
- — trending lots (configurable time window)
- All shortcodes support limit, columns, auction_id, and period filtering
- New DB tables: af_lot_analytics, af_lot_favorites (DB version 19)
-
v2.5.9.0 Stable Mar 07, 2026Download v2.5.9.0📦 5.03 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.5.9.0
Improvements:
- Redesigned Recently Viewed items as square card grid (rows/columns) instead of horizontal list.
- Changed lot thumbnail to contain (show full image) instead of cover (crop to fill).
-
v2.5.8.0 Beta Mar 07, 2026Download v2.5.8.0📦 5.03 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.5.8.0
Fixes:
- Fixed Recently Viewed cloned image thumbnails using non-existent resize prefix. Now uses raw image filename for reliable CDN loading.
-
v2.5.7.0 Beta Mar 07, 2026Download v2.5.7.0📦 5.03 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.5.7.0
Fixes:
- Fixed Recently Viewed thumbnail URLs — now builds correct BidSpirit CDN URLs for both cloned (Fastly) and non-cloned (Cloudinary) images instead of broken relative paths.
-
v2.5.6.0 Beta Mar 07, 2026Download v2.5.6.0📦 5.03 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.5.6.0
Fixes:
- Fixed placeholder image fallback path still referencing old uco-auctions directory.
- Added background color for empty thumbnail containers in Recently Viewed.
-
v2.5.5.0 Beta Mar 07, 2026Download v2.5.5.0📦 5.03 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.5.5.0
Improvements:
- Removed PAST/Upcoming badges from Recently Viewed items display.
- Added lot thumbnail images to Recently Viewed items with proper card layout.
-
v2.5.4.0 Stable Mar 07, 2026Download v2.5.4.0📦 5.03 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.5.4.0
Fixes:
- Fixed item variable not being passed to auctionIsLive-template (was lost during previous file edits).
-
v2.5.3.0 Beta Mar 07, 2026Download v2.5.3.0📦 5.03 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.5.3.0
Fixes:
- Fixed
item is not definedindisplayBidFormData— removed strayitem: itemfrom house-info-template call where item variable does not exist in scope.
- Fixed
-
v2.5.2.0 Beta Mar 07, 2026Download v2.5.2.0📦 5.03 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.5.2.0
Fixes:
- Fixed
item is not definedJavaScript error inauctionIsLive-templateon single lot pages. - Fixed statistics table auto-creation running on frontend requests (restricted to admin only).
- Fixed
-
v2.5.1.0 Beta Mar 07, 2026Download v2.5.1.0📦 5.03 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.5.1.0
Fixes:
- Fixed
hideleadingBidSection is not definedJavaScript error on single lot pages. - Fixed statistics database tables not auto-creating on plugin update (only activated on fresh install).
- Added self-healing table creation on
plugins_loadedfor sites that update without re-activating. - Cleaned up duplicate
AF_Stats_Init::activate()calls in DB update function. - Bumped DB version to 18.
- Fixed
-
v2.5.0.0 Beta Mar 07, 2026Download v2.5.0.0📦 5.03 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.5.0.0
New Features:
- Site Statistics System — Full website analytics built into the plugin admin (Lots → Site Statistics).
- Dashboard with KPI cards: visitors, sessions, pageviews, bounce rate, avg duration, pages per session.
- Period comparison: current vs previous period with percentage change indicators.
- Traffic over time line chart (pageviews, visitors, sessions).
- Traffic channels doughnut chart (organic search, direct, referral, social, email, paid).
- Hourly distribution bar chart.
- Device type pie chart (desktop, mobile, tablet).
- Top Pages tab with views, unique visitors, avg time on page, search, pagination, CSV export.
- Referrers tab with traffic channel badges, sessions, bounce rate, avg duration, CSV export.
- Countries tab with horizontal bar chart and visitor/pageview counts.
- Technology tab with browser, OS, device breakdown and screen resolutions.
- Searches tab tracking internal WordPress and auction search queries.
- 404 Errors tab tracking broken URLs with hit counts and referrers.
- Real-Time visitors view (last 5 minutes, auto-refreshes every 10 seconds).
- Settings tab: enable/disable tracking, exclude IPs and roles, IP anonymization (GDPR), data retention.
- Date range picker with presets (7, 14, 30, 90, 365 days) and custom range.
- Lightweight frontend tracker (~3KB JS) with session management, heartbeat, and beacon API.
- GeoIP country/city lookup via ip-api.com with 24-hour cache.
- Bot detection (30+ crawler patterns filtered).
- UTM parameter tracking (source, medium, campaign, term, content).
- Traffic channel auto-classification.
- Outbound link click tracking.
- Time-on-page tracking via 30-second heartbeat + page unload beacon.
- Daily cron for automatic data retention purge.
- Recently Viewed Items — New profile page tab showing the last 20 viewed lots per user.
- Admin setting to enable/disable (Lots → Settings → Enable user viewing history).
- Tracks lot views with timestamp, stores in custom DB table.
- Clear history button for users.
- Disabled by default.
Fixes:
- Fixed
settings is not definedJavaScript error on auction catalog and single lot pages. - Fixed
postAuctionSaleOpened is not definedJavaScript error on auction pages. - Fixed missing
vendor.min.cssandpassword-validation.min.css(404 errors). - Added missing template variables to all
loadTemplatecalls forlot-content-template,leadingBid-template.
-
v2.4.4.0 Stable Mar 06, 2026Download v2.4.4.0📦 4.79 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.4.4.0
Fixes:
- Fixed the display of purchase prices on shop pages for unsold items. Items with a buyout price now correctly show the "Purchase Price" in both ended and active auction states.
-
v2.4.3.9 Stable Mar 04, 2026Download v2.4.3.9📦 4.79 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
-
v2.4.3.8 Stable Mar 02, 2026Download v2.4.3.8📦 4.79 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
-
v2.4.3.7 Stable Feb 24, 2026Download v2.4.3.7📦 4.79 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
-
v2.4.3.4 Stable Feb 23, 2026Download v2.4.3.4📦 4.79 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
-
v2.4.3.3 Stable Feb 23, 2026Download v2.4.3.3📦 5.01 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.9
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
-
v2.4.3.2 Stable Feb 23, 2026Download v2.4.3.2📦 5.01 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.9
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
-
v2.4.3.1 Stable Feb 23, 2026Download v2.4.3.1📦 5.01 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
-
v2.4.3.0 Stable Feb 23, 2026Download v2.4.3.0📦 5.01 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
-
v2.4.2.0 Stable Feb 23, 2026Download v2.4.2.0📦 4.79 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
-
v2.4.1.5 Stable Feb 22, 2026Download v2.4.1.5📦 4.78 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
-
v2.4.1.4 Stable Feb 21, 2026Download v2.4.1.4📦 4.78 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
-
v2.4.1.0 Stable Feb 19, 2026Download v2.4.1.0📦 4.78 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
-
v2.4.0.1 Stable Feb 18, 2026Download v2.4.0.1📦 4.78 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
-
v2.4.0.0 Stable Feb 17, 2026Download v2.4.0.0📦 4.77 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
-
v2.3.8.40 Stable Feb 16, 2026Download v2.3.8.40📦 4.99 MB⚙️ WP 5.2+🔧 PHP 7.2+✓ Tested 6.4
2.3.5
New Features:
- Added the ability to create a menu item that opens a modal login window. (
[href="#bp-profile"]) - Prices are now displayed with a secondary currency.
- Added an option to display prices including taxes.
Improvements:
- Login and registration pages no longer leave users on the default welcome page after authentication.
- Updated German localization.
- Added Dutch localization.
- Optimized the synchronization process for auction houses with a large number of auctions.
- Updated the user manual. Added a section with available shortcodes.
Fixes:
- Fixed a synchronization issue when default and secondary language settings were incorrect in the plugin.
- Fixed broken content in the *Make an inquiry about this item* link when special characters were present in the lot title.
- Fixed display of current bids for lots depending on the auction house settings.
- Fixed display of current bids for lots that are loaded when scrolling the page, if the "Enable pagination" option is disabled.
- Fixed handling of lots that were removed from the auction.
- Fixed bid display on the catalog page when the *enablePrebidsUnderStartPrice* option is enabled.
- Added the ability to create a menu item that opens a modal login window. (
Plugin Info
Requirements
-
⚙️WordPress5.2 or higher
-
🔧PHP7.2 or higher
-
✓Tested Up ToWordPress 6.4
-
📦File Size9.42 MB