Jake Vanderwerf
2026-05-13 226b50642af0895948fbaa623a9b7180399a63b6
=Queue fixes
13 files modified
639 ■■■■■ changed files
activate.php 1 ●●●● patch | view | raw | blame | history
assets/css/nav.min.css 2 ●●● patch | view | raw | blame | history
inc/admin/SEOAdmin.php 6 ●●●● patch | view | raw | blame | history
inc/helpers/all.php 2 ●●●●● patch | view | raw | blame | history
inc/integrations/PostMark.php 4 ●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php 1 ●●●● patch | view | raw | blame | history
inc/managers/ScriptLoader.php 2 ●●● patch | view | raw | blame | history
inc/managers/queue/Locker.php 42 ●●●●● patch | view | raw | blame | history
inc/managers/queue/Processor.php 43 ●●●●● patch | view | raw | blame | history
inc/managers/queue/Queue.php 85 ●●●●● patch | view | raw | blame | history
inc/managers/queue/Storage.php 442 ●●●● patch | view | raw | blame | history
inc/meta/Meta.php 8 ●●●● patch | view | raw | blame | history
inc/rest/routes/FormRoutes.php 1 ●●●● patch | view | raw | blame | history
activate.php
@@ -30,7 +30,6 @@
    do_action(BASE.'activation');
    error_log('Action done!');
    error_log('Checking custom tables...');
    Queue::defineTables();
    CustomTable::ensureTables();
    error_log('Dashboard is setup: '.print_r(JVB()->dashboard(), true));
assets/css/nav.min.css
@@ -1 +1 @@
nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;font-family:var(--heading)}nav,nav a,nav li,nav ol,nav ul{display:flex;flex-direction:var(--dir,row);justify-content:var(--justify,flex-start);align-items:var(--align,center);gap:var(--gap,0);flex-wrap:var(--wrap,nowrap);height:var(--btn,3rem);max-width:100%;padding:0;margin:0}nav li{width:100%;--justify:center;max-inline-size:none;padding:0;list-style:none}nav a,nav button{--justify:center;width:100%;white-space:nowrap;text-transform:uppercase;border-radius:0;background-color:transparent;text-decoration:none}nav a{padding:var(--padding)}nav .toggle{aspect-ratio:1;border:1px solid var(--base);color:var(--contrast)}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon-caret-down{transform:rotate(0);transition:transform var(--trans-base)}.open>.row>.toggle .icon-caret-down,.open>.toggle .icon-caret-down{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;right:0;max-height:0;transform:scaleY(0);transform-origin:top;width:100%;min-width:max-content;background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:max-height var(--trans-base),transform var(--trans-base);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.submenu a{height:var(--chipchip)}.open>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.screen-reader-text{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}nav a:focus:not(:focus-visible){outline:0}nav a:focus-visible{outline:1px solid var(--action-0);outline-offset:1px}nav.always{--dir:column;--justify:flex-end;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}nav.always>ul{z-index:1;--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;max-height:100%;position:relative;right:-300vw;width:100vw;padding:1rem 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{--wrap:wrap;--dir:row;height:max-content;--justify:flex-start;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;--justify:center;max-width:calc(100% - var(--btn))}nav.always .has-submenu>a{z-index:var(--z-3)}nav.always .has-submenu>button{width:var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;top:0;border:1px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--op-1))}nav.always .submenu li{background-color:rgba(var(--base-rgb),var(--op-3))}nav.always>.toggle{position:fixed;bottom:0;right:0;width:var(--btn);height:var(--btn);background-color:var(--base);color:var(--contrast);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);transition:width var(--trans-base)}nav.always>.toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>.toggle{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>.toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always button .icon-x,nav.always.open button .icon-list{display:none}nav.always button .icon-list,nav.always.open button .icon-x{display:block;--w:32px}@media (min-width:768px){nav.always>ul{padding-top:var(--btn)}}nav#breadcrumbs{height:max-content;--wrap:wrap;--gap:0;width:max-content;max-width:var(--full);position:absolute;background-color:rgba(var(--base-rgb),var(--op-4));font-size:var(--txt-x-small);padding:.125em;z-index:var(--z-5)}nav#breadcrumbs ol{height:max-content;--wrap:wrap}nav#breadcrumbs li{width:max-content;height:var(--chip);--wrap:nowrap}nav#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}nav#breadcrumbs li:last-of-type::after{display:none}nav#breadcrumbs a{height:var(--chip)}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;color:var(--contrast);text-transform:none}nav#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed{position:fixed;left:0;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom{bottom:0;width:calc(100% - var(--btn))}nav.fixed ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.fixed li{flex:1}nav.fixed a{--gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:100vw;z-index:var(--z-5);background-color:rgba(var(--base-rgb),var(--op-45));max-width:none}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}nav.on-this-page button{order:3;padding:0 1rem;width:max-content;aspect-ratio:unset}nav.on-this-page.open button{order:0}nav.on-this-page ul{width:100%;--gap:0}nav.on-this-page a{padding:0}nav.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}nav.on-this-page #back-to-top span{display:none}nav.on-this-page .active a{background-color:var(--action-0);color:var(--action-contrast)}nav.letters,nav.letters a,nav.letters li,nav.letters ul{height:var(--chip)}nav.letters li{max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}@media (min-width:768px){nav.letters,nav.letters ul{height:var(--chip)}nav.letters ul{--wrap:nowrap}nav.letters li{max-width:none}nav.letters a{padding:.25rem .66rem}}nav.index{--justify:space-between;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}nav.index ul{width:100%}nav.index li{flex-shrink:0;transform:scaleX(0);max-width:0;overflow:hidden}nav.index li.active,nav.index li.adj{transform:scaleX(1);width:calc(100% - var(--btn_));flex-shrink:1;max-width:none}nav.index li:first-of-type{flex-shrink:1;transform:scaleX(1);order:9999;width:var(--btn);height:var(--btn);max-width:none}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}nav.index a{border-bottom:4px solid transparent}nav.index .active a{border-color:var(--action-0);color:var(--contrast)}nav.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;--align:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}nav.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}nav.index.open a{--justify:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed,nav.condensed a,nav.condensed li,nav.condensed ul{height:max-content;width:max-content;--wrap:wrap;min-height:var(--chip)}nav.condensed{--gap:0 .25rem;width:100%}nav.condensed li+li::before{content:'·';padding:0 .25em}nav.condensed a{font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:transparent;color:var(--contrast);border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:stretch;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x}.always ul.socials{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{padding-bottom:2px;touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button.active{cursor:default}nav.tabs button{font-family:var(--heading);font-size:var(--txt-x-small);border-bottom:4px solid transparent}nav.tabs button.active,nav.tabs button.active:hover{background-color:var(--action-0);color:var(--action-contrast);border-color:var(--base)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height:0}.tab-content h2{margin:0 0 .5rem}.tab-content nav.tabs{height:max-content;background-color:var(--base);--gap:0}.tab-content .tab-content nav.tabs{background-color:var(--base-100)}.tab-content .tab-content .tab-content nav.tabs{background-color:var(--base-200)}.tab-content nav.tabs button.active h2{color:var(--action-0)}nav.menu a{padding:.5rem .66rem}nav.share{height:max-content;margin:1rem 0}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--txt-x-small)}.wp-site-blocks>header,body>header{--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:center;padding:0 .5rem;background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}.wp-site-blocks>header img{width:var(--btn)}.dashboard-nav{width:100%}nav.filters{--dir:row;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem}nav.term-navigation:has([hidden]){display:none}
nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;font-family:var(--heading)}nav,nav a,nav li,nav ol,nav ul{display:flex;flex-direction:var(--dir,row);justify-content:var(--justify,flex-start);align-items:var(--align,center);gap:var(--gap,0);flex-wrap:var(--wrap,nowrap);height:var(--btn,3rem);max-width:100%;padding:0;margin:0}nav li{width:100%;--justify:center;max-inline-size:none;padding:0;list-style:none}nav a,nav button{--justify:center;width:100%;white-space:nowrap;text-transform:uppercase;border-radius:0;background-color:transparent;text-decoration:none}nav a{padding:var(--padding)}nav .toggle{aspect-ratio:1;border:1px solid var(--base);color:var(--contrast)}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon-caret-down{transform:rotate(0);transition:transform var(--trans-base)}.open>.row>.toggle .icon-caret-down,.open>.toggle .icon-caret-down{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;right:0;max-height:0;transform:scaleY(0);transform-origin:top;width:100%;min-width:max-content;background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:max-height var(--trans-base),transform var(--trans-base);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.submenu a{height:var(--chipchip)}.open>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.screen-reader-text{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}nav a:focus:not(:focus-visible){outline:0}nav a:focus-visible{outline:1px solid var(--action-0);outline-offset:1px}nav.always{--dir:column;--justify:flex-end;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}nav.always>ul{z-index:1;--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;max-height:100%;position:relative;right:-300vw;width:100vw;padding:var(--btn) 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{--wrap:wrap;--dir:row;height:max-content;--justify:flex-start;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;--justify:center;max-width:calc(100% - var(--btn))}nav.always .has-submenu>a{z-index:var(--z-3)}nav.always .has-submenu>button{width:var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;top:0;border:1px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--op-1))}nav.always .submenu li{background-color:rgba(var(--base-rgb),var(--op-3))}nav.always>.toggle{position:fixed;bottom:0;right:0;width:var(--btn);height:var(--btn);background-color:var(--base);color:var(--contrast);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);transition:width var(--trans-base)}nav.always>.toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>.toggle{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>.toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always button .icon-x,nav.always.open button .icon-list{display:none}nav.always button .icon-list,nav.always.open button .icon-x{display:block;--w:32px}nav#breadcrumbs{height:max-content;--wrap:wrap;--gap:0;width:max-content;max-width:var(--full);position:absolute;background-color:rgba(var(--base-rgb),var(--op-4));font-size:var(--txt-x-small);padding:.125em;z-index:var(--z-5)}nav#breadcrumbs ol{height:max-content;--wrap:wrap}nav#breadcrumbs li{width:max-content;height:var(--chip);--wrap:nowrap}nav#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}nav#breadcrumbs li:last-of-type::after{display:none}nav#breadcrumbs a{height:var(--chip)}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;color:var(--contrast);text-transform:none}nav#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed{position:fixed;left:0;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom{bottom:0;width:calc(100% - var(--btn))}nav.fixed ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.always.fixed>ul{padding-top:var(--btn)}nav.fixed li{flex:1}nav.fixed a{--gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:100vw;z-index:var(--z-5);background-color:rgba(var(--base-rgb),var(--op-45));max-width:none}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}nav.on-this-page button{order:3;padding:0 1rem;width:max-content;aspect-ratio:unset}nav.on-this-page.open button{order:0}nav.on-this-page ul{width:100%;--gap:0}nav.on-this-page a{padding:0}nav.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}nav.on-this-page #back-to-top span{display:none}nav.on-this-page .active a{background-color:var(--action-0);color:var(--action-contrast)}nav.letters,nav.letters a,nav.letters li,nav.letters ul{height:var(--chip)}nav.letters li{max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}@media (min-width:768px){nav.letters,nav.letters ul{height:var(--chip)}nav.letters ul{--wrap:nowrap}nav.letters li{max-width:none}nav.letters a{padding:.25rem .66rem}}nav.index{--justify:space-between;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}nav.index ul{width:100%}nav.index li{flex-shrink:0;transform:scaleX(0);max-width:0;overflow:hidden}nav.index li.active,nav.index li.adj{transform:scaleX(1);width:calc(100% - var(--btn_));flex-shrink:1;max-width:none}nav.index li:first-of-type{flex-shrink:1;transform:scaleX(1);order:9999;width:var(--btn);height:var(--btn);max-width:none}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}nav.index a{border-bottom:4px solid transparent}nav.index .active a{border-color:var(--action-0);color:var(--contrast)}nav.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;--align:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}nav.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}nav.index.open a{--justify:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed,nav.condensed a,nav.condensed li,nav.condensed ul{height:max-content;width:max-content;--wrap:wrap;min-height:var(--chip)}nav.condensed{--gap:0 .25rem;width:100%}nav.condensed li+li::before{content:'·';padding:0 .25em}nav.condensed a{font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:transparent;color:var(--contrast);border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:stretch;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x}.always ul.socials{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{padding-bottom:2px;touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button.active{cursor:default}nav.tabs button{font-family:var(--heading);font-size:var(--txt-x-small);border-bottom:4px solid transparent}nav.tabs button.active,nav.tabs button.active:hover{background-color:var(--action-0);color:var(--action-contrast);border-color:var(--base)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height:0}.tab-content h2{margin:0 0 .5rem}.tab-content nav.tabs{height:max-content;background-color:var(--base);--gap:0}.tab-content .tab-content nav.tabs{background-color:var(--base-100)}.tab-content .tab-content .tab-content nav.tabs{background-color:var(--base-200)}.tab-content nav.tabs button.active h2{color:var(--action-0)}nav.menu a{padding:.5rem .66rem}nav.share{height:max-content;margin:1rem 0}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--txt-x-small)}.wp-site-blocks>header,body>header{--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:center;padding:0 .5rem;background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}.wp-site-blocks>header img{width:var(--btn)}.dashboard-nav{width:100%}nav.filters{--dir:row;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem}nav.term-navigation:has([hidden]){display:none}
inc/admin/SEOAdmin.php
@@ -145,7 +145,7 @@
    public function addDashboardSection(string $content, string $page):string
    {
        if ($page !== 'jvb-seo') {
        if ($page !== 'SEO') {
            return $content;
        }
        ob_start();
@@ -203,6 +203,10 @@
            $this->renderStyles();
        }
    }
        private function renderStyles(): void
        {
            jvbInlineStyles('forms');
        }
    public function renderProperty(string $property, ?string $value, mixed $class):void
    {
inc/helpers/all.php
@@ -54,6 +54,8 @@
//    delete_option(BASE.'do_these_once');
    //Ensure we have the option starting with BASE
    $option = jvbCheckBase($option);
//  delete_option($option);
//  delete_option(BASE.'do_these_once');
    $options = get_option(BASE.'do_these_once', []);
//    delete_option($option);
    if (!array_key_exists($option, $options)) {// Prevent concurrent runs
inc/integrations/PostMark.php
@@ -166,10 +166,12 @@
        $result = $this->sendEmail($payload);
        if ($result === true) {
            error_log('================================ Email sent! ================================');
            // Prevent default wp_mail from sending
            add_filter('pre_wp_mail', '__return_true');
            do_action('postmark_email_sent', $args, $payload);
        } else {
            error_log('=-======================[POSTMARK]Something went wrong... ================================');
            // Log failure but allow fallback to default mail
            do_action('postmark_email_failed', $args, $result);
@@ -263,7 +265,7 @@
        }
        try {
            $response = $this->postRequest('email', $payload);
            error_log('================================ POSTMARK RESPONSE: ================================'.print_r($response, true));
            if (is_wp_error($response)) {
                return $response;
            }
inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php
@@ -2,3 +2,4 @@
require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/AdministrativeArea.php');
require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php');
require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/Country.php');
require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/State.php');
inc/managers/ScriptLoader.php
@@ -2,7 +2,7 @@
add_action('init', 'jvbRegisterScripts', 5);
function jvbRegisterScripts() {
    $version = '1.1.5';
    $version = '1.1.55';
    $strategy = [
        'strategy'  => 'defer',
        'in_footer' => true
inc/managers/queue/Locker.php
@@ -1,43 +1,24 @@
<?php
namespace JVBase\managers\queue;
use wpdb;
if (!defined('ABSPATH')) {
    exit;
}
class Locker
{
    private string $lockKey;
    private int $timeout;
    protected wpdb $wpdb;
    private ?string $token = null;
    public function __construct(string $key = 'queue', int $timeout = 0)
    public function __construct(string $key = 'queue', int $timeout = 60)
    {
        $this->lockKey = BASE . $key . '_lock';
        $this->timeout = $timeout;
        global $wpdb;
        $this->wpdb = $wpdb;
    }
    /**
     * Execute callback with lock, auto-release after
     */
    public function withLock(callable $callback): void
    {
        $acquired = $this->wpdb->get_var(
            $this->wpdb->prepare(
                'SELECT GET_LOCK(%s, %d)',
                $this->lockKey,
                $this->timeout
            )
        );
        if ((int) $acquired !== 1) {
            // Lock already held — just exit quietly
            return;
        }
        if (!$this->acquire()) return;
        try {
            $callback();
@@ -46,13 +27,18 @@
        }
    }
    public function unlock():void
    private function acquire(): bool
    {
        $this->wpdb->get_var(
            $this->wpdb->prepare(
                'SELECT RELEASE_LOCK(%s)',
                $this->lockKey
            )
        );
        $this->token = bin2hex(random_bytes(8));
        return (bool) wp_cache_add($this->lockKey, $this->token, 'locks', $this->timeout);
    }
    public function unlock(): void
    {
        $current = wp_cache_get($this->lockKey, 'locks');
        if ($current === $this->token) {
            wp_cache_delete($this->lockKey, 'locks');
        }
        $this->token = null;
    }
}
inc/managers/queue/Processor.php
@@ -14,48 +14,39 @@
    public function run(): void
    {
        if (get_transient(BASE.'queue_running')) {
            return;
        }
        set_transient(BASE.'queue_running', true, 60);
        if (!$this->hasAdequateResources()) {
            error_log('[Processor] Insufficient resources to start processing');
            return;
        }
        $ops = $this->storage->fetchRunnable();
        if (empty($ops)) {
            return;
        }
        foreach ($ops as $op) {
            if ($op->state === 'completed') {
                return;
        $op = null;
        $this->storage->withTransaction(function() use (&$op) {
            $candidates = $this->storage->fetchRunnable();
            foreach ($candidates as $candidate) {
                if ($candidate->state === 'completed') continue;
                if (!$this->dependenciesSatisfied($candidate)) continue;
                if ($this->storage->markProcessing($candidate->id)) {
                    $op = $candidate;
                    break;
                }
            }
            if (!$this->dependenciesSatisfied($op)) {
                continue;
            }
            if (!$this->storage->markProcessing($op->id)) {
                continue;
            }
            $this->processOne($op);
            usleep(10000);
        }
        });
        if (!$op) return;
        $this->processOne($op);
        usleep(10000);
        $this->storage->invalidateQueueCache();
    }
    private function processOne(Operation $op): void
    {
        if (get_transient(BASE.$op->id)) {
            return;
        }
        set_transient(BASE.$op->id, true, 500);
        $progress = new Progress($op);
        $executor = $this->registry->getExecutor($op->type) ?? $this->defaultExecutor;
        $op->startedAt = current_time('mysql');
        $op->state = 'processing';
        $this->storage->saveProgress($op);
        try {
inc/managers/queue/Queue.php
@@ -19,7 +19,6 @@
    public function __construct()
    {
        $this->defineTables();
        $this->storage = new Storage();
        $this->registry = new TypeRegistry();
        $this->locker = new Locker();
@@ -29,6 +28,7 @@
        add_action('jvb_process_queue', [$this, 'checkQueue']);
        add_action('jvb_queue_maintenance', [$this, 'maintenance']);
        add_action('jvb_daily_snapshot', [$this->storage, 'snapshotDaily']);
        if (!wp_next_scheduled('jvb_process_queue')) {
            wp_schedule_event(time(), 'every-minute', 'jvb_process_queue');
@@ -36,89 +36,16 @@
        if (!wp_next_scheduled('jvb_queue_maintenance')) {
            wp_schedule_event(time(), 'hourly', 'jvb_queue_maintenance');
        }
        if (!wp_next_scheduled('jvb_daily_snapshot')) {
            // Schedule for next 3am
            $next3am = strtotime('tomorrow 3am', current_time('timestamp'));
            wp_schedule_event($next3am, 'daily', 'jvb_daily_snapshot');
        }
        jvb_register_do_once('queue_admin_action_registered', [$this, 'registerAdminAction']);
        add_filter(BASE.'admin_action_filter', [$this, 'adminActionFilter'], 10, 3);
    }
    public static function defineTables():void
    {
        $queue = CustomTable::for('_operation_queue');
        $queue->setColumns([
            'id'            => 'VARCHAR(64) NOT NULL',
            'type'          => 'VARCHAR(50) NOT NULL',
            'user_id'       => $queue->getUserIDType().' NOT NULL',
            'request_data'  => 'JSON NOT NULL CHECK (JSON_VALID(request_data))',
            'total_items'   => 'INT(11) NOT NULL DEFAULT 1',
            'processed_items'   => 'INT(11) DEFAULT 0',
            'failed_items'  => 'JSON',
            'priority'      => 'ENUM(\'high\',\'normal\',\'low\') DEFAULT \'normal\'',
            'state'         => 'ENUM(\'pending\', \'scheduled\', \'processing\', \'completed\') DEFAULT \'pending\'',
            'outcome'       => 'ENUM(\'pending\', \'success\',\'partial\',\'merged\',\'failed\',\'failed_permanent\') DEFAULT \'pending\'',
            'retries'       => 'INT(11) DEFAULT 0',
            'last_error_hash'=> 'CHAR(32) DEFAULT NULL',
            'error_message' => 'TEXT',
            'scheduled_at'  => 'DATETIME DEFAULT NULL',
            'started_at'    => 'DATETIME DEFAULT CURRENT_TIMESTAMP',
            'completed_at'  => 'DATETIME DEFAULT NULL',
            'metadata'      => 'JSON DEFAULT NULL',
            'result'        => 'JSON',
            'dependencies'  => 'JSON',
            'merged_into'   => 'VARCHAR(64) DEFAULT NULL',
            'user_dismissed'=> 'tinyint(1) DEFAULT 0',
            'created_at'    => 'DATETIME DEFAULT CURRENT_TIMESTAMP',
            'updated_at'    => 'DATETIME DEFAULT CURRENT_TIMESTAMP',
        ]);
        $queue->setKeys([
            ['key' => 'PRIMARY', 'value' => '(`id`)'],
            '`idx_run_queue` (`state`, `priority`, `scheduled_at`)',
            '`idx_user_ops` (`user_id`, `state`)',
            '`idx_user_type_pending` (`user_id`, `type`, `state`)',
            '`idx_completed_at` (`completed_at`)',
            '`idx_processing_stuck` (`state`, `started_at`)'
        ]);
        $queue->defineTable();
        $stats = CustomTable::for('stats__operation_queue');
        $stats->setColumns([
            'id'    => 'BIGINT unsigned AUTO_INCREMENT',
            'date'  => 'DATE NOT NULL',
            'type'  => 'VARCHAR(50) NOT NULL',
            'total_operations'      => 'INT NOT NULL DEFAULT 0',
            'successful_operations' => 'INT NOT NULL DEFAULT 0',
            'partial_operations'    => 'INT NOT NULL DEFAULT 0',
            'failed_operations'     => 'INT NOT NULL DEFAULT 0',
            'failed_permanent_operations'=> 'INT NOT NULL DEFAULT 0',
            'total_items_processed' => 'INT NOT NULL DEFAULT 0',
            'average_duration'      => 'FLOAT DEFAULT NULL',
            'max_duration'          => 'INT DEFAULT NULL',
            'peak_queue_size'       => 'INT NOT NULL DEFAULT 0',
            'peak_memory_usage'     => 'INT DEFAULT NULL',
            'peak_cpu_usage'        => 'FLOAT DEFAULT NULL',
            'created_at'            => 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
        ]);
        $stats->setKeys([
            ['key' => 'PRIMARY', 'value' => '(`id`)'],
            ['key' => 'UNIQUE', 'value' => '(`date`, `type`)'],
            '`date_idx` (`date`)',
            '`type_idx` (`type`)'
        ]);
        $stats->defineTable();
    }
    /**
     * Access type registry for registering operation configs
     */
inc/managers/queue/Storage.php
@@ -5,48 +5,39 @@
}
use JVBase\managers\Cache;
use JVBase\managers\CustomTable;
use LogicException;
class Storage
{
    private \wpdb $wpdb;
    private string $table;
    private CustomTable $table;
    private CustomTable $stats;
    private Cache $cache;
    private const CACHE_QUEUE_INFO = 'queue_info';
    public function __construct()
    {
        global $wpdb;
        $this->wpdb = $wpdb;
        $this->table = $wpdb->prefix . BASE . '_operation_queue';
        $this->defineTables();
        $this->cache = Cache::for('queue', DAY_IN_SECONDS);
    }
    public function hasProcessingOperations(): bool
    {
        return (bool) $this->wpdb->get_var(
            "SELECT 1 FROM {$this->table} WHERE state = 'processing' LIMIT 1"
        );
        return (bool) $this->table->queryVar("SELECT 1 FROM {table} WHERE state = 'processing' LIMIT 1");
    }
    public function fetchRunnable(int $offset = 0): array
    {
        $now = current_time('mysql');
        $rows = $this->wpdb->get_results(
            $this->wpdb->prepare("
            SELECT *
            FROM {$this->table}
            WHERE state IN ('pending', 'scheduled')
              AND scheduled_at <= %s
            ORDER BY
              FIELD(priority, 'high', 'normal', 'low'),
              scheduled_at
            LIMIT 10 OFFSET %d
            FOR UPDATE SKIP LOCKED
        ", $now, $offset)
        );
        $rows = $this->table->queryResults("
        SELECT * FROM {table}
        WHERE state IN ('pending', 'scheduled')
          AND scheduled_at <= %s
        ORDER BY FIELD(priority, 'high', 'normal', 'low'), scheduled_at
        LIMIT 10 OFFSET %d
        FOR UPDATE SKIP LOCKED
    ", [$now, $offset]);
        $total = count($rows);
        foreach ($rows as $row) {
@@ -88,26 +79,22 @@
    public function markProcessing(string $id): bool
    {
        $now = current_time('mysql');
        $affected = $this->wpdb->query($this->wpdb->prepare("
            UPDATE {$this->table}
            SET state = 'processing', started_at = %s, updated_at = %s
            WHERE id = %s AND state IN ('pending', 'scheduled')
        ", $now, $now, $id));
        $affected = $this->table->query("
        UPDATE {table}
        SET state = 'processing', started_at = %s, updated_at = %s
        WHERE id = %s AND state IN ('pending', 'scheduled')
    ", [$now, $now, $id]);
        if ($affected > 0) {
            $op = $this->find($id);
            if ($op) {
                $this->invalidateUser($op->userId);
            }
            if ($op) $this->invalidateUser($op->userId);
        }
        return $affected > 0;
    }
    public function save(Operation $op): bool
    {
        $data = [
        $result = $this->table->update([
            'request_data'    => json_encode($op->requestData),
            'total_items'     => $op->totalItems,
            'processed_items' => $op->processedItems,
@@ -118,23 +105,17 @@
            'retries'         => $op->retries,
            'last_error_hash' => $op->lastErrorHash,
            'error_message'   => $op->errorMessage,
            'scheduled_at'    => $op->scheduledAt,
            'started_at'      => $op->startedAt,
            'completed_at'    => $op->completedAt,
            'metadata'        => json_encode($op->metadata),
            'result'          => $op->result ? json_encode($op->result) : null,
            'dependencies'    => json_encode($op->dependencies),
            'merged_into'     => $op->merged_into,
            'user_dismissed'  => $op->userDismissed ? 1 : 0,
            'updated_at'      => current_time('mysql'),
        ];
        ],
            ['id' => $op->id]
        );
        $result = $this->wpdb->update($this->table, $data, ['id' => $op->id]);
        if ($result !== false) {
            $this->invalidateUser($op->userId);
        }
        if ($result !== false) $this->invalidateUser($op->userId);
        return $result !== false;
    }
@@ -143,35 +124,17 @@
     * @param Operation $op
     * @return bool
     */
    public function saveProgress(Operation $op): bool {
        global $wpdb;
        $table = $this->table;
        $data = [
    public function saveProgress(Operation $op): bool
    {
        $result = $this->table->update([
            'processed_items' => $op->processedItems ?? 0,
            'failed_items'    => $op->failedItems ? json_encode($op->failedItems) : null,
            'metadata'        => ($op->metadata) ? json_encode($op->metadata) : null,
            'result'          => ($op->result) ? json_encode($op->result) :null,
            'updated_at'      => current_time('mysql'),
        ];
            'metadata'        => $op->metadata ? json_encode($op->metadata) : null,
            'result'          => $op->result ? json_encode($op->result) : null,
        ], ['id' => $op->id, 'state' => 'processing']); // state guard preserved
        // IMPORTANT: never touch terminal state
        $where = [
            'id'    => $op->id,
            'state' => 'processing',
        ];
        $updated = $wpdb->update($table, $data, $where);
        if ($updated === false) {
            error_log('[Storage::saveProgress] DB error: ' . $wpdb->last_error);
            return false;
        }
        if ($result === false) return false;
        $this->invalidateUser($op->userId);
        return true;
    }
@@ -180,50 +143,32 @@
     * @param Operation $op
     * @return bool
     */
    public function saveFinal(Operation $op): bool {
        global $wpdb;
        if (($op->state?? null) !== 'completed') {
    public function saveFinal(Operation $op): bool
    {
        if ($op->state !== 'completed') {
            throw new LogicException('saveFinal called without completed state');
        }
        $table = $this->table;
        $result = $this->table->update([
            'state'           => 'completed',
            'outcome'         => $op->outcome ?? 'success',
            'processed_items' => $op->processedItems ?? 0,
            'failed_items'    => $op->failedItems ? json_encode($op->failedItems) : null,
            'result'          => isset($op->result) ? wp_json_encode($op->result) : null,
            'completed_at'    => $op->completedAt ?? current_time('mysql'),
        ], ['id' => $op->id, 'state' => 'processing']); // hard guard preserved
        $data = [
            'state'          => 'completed',
            'outcome'        => $op->outcome?? 'success',
            'processed_items'=> $op->processedItems ?? 0,
            'failed_items'   => $op->failedItems ? json_encode($op->failedItems) : null,
            'result'         => isset($op->result) ? wp_json_encode($op->result) : null,
            'completed_at'   => $op->completedAt ?? current_time('mysql'),
            'updated_at'     => current_time('mysql'),
        ];
        if ($result === 0) return true; // already completed, not an error
        if ($result === false) return false;
        // HARD GUARD: cannot overwrite completed
        $where = [
            'id'    => $op->id,
            'state' => 'processing',
        ];
        $updated = $wpdb->update($table, $data, $where);
        if ($updated === 0) {
            return true;
        }
        if ($updated === false) {
            error_log('[Storage::saveFinal] DB error: ' . $wpdb->last_error);
            return false;
        }
        $this->invalidateQueueCache();
        $this->invalidateUser($op->userId);
        return true;
    }
    public function insert(Operation $op): bool
    {
        $result = $this->wpdb->insert($this->table, [
        $result = $this->table->insert([
            'id'              => $op->id,
            'type'            => $op->type,
            'user_id'         => $op->userId,
@@ -235,18 +180,10 @@
            'state'           => $op->state,
            'outcome'         => $op->outcome,
            'retries'         => 0,
            'last_error_hash' => null,
            'error_message'   => null,
            'scheduled_at'    => $op->scheduledAt ?? current_time('mysql'),
            'started_at'      => null,
            'completed_at'    => null,
            'metadata'        => json_encode($op->metadata),
            'result'          => null,
            'dependencies'    => json_encode($op->dependencies),
            'user_dismissed'  => 0,
            'merged_into'     => null,
            'created_at'      => current_time('mysql'),
            'updated_at'      => current_time('mysql'),
        ]);
        if ($result) {
@@ -258,36 +195,25 @@
    public function find(string $id): ?Operation
    {
        $row = $this->wpdb->get_row($this->wpdb->prepare(
            "SELECT * FROM {$this->table} WHERE id = %s",
            $id
        ));
        $row = $this->table->get(['id' => $id]);
        return $row ? $this->rowToOperation($row) : null;
    }
    public function findMergeable(string $type, int $userId, array $criteria = []): ?Operation
    {
        $sql = "SELECT * FROM {$this->table}
            WHERE type = %s AND user_id = %d AND state IN ('pending', 'scheduled')";
        $sql = "SELECT * FROM {table} WHERE type = %s AND user_id = %d AND state IN ('pending', 'scheduled')";
        $params = [$type, $userId];
        foreach ($criteria as $key => $value) {
            if ($value === null) {
                continue;
            }
            if ($value === null) continue;
            $sql .= " AND JSON_UNQUOTE(JSON_EXTRACT(request_data, %s)) = %s";
            $params[] = '$.' . $key;
            $params[] = (string) $value;
        }
        $sql .= " ORDER BY created_at DESC LIMIT 1";
        $row = $this->wpdb->get_row($this->wpdb->prepare($sql, ...$params));
        $this->invalidateUser($userId);
        return $row ? $this->rowToOperation($row) : null;
        $rows = $this->table->queryResults($sql, $params);
        return !empty($rows) ? $this->rowToOperation($rows[0]) : null;
    }
    public function getUserOperations(int $userId, array $filters = []): array
@@ -328,14 +254,12 @@
        // Order by state priority, then created_at
        $orderBy = $filters['order_by'] ?? "FIELD(state, 'processing', 'pending', 'scheduled', 'completed'), created_at DESC";
        $limit = $filters['limit'] ?? 50;
        $params[] = $limit;
        $params[] = $filters['limit'] ?? 50;
        $rows = $this->wpdb->get_results($this->wpdb->prepare(
            "SELECT * FROM {$this->table} WHERE " . implode(' AND ', $where) .
            " ORDER BY {$orderBy} LIMIT %d",
            ...$params
        ));
        $rows = $this->table->queryResults(
            "SELECT * FROM {table} WHERE " . implode(' AND ', $where) . " ORDER BY {$orderBy} LIMIT %d",
            $params
        );
        return array_map([$this, 'rowToOperation'], $rows ?: []);
    }
@@ -343,19 +267,17 @@
    public function getQueueInfo(): array
    {
        $cached = $this->cache->get(self::CACHE_QUEUE_INFO);
        if ($cached !== false) {
            return $cached;
        }
        if ($cached !== false) return $cached;
        $now = current_time('mysql');
        $row = $this->wpdb->get_row($this->wpdb->prepare("
            SELECT
                COUNT(*) as total,
                SUM(IF(state IN ('pending', 'processing'), 1, 0)) as active,
                SUM(IF(state = 'scheduled' AND scheduled_at <= %s, 1, 0)) as ready_scheduled
            FROM {$this->table}
            WHERE state IN ('pending', 'processing', 'scheduled')
        ", $now));
        $row = $this->table->queryResults("
        SELECT COUNT(*) as total,
            SUM(IF(state IN ('pending', 'processing'), 1, 0)) as active,
            SUM(IF(state = 'scheduled' AND scheduled_at <= %s, 1, 0)) as ready_scheduled
        FROM {table}
        WHERE state IN ('pending', 'processing', 'scheduled')
    ", [$now]);
        $row = $row[0] ?? null;
        $info = [
            'total'     => (int) ($row->total ?? 0),
@@ -399,33 +321,28 @@
    public function getQueueStatus(): array
    {
        $now = current_time('mysql');
        $rows = $this->table->queryResults("
        SELECT state, COUNT(*) as count,
            SUM(IF(state = 'scheduled' AND scheduled_at <= %s, 1, 0)) as ready
        FROM {table} GROUP BY state
    ", [$now]);
        $rows = $this->wpdb->get_results($this->wpdb->prepare("
            SELECT
                state,
                COUNT(*) as count,
                SUM(IF(state = 'scheduled' AND scheduled_at <= %s, 1, 0)) as ready
            FROM {$this->table}
            GROUP BY state
        ", $now), OBJECT_K);
        $indexed = array_column($rows, null, 'state');
        return [
            'pending'         => (int) ($rows['pending']->count ?? 0),
            'scheduled'       => (int) ($rows['scheduled']->count ?? 0),
            'scheduled_ready' => (int) ($rows['scheduled']->ready ?? 0),
            'processing'      => (int) ($rows['processing']->count ?? 0),
            'completed'       => (int) ($rows['completed']->count ?? 0),
            'pending'         => (int) ($indexed['pending']->count ?? 0),
            'scheduled'       => (int) ($indexed['scheduled']->count ?? 0),
            'scheduled_ready' => (int) ($indexed['scheduled']->ready ?? 0),
            'processing'      => (int) ($indexed['processing']->count ?? 0),
            'completed'       => (int) ($indexed['completed']->count ?? 0),
        ];
    }
    public function getUserStats(int $userId): array
    {
        $rows = $this->wpdb->get_results($this->wpdb->prepare("
            SELECT state, outcome, COUNT(*) as count
            FROM {$this->table}
            WHERE user_id = %d
            GROUP BY state, outcome
        ", $userId));
        $rows = $this->table->queryResults(
            "SELECT state, outcome, COUNT(*) as count FROM {table} WHERE user_id = %d GROUP BY state, outcome",
            [$userId]
        );
        $stats = [
            'pending' => 0,
@@ -458,13 +375,7 @@
    public function dismiss(string $id): bool
    {
        $result = $this->wpdb->update(
            $this->table,
            ['user_dismissed' => 1, 'updated_at' => current_time('mysql')],
            ['id' => $id]
        );
        return $result !== false;
        return $this->table->update(['user_dismissed' => 1], ['id' => $id]) !== false;
    }
    /**
@@ -473,14 +384,8 @@
    public function delete(string $id): bool
    {
        $op = $this->find($id);
        $userId = $op?->userId;
        $result = $this->wpdb->delete($this->table, ['id' => $id]);
        if ($result && $userId) {
            $this->invalidateUser($userId);
        }
        $result = $this->table->delete(['id' => $id]);
        if ($result && $op) $this->invalidateUser($op->userId);
        return $result !== false;
    }
@@ -493,21 +398,16 @@
    {
        Cache::for($userId.'_queue')->flush();
    }
    public function getLastError(): string
    {
        return $this->wpdb->last_error;
    }
    public function withTransaction(callable $callback): mixed
    {
        $this->wpdb->query('START TRANSACTION');
        $this->table->startTransaction();
        try {
            $result = $callback();
            $this->wpdb->query('COMMIT');
            $this->table->commit();
            return $result;
        } catch (\Throwable $e) {
            $this->wpdb->query('ROLLBACK');
            $this->table->rollback();
            error_log('[Storage] Transaction rolled back: ' . $e->getMessage());
            throw $e;
        }
@@ -518,36 +418,19 @@
        return $this->withTransaction(function () use ($stuckMinutes) {
            $cutoff = date('Y-m-d H:i:s', strtotime("-{$stuckMinutes} minutes"));
            // Get IDs first for logging
            $stuckIds = $this->wpdb->get_col($this->wpdb->prepare("
            SELECT id FROM {$this->table}
            WHERE state = 'processing'
              AND started_at < %s
            FOR UPDATE
        ", $cutoff));
            $stuckIds = $this->table->queryResults(
                "SELECT id FROM {table} WHERE state = 'processing' AND started_at < %s FOR UPDATE",
                [$cutoff]
            );
            $stuckIds = array_column($stuckIds, 'id');
            if (empty($stuckIds)) {
                return 0;
            }
            if (empty($stuckIds)) return 0;
            // Reset them
            $affected = $this->wpdb->query($this->wpdb->prepare("
            UPDATE {$this->table}
            SET state = 'scheduled',
                scheduled_at = %s,
                retries = retries + 1,
                updated_at = %s
            WHERE id IN (" . implode(',', array_fill(0, count($stuckIds), '%s')) . ")
        ",
                date('Y-m-d H:i:s', time() + 60),
                current_time('mysql'),
                ...$stuckIds
            ));
            // Optional: Log to audit table
            // $this->logStuckReset($stuckIds);
            return (int) $affected;
            $placeholders = implode(',', array_fill(0, count($stuckIds), '%s'));
            return (int) $this->table->query(
                "UPDATE {table} SET state = 'scheduled', scheduled_at = %s, retries = retries + 1, updated_at = %s WHERE id IN ({$placeholders})",
                array_merge([date('Y-m-d H:i:s', time() + 60), current_time('mysql')], $stuckIds)
            );
        });
    }
@@ -557,23 +440,124 @@
    public function replaceDependency(string $fromId, string $toId): int
    {
        return $this->withTransaction(function () use ($fromId, $toId) {
            // Only affect pending/scheduled operations
            $affected = $this->wpdb->query($this->wpdb->prepare("
            UPDATE {$this->table}
            SET dependencies = REPLACE(dependencies, %s, %s),
                updated_at = %s
            WHERE state IN ('pending', 'scheduled')
              AND dependencies LIKE %s
        ",
                '"' . $fromId . '"',
                '"' . $toId . '"',
                current_time('mysql'),
                '%"' . $fromId . '"%'
            ));
            return (int) $affected;
            return (int) $this->table->query(
                "UPDATE {table} SET dependencies = REPLACE(dependencies, %s, %s), updated_at = %s WHERE state IN ('pending', 'scheduled') AND dependencies LIKE %s",
                ['"'.$fromId.'"', '"'.$toId.'"', current_time('mysql'), '%"'.$fromId.'"%']
            );
        });
    }
    public function defineTables():void
    {
        $queue = CustomTable::for('_operation_queue');
        $queue->setColumns([
            'id'            => 'VARCHAR(64) NOT NULL',
            'type'          => 'VARCHAR(50) NOT NULL',
            'user_id'       => $queue->getUserIDType().' NOT NULL',
            'request_data'  => 'JSON NOT NULL CHECK (JSON_VALID(request_data))',
            'total_items'   => 'INT(11) NOT NULL DEFAULT 1',
            'processed_items'   => 'INT(11) DEFAULT 0',
            'failed_items'  => 'JSON',
            'priority'      => 'ENUM(\'high\',\'normal\',\'low\') DEFAULT \'normal\'',
            'state'         => 'ENUM(\'pending\', \'scheduled\', \'processing\', \'completed\') DEFAULT \'pending\'',
            'outcome'       => 'ENUM(\'pending\', \'success\',\'partial\',\'merged\',\'failed\',\'failed_permanent\') DEFAULT \'pending\'',
            'retries'       => 'INT(11) DEFAULT 0',
            'last_error_hash'=> 'CHAR(32) DEFAULT NULL',
            'error_message' => 'TEXT',
            'scheduled_at'  => 'DATETIME DEFAULT NULL',
            'started_at'    => 'DATETIME DEFAULT CURRENT_TIMESTAMP',
            'completed_at'  => 'DATETIME DEFAULT NULL',
            'metadata'      => 'JSON DEFAULT NULL',
            'result'        => 'JSON',
            'dependencies'  => 'JSON',
            'merged_into'   => 'VARCHAR(64) DEFAULT NULL',
            'user_dismissed'=> 'tinyint(1) DEFAULT 0',
            'created_at'    => 'DATETIME DEFAULT CURRENT_TIMESTAMP',
            'updated_at'    => 'DATETIME DEFAULT CURRENT_TIMESTAMP',
        ]);
        $queue->setKeys([
            ['key' => 'PRIMARY', 'value' => '(`id`)'],
            '`idx_run_queue` (`state`, `priority`, `scheduled_at`)',
            '`idx_user_ops` (`user_id`, `state`)',
            '`idx_user_type_pending` (`user_id`, `type`, `state`)',
            '`idx_completed_at` (`completed_at`)',
            '`idx_processing_stuck` (`state`, `started_at`)'
        ]);
        $queue->defineTable();
        $this->table = $queue;
        $stats = CustomTable::for('stats__operation_queue');
        $stats->setColumns([
            'id'            => 'BIGINT unsigned AUTO_INCREMENT',
            'date'          => 'DATE NOT NULL',
            'type'          => 'VARCHAR(50) NOT NULL',
            // Only store what can't be queried from the main table later
            'peak_queue_size'   => 'INT NOT NULL DEFAULT 0',
            'peak_memory_bytes' => 'BIGINT DEFAULT NULL',
            // Snapshot totals for post-purge historical view
            'total_operations'          => 'INT NOT NULL DEFAULT 0',
            'successful_operations'     => 'INT NOT NULL DEFAULT 0',
            'failed_permanent_operations' => 'INT NOT NULL DEFAULT 0',
            'total_items_processed'     => 'INT NOT NULL DEFAULT 0',
            'created_at'    => 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
            'updated_at'    => 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
        ]);
        $stats->setKeys([
            ['key' => 'PRIMARY', 'value' => '(`id`)'],
            ['key' => 'UNIQUE', 'value' => '(`date`, `type`)'],
            '`date_idx` (`date`)',
            '`type_idx` (`type`)'
        ]);
        $stats->defineTable();
        $this->stats = $stats;
    }
    public function snapshotDaily(): void
    {
        $today = current_time('Y-m-d');
        $types = $this->table->queryResults(
            "SELECT DISTINCT type FROM {table} WHERE DATE(completed_at) = %s",
            [$today]
        );
        foreach ($types as $row) {
            $stats = $this->table->queryResults("
            SELECT
                COUNT(*) as total,
                SUM(outcome = 'success') as successful,
                SUM(outcome = 'failed_permanent') as failed_permanent,
                SUM(processed_items) as items_processed
            FROM {table}
            WHERE type = %s AND DATE(completed_at) = %s
        ", [$row->type, $today]);
            $s = $stats[0] ?? null;
            if (!$s) continue;
            $this->stats->table->query("
            INSERT INTO {table} (date, type, total_operations, successful_operations, failed_permanent_operations, total_items_processed)
            VALUES (%s, %s, %d, %d, %d, %d)
            ON DUPLICATE KEY UPDATE
                total_operations = VALUES(total_operations),
                successful_operations = VALUES(successful_operations),
                failed_permanent_operations = VALUES(failed_permanent_operations),
                total_items_processed = VALUES(total_items_processed),
                updated_at = NOW()
        ", [$today, $row->type, $s->total, $s->successful, $s->failed_permanent, $s->items_processed]);
        }
    }
}
inc/meta/Meta.php
@@ -17,9 +17,9 @@
     */
    protected string $type;
    /**
     * @var string the full slug, with BASE
     * @var ?string the full slug, with BASE
     */
    protected string $slug;
    protected ?string $slug;
    protected string $contentType;
    protected Item $item;
@@ -27,7 +27,7 @@
    protected Validator $validator;
    protected Sanitizer $sanitizer;
    protected array $fields;
    protected WP_Post|WP_Term|WP_User|null $wpObject;
    protected WP_Post|WP_Term|WP_User|false|null $wpObject;
    protected int|string $ID;
    protected MetaTypeManager $typeManager;
    protected static array $instances = ['post' => [],'term' => [], 'user'=>[],'options'=>[]];
@@ -116,7 +116,7 @@
        $registrar = Registrar::getInstance($this->slug);
        $registrar = !is_null($this->slug) ? Registrar::getInstance($this->slug) : false;
        $fields = $registrar ? $registrar->getFields() : [];
        $meta = match($type) {
            'post'  => get_post_meta($id),
inc/rest/routes/FormRoutes.php
@@ -165,7 +165,6 @@
        } catch (\Exception $e) {
            return new WP_Error('validation_failed', 'Data validation error: ' . $e->getMessage());
        }
        if (array_key_exists('success', $processed_data) && $processed_data['success'] === false) {
            return $processed_data;
        }