Jake Vanderwerf
2026-09-18 a6413da67a4579866f0fd0323ddb23a7b6b9654a
=Cleaning up the Integrations setup a bit, started the Helcim integration work too
1 files added
2 files deleted
53 files modified
4663 ■■■■■ changed files
inc/admin/Integrations.php 29 ●●●●● patch | view | raw | blame | history
inc/blocks/_setup.php 3 ●●●●● patch | view | raw | blame | history
inc/helpers/renderFields.php 6 ●●●●● patch | view | raw | blame | history
inc/integrations/Admin.php 119 ●●●●● patch | view | raw | blame | history
inc/integrations/Auth.php 12 ●●●●● patch | view | raw | blame | history
inc/integrations/Field.php 41 ●●●●● patch | view | raw | blame | history
inc/integrations/Integrations.php 18 ●●●●● patch | view | raw | blame | history
inc/integrations/OAuth.php 83 ●●●●● patch | view | raw | blame | history
inc/integrations/ServiceMap.php 6 ●●●●● patch | view | raw | blame | history
inc/integrations/SyncDefaults.php 1 ●●●●● patch | view | raw | blame | history
inc/integrations/SyncHelpers.php 2 ●●●●● patch | view | raw | blame | history
inc/integrations/_Base.php 25 ●●●●● patch | view | raw | blame | history
inc/integrations/services/Cloudflare.php 9 ●●●●● patch | view | raw | blame | history
inc/integrations/services/Facebook.php 1 ●●●●● patch | view | raw | blame | history
inc/integrations/services/GoogleMaps.php 1 ●●●●● patch | view | raw | blame | history
inc/integrations/services/GoogleMyBusiness.php 35 ●●●●● patch | view | raw | blame | history
inc/integrations/services/Helcim.php 109 ●●●●● patch | view | raw | blame | history
inc/integrations/services/PostMark.php 13 ●●●●● patch | view | raw | blame | history
inc/integrations/services/SquareOld.php 12 ●●●●● patch | view | raw | blame | history
inc/integrations/services/Umami.php 21 ●●●●● patch | view | raw | blame | history
inc/managers/CustomTable.php 31 ●●●●● patch | view | raw | blame | history
inc/managers/Dashboard/Dashboard.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/DirectoryManager.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/ErrorHelper.php 27 ●●●●● patch | view | raw | blame | history
inc/managers/Errors.php 513 ●●●●● patch | view | raw | blame | history
inc/managers/Favourites.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/InvitationsManager.php 10 ●●●●● patch | view | raw | blame | history
inc/managers/MagicLinks.php 20 ●●●●● patch | view | raw | blame | history
inc/managers/OperationQueue.php 2183 ●●●●● patch | view | raw | blame | history
inc/managers/Responses.php 12 ●●●●● patch | view | raw | blame | history
inc/managers/UploadManager.php 17 ●●●●● patch | view | raw | blame | history
inc/managers/UserTermsManager.php 11 ●●●●● patch | view | raw | blame | history
inc/managers/_setup.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/queue/Queue.php 6 ●●●●● patch | view | raw | blame | history
inc/managers/queue/executors/ContentExecutor.php 16 ●●●●● patch | view | raw | blame | history
inc/managers/queue/executors/IntegrationExecutor.php 17 ●●●●● patch | view | raw | blame | history
inc/managers/queue/executors/UploadExecutor.php 16 ●●●●● patch | view | raw | blame | history
inc/meta/Storage.php 11 ●●●●● patch | view | raw | blame | history
inc/registrar/Posts.php 4 ●●●●● patch | view | raw | blame | history
inc/registrar/helpers/AddCustomFields.php 18 ●●●●● patch | view | raw | blame | history
inc/rest/Rest.php 16 ●●●●● patch | view | raw | blame | history
inc/rest/RestRouteManager.php 1058 ●●●●● patch | view | raw | blame | history
inc/rest/routes/ContentTermsRoutes.php 9 ●●●●● patch | view | raw | blame | history
inc/rest/routes/ErrorRoutes.php 4 ●●●●● patch | view | raw | blame | history
inc/rest/routes/FavouritesRoutes.php 18 ●●●●● patch | view | raw | blame | history
inc/rest/routes/FormRoutes.php 2 ●●●●● patch | view | raw | blame | history
inc/rest/routes/ImporterRoutes.php 4 ●●●●● patch | view | raw | blame | history
inc/rest/routes/IntegrationsHelcimRoutes.php 2 ●●●●● patch | view | raw | blame | history
inc/rest/routes/IntegrationsRoutes.php 10 ●●●●● patch | view | raw | blame | history
inc/rest/routes/IntegrationsSquareRoutes.php 2 ●●●●● patch | view | raw | blame | history
inc/rest/routes/LoginRoutes.php 12 ●●●●● patch | view | raw | blame | history
inc/rest/routes/NotificationsRoutes.php 6 ●●●●● patch | view | raw | blame | history
inc/rest/routes/TermRoutes.php 4 ●●●●● patch | view | raw | blame | history
inc/rest/routes/UploadRoutes.php 31 ●●●●● patch | view | raw | blame | history
inc/rest/routes/UserRoutes.php 1 ●●●●● patch | view | raw | blame | history
jvb.php 9 ●●●●● patch | view | raw | blame | history
inc/admin/Integrations.php
@@ -39,7 +39,8 @@
        // Add submenu pages for configured integrations
        foreach ($this->getAvailableServices() as $service) {
            $integration = $this->getService($service);
            if ($integration->isSetUp() && method_exists($integration, 'getAllowedContent') && !empty($integration->getAllowedContent())) {
            if (!empty(Registrar::withIntegration($service)) && $integration->isSetUp() && method_exists($integration, 'getAllowedContent') && !empty($integration->getAllowedContent())) {
                add_submenu_page(
                    'jvb-integrations',
                    $integration->getTitle() . ' Defaults',
@@ -65,6 +66,9 @@
        wp_enqueue_style(
            'jvb-dash',
        );
        wp_enqueue_style(
            'jvb-forms'
        );
        wp_enqueue_script(
            'jvb-admin-scripts',
@@ -107,6 +111,22 @@
    public function renderPage(): void
    {
        ?>
        <style>
            form.integration .actions.x-btw.x-btw {
                justify-content: flex-start;
            }
            form.integration button {
                display: flex;
                gap: .5rem;
                padding: .25rem .5rem;
                align-items: center;
                justify-content: space-between;
            }
            form.integration button svg {
                width: 1.5em;
                height: 1.5em;
            }
        </style>
        <div class="wrap jvb-admin-wrap">
            <h1>Integrations</h1>
@@ -124,7 +144,7 @@
                    <?php
                    foreach ($this->getAvailableServices() as $service) {
                        $integration = $this->getService($service);
                        if ($integration->isSetUp() && method_exists($integration, 'renderConnection')) {
                        if (method_exists($integration, 'renderConnection')) {
                            $integration->renderConnection();
                        }
                    }
@@ -527,7 +547,7 @@
            .item-grid.integrations {
                display: grid;
                grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
                grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
                gap: 2rem;
                margin-top: 2rem;
            }
@@ -1045,6 +1065,9 @@
                height: auto;
            }
            #wpfooter#wpfooter {
                position: relative;
            }
        </style>
        <?php
    }
inc/blocks/_setup.php
@@ -1,11 +1,12 @@
<?php
use JVBase\blocks\CustomBlocks;
use JVBase\registrar\Registrar;
use JVBase\base\Site;
//require(JVB_DIR . '/inc/blocks/RegisterBlocks.php');
require(JVB_DIR . '/inc/blocks/CustomBlocks.php');
CustomBlocks::getInstance();
add_action('init', 'jvb_conditionally_load_blocks', 1);
function jvb_conditionally_load_blocks(): void
inc/helpers/renderFields.php
@@ -352,8 +352,14 @@
            return '';
        }
    }
    if (is_null($term)) {
        return '';
    }
    $cache = Cache::for($term->taxonomy.'_link', 'term');
    $key = $term->term_id;
    if (is_null($key)) {
        return '';
    }
    return $cache->remember(
        $key,
        function() use ($term) {
inc/integrations/Admin.php
@@ -9,6 +9,7 @@
trait Admin {
    use _Base;
    protected bool $canTest = false;
    //An array of AdminAction objects
    protected array $actions = [];
    protected array $instructions = [];
@@ -22,18 +23,33 @@
                'label'     => 'Save Changes',
                'icon'      => 'floppy-disk',
                'action'    => [$this, 'saveCredentials']
            ],
            [
            ]
        ];
        if ($this->isSetUp()) {
            $actions[] = [
                'name'      => 'Clear Credentials',
                'label'     => 'Clear Credentials',
                'icon'      => 'broom',
                'action'    => [$this, 'deleteCredentials']
            ],
        ];
            ];
        }
        if ($this->canTest) {
            $actions[] = [
                'name'  => 'Test Connection',
                'label' => 'Test Connection',
                'icon'  => 'plugs-connected',
                'action'    => [$this, 'testConnectionAction']
            ];
        }
        foreach ($actions as $a) {
            $this->addAction($a['name'], $a['action'], $a['slug']??null, $a['icon']??null, $a['label']??null);
        }
        $this->defineActions();
    }
    abstract protected function defineActions():void;
    public function addAction(string $name, callable $callback, ?string $slug = null, ?string $icon = null, ?string $label = null):void
    {
        $action = new AdminActions($name, $callback);
@@ -89,13 +105,28 @@
        $isConnected = !empty($credentials);
        $meta = '';
        if ($isConnected && array_key_exists('updated_at', $credentials) && $credentials['updated_at'] > 0) {
            $meta = sprintf(
            '<div class="meta">
                    <small>Last updated: %s ago</small>
                </div>',
                human_time_diff($credentials['updated_at'])
            );
        if ($isConnected) {
            $updated = Auth::getInstance()->getLastUpdated($this->service_name, $this->userID);
            $meta = [];
            if ($updated) {
                $meta[] = sprintf(
                '<small>Last updated: %s ago</small>',
                    human_time_diff(strtotime($updated))
                );
            }
            $tested = Auth::getInstance()->getLastTested($this->service_name, $this->userID);
            if ($tested && $tested !== '0000-00-00 00:00:00') {
                $meta[] = sprintf(
                    '<small>Last tested: %s ago</small>',
                    human_time_diff(strtotime($tested))
                );
            }
            if (!empty($meta)) {
                $meta = sprintf(
                    '<div class="meta">%s</div>',
                    implode('<br>', $meta)
                );
            }
        }
        $form = sprintf(
@@ -117,7 +148,7 @@
            $meta,
        );
        if (!empty($this->instructions)) {
        if (!$this->isSetUp() && !empty($this->instructions)) {
            $form .= sprintf(
                '<details%s>
                    <summary>Instructions</summary>
@@ -125,13 +156,22 @@
                </details>',
                $isConnected ? '' : ' open',
                implode('',array_map(function($el) {
                    if (is_array($el)) {
                        $bits = implode('', array_map(function ($b) {
                            return sprintf('<li>%s</li>', $b);
                        }, $el));
                        return sprintf('<ol>%s</ol>', $bits);
                    }
                    return sprintf('<li>%s</li>', $el);
                }, $this->instructions))
            );
        }
        $setupFields = array_filter($this->fields, function($field) {
            return $field->isSetupField();
        });
        if (current_user_can('manage_options')) {
        if ((method_exists($this, 'userCanConnect') && $this->userCanConnect(get_current_user_id())) || current_user_can('manage_options')) {
            $form .= sprintf(
                '<details class="initial-setup"%s>
                <summary>Setup</summary>
@@ -139,7 +179,7 @@
                %s
            </details>',
                $isConnected ? '' : ' open',
                $this->outputFields($this->credentialFields),
                $this->outputFields($setupFields),
                method_exists($this, 'renderWebhookUrl') ? $this->renderWebhookUrl() : ''
            );
        }
@@ -148,13 +188,32 @@
            $form .= $this->renderOAuthConnectionForm();
        }
        if (!empty($this->optionalFields) || $this->hasExtraOptions()) {
            $form .= '<div class="integration-content">';
            if (!empty($this->optionalFields)) {
                $form .= sprintf(
        $advancedFields = array_filter($this->fields, function($field) {
            return $field->isAdvancedField();
        });
        if (!empty($advancedFields) && $this->isSetUp()) {
            $form .= sprintf(
                '<details class="advanced">
                        <summary>Advanced Settings</summary>%s</details>',
                    $this->outputFields($this->optionalFields)
                <summary>Advanced Options</summary>
                %s
            </details>',
                $this->outputFields($advancedFields),
            );
        }
        $optionalFields = array_filter($this->fields, function($field) {
            return $field->isAdditionalField();
        });
        if ($this->isSetUp() && (!empty($optionalFields) || $this->hasExtraOptions())) {
            $form .= '<div class="integration-content">';
            if (!empty($optionalFields)) {
                $form .= sprintf(
                '<details class="advanced"%s>
                        <summary>Optional Settings</summary>%s</details>',
                    $this->isSetUp() ? ' open' : '',
                    $this->outputFields($optionalFields)
                );
            }
            if ($this->hasExtraOptions()) {
@@ -185,7 +244,7 @@
                $label .= $action->getLabel();
                return sprintf(
                    '<button type="button" data-action="%s"%s%s>%s</button>',
                    $action->getSlug,
                    $action->getSlug(),
                    $action->getName(),
                    empty($action->getConfirm()) ? '' : ' data-confirm="'.$action->getConfirm().'"',
                    $label
@@ -207,6 +266,13 @@
            if (!empty($value)) {
                $config['readonly'] = true;
            }
            if (!empty($value) && $f->isHideStored()) {
                return sprintf(
                    '<span class="label">%s</span><code>%s</code>',
                    $config['label'],
                    preg_replace('/.(?=.{4})/', '*', $value)
                );
            }
            $config['base'] = $this->service_name.'_';
            return Form::render($f->name, $value, $f->getConfig());
        }, $fields));
@@ -226,7 +292,16 @@
            $this->fields[$name]->setRequired();
            return true;
        }
        public function updateField(string $name, Field $field):void
        {
            $this->fields[$name] = $field;
        }
    public function testConnectionAction(?array $data = null):array
    {
        $success = $this->testConnection();
        $message = $success ? 'Successfully tested connection' : 'Could not test connection';
        return $this->response($success, $message);
    }
}
inc/integrations/Auth.php
@@ -79,6 +79,12 @@
        return (bool)$updated;
    }
    public function getLastUpdated(string $service, ?int $userID = null):?string
    {
        $find = $this->findBy($service, $userID);
        return $this->table->pluck('updated_at', $find)[0]??null;
    }
    /**
     * Retrieve and decrypt credentials
     */
@@ -164,6 +170,12 @@
        ], $find);
    }
    public function getLastTested(string $service, ?int $userID = null):?string
    {
        $find = $this->findBy($service, $userID);
        return $this->table->pluck('last_tested', $find)[0]??null;
    }
    public function markUnhealthy(string $service, string $type = '', ?int $userID = null):void
    {
        $update = [];
inc/integrations/Field.php
@@ -21,6 +21,10 @@
    protected bool $hidden = false;
    protected int $min;
    protected int $max;
    protected bool $isAdditionalField = false;
    protected bool $isSetupField = false;
    protected bool $isAdvancedField = false;
    protected bool $hideStored = false;
    public function __construct(string $name, string $label, string $type, callable|string|bool $permission)
    {
@@ -52,6 +56,14 @@
    {
        $this->required = true;
    }
    public function setHideStored():void
    {
        $this->hideStored = true;
    }
    public function isHideStored():bool
    {
        return $this->hideStored;
    }
    public function setOptions(array $options):void
    {
        $this->options = $options;
@@ -61,6 +73,30 @@
        $this->default = $default;
    }
    public function makeSetupField():void
    {
        $this->isSetupField = true;
    }
    public function isSetupField():bool
    {
        return $this->isSetupField;
    }
    public function makeAdvancedField():void
    {
        $this->isAdvancedField = true;
    }
    public function isAdvancedField():bool{
        return $this->isAdvancedField;
    }
    public function makeAdditionalField():void
    {
        $this->isAdditionalField = true;
    }
    public function isAdditionalField():bool
    {
        return $this->isAdditionalField;
    }
    public function setHint(string $hint):void
    {
        $this->hint = $hint;
@@ -108,8 +144,9 @@
        if (isset($this->hint)) {
            $conf['hint'] = $this->hint;
        }
        if (isset($this->subType)) {
            $conf['subtype'] = $this->subType;
        if (isset($this->subtype)) {
            $conf['subtype'] = $this->subtype;
        }
        if ($this->hidden) {
            $conf['hidden'] = true;
inc/integrations/Integrations.php
@@ -247,7 +247,7 @@
    public function testConnection(bool $force = false): bool
    {
        if (!$this->isSetUp()) {
            return false;
            return true;
        }
        $this->ensureInitialized();
@@ -255,13 +255,6 @@
            return false;
        }
        // Cache test results to avoid excessive API calls
        $cacheKey = "connection_test_$this->service_name" . ($this->userID ? "_$this->userID" : '');
        $cached = $this->cache->get($cacheKey);
        if ($cached !== false && !$force) {
            return (bool)$cached;
        }
        try {
            if (!$this->hasOAuthCredentials()){
@@ -272,11 +265,9 @@
            }
            $this->updateLastTestedTime();
            $this->cache->set($cacheKey, $result, 300); // Cache for 5 minutes
            return $result;
        } catch (Exception $e) {
            $this->logError('Connection test failed', ['error' => $e->getMessage()]);
            $this->cache->set($cacheKey, false, 60); // Cache failure for 1 minute
            $this->logError('testConnection', $e->getMessage());
            return false;
        }
    }
@@ -634,4 +625,9 @@
        return $this->icon;
    }
    public function getUserID():?int
    {
        return $this->userID;
    }
}
inc/integrations/OAuth.php
@@ -58,26 +58,29 @@
    function addOAuthActions():void
    {
        $actions = [
            [
                'name'      => 'Connect to Service',
                'label'     => 'Connect',
                'icon'      => 'plugs-connected',
                'action'    => [$this, 'handleOAuthConnect']
            ],
            [
                'name'      => 'Disconnect from Service',
                'icon'      => 'plugs-disconnected',
                'action'    => [$this, 'handleOAuthDisconnect']
            ],
        if ($this->isSetUp()) {
            $actions = [
                [
                    'name'      => 'Connect to Service',
                    'label'     => 'Connect',
                    'icon'      => 'plugs-connected',
                    'action'    => [$this, 'handleOAuthConnect']
                ],
                [
                    'name'      => 'Disconnect from Service',
                    'label'     => 'Disconnect',
                    'icon'      => 'plugs-disconnected',
                    'action'    => [$this, 'handleOAuthDisconnect']
                ],
//          [
//              'name'      => 'Refresh Token',
//              'icon'      => 'arrows-clockwise',
//              'action'    => [$this, 'handleOAuthRefreshToken']
//          ]
        ];
        foreach ($actions as $a) {
            $this->addAction($a['name'], $a['action'], $a['slug']??null, $a['icon']??null, $a['label']??null);
            ];
            foreach ($actions as $a) {
                $this->addAction($a['name'], $a['action'], $a['slug']??null, $a['icon']??null, $a['label']??null);
            }
        }
    }
        public function hasOAuthCredentials(?int $userID = null):bool
@@ -109,7 +112,7 @@
        public function revokeOAuth():bool
        {
            if (empty($this->oauth['revoke'])) {
            if (empty($this->oauth->getRevoke())) {
                error_log('No revoke url set for '.$this->service_name);
                return false;
            }
@@ -117,7 +120,7 @@
            if (empty($credentials['access_token'])) {
                error_log('Could not send revoke request, no access_token for '.$this->service_name);
            } else {
                $response = $this->postOAuthRequest($this->oauth['revoke'], [
                $response = $this->postOAuthRequest($this->oauth->getRevoke(), [
                    'body'  => [
                        'token' => $credentials['access_token']
                    ]
@@ -328,20 +331,23 @@
        $credentials = $this->loadCredentials();
        $hasCredentials = $this->hasValidOAuth();
        $returnURL = $this->getReturnURL();
        if ($hasCredentials) {
        if (!$this->isSetUp()) {
            return '';
        }
        if (!$hasCredentials) {
            return sprintf(
            '<div class="oauth-connect">
                <h3>Connect to your %s account</h3>
                <a href="%s" class="btn jvb-oauth-connect" data-service="%s">%s Authorize Connection</a>',
                <a href="%s" class="btn jvb-oauth-connect" data-service="%s">%s Authorize Connection</a>
                </div>',
                $this->title,
                $this->getOAuthURL($returnURL),
                esc_attr($this->service_name),
                $this->outputIcon($this->icon)
            );
        }
        $form = sprintf(
            '<details open>
        } else {
            $form = sprintf(
                '<details open>
            <summary>Connected Account
                <div class="connection-status connected">
                    <span class="status-indicator">●</span>
@@ -350,22 +356,23 @@
            </summary>
            <span class="label">OAuth Redirect URL:</span>
            <code>%s</code>',
            $this->getRedirectUri()
        );
        if (!empty($credentials['updated_at'])) {
            $form .= sprintf(
                '<div class="oauth-meta"><small>Token expires: %s</small></div>',
                array_key_exists('expires_at', $credentials) ?
                $this->getRedirectUri()
            );
            if (!empty($credentials['updated_at'])) {
                $form .= sprintf(
                    '<div class="oauth-meta"><small>Token expires: %s</small></div>',
                    array_key_exists('expires_at', $credentials) ?
                        human_time_diff($credentials['expires_at']) :
                        'Never'
            );
                );
            }
            //
            if (method_exists($this, 'renderOAuthConnectedOptions')) {
                $form .= $this->renderOAuthConnectedOptions();
            }
            $form .= '</details>';
            return $form;
        }
        //
        if (method_exists($this, 'renderOAuthConnectedOptions')) {
            $form .= $this->renderOAuthConnectedOptions();
        }
        $form .= '</details';
        return $form;
    }
    /**
@@ -380,7 +387,7 @@
    {
        $credentials = $this->loadCredentials();
        if (empty($this->oauth['authorize'])) {
        if (empty($this->oauth->getAuthorize())) {
            $this->logError('getOAuthUrl', 'OAuth authorize URL not configured');
            return '';
        }
@@ -418,7 +425,7 @@
            $params = $this->addOAuthParams($params);
        }
        return $this->oauth['authorize'] . '?' . http_build_query($params);
        return $this->oauth->getAuthorize() . '?' . http_build_query($params);
    }
    protected function getRedirectUri():string
inc/integrations/ServiceMap.php
@@ -29,7 +29,7 @@
    public function getServiceUser(string $service, ?int $userID = null):Integrations|false
    {
        $service = $this->getService($service);
        if ($service->userID !== $userID && method_exists($service, 'switchUser')) {
        if ($service->getUserID() !== $userID && method_exists($service, 'switchUser')) {
            $service->switchUser($userID);
        }
        return $service;
@@ -37,8 +37,6 @@
    public function getAvailableServices():array
    {
        return array_filter(array_keys($this->serviceMap), function ($service) {
            return Site::hasIntegration($service);
        }, ARRAY_FILTER_USE_KEY);
        return array_keys(Site::getIntegrations());
    }
}
inc/integrations/SyncDefaults.php
@@ -128,6 +128,7 @@
                ->description($options['description'])
                ->content($this->getContentTypeContent($allowed));
        }
        $tabs->render(false);
    }
    public function getContentTypeContent(array $content):string
inc/integrations/SyncHelpers.php
@@ -13,7 +13,7 @@
}
trait SyncHelpers {
    use _Base;
    use _Base, SyncDefaults;
    /**
     * Queue types
     * These types match with IntegrationExecutor
inc/integrations/_Base.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\integrations;
use JVBase\inc\managers\ErrorHelper;
use JVBase\managers\Cache;
use JVBase\managers\Errors;
use JVBase\managers\IconsManager;
@@ -10,6 +11,7 @@
}
trait _Base {
    use ErrorHelper;
    /**
     * @var string Unique identifier for this service (ex: square, facebook)
     */
@@ -95,7 +97,16 @@
     */
    public function isSetUp(): bool
    {
        return !empty($this->loadCredentials());
        $tmp = null;
        if ($this->userID) {
            $tmp = $this->userID;
            $this->switchUser();
        }
        $isSetUp = !empty($this->loadCredentials()??[]);
        if ($tmp) {
            $this->switchUser($tmp);
        }
        return $isSetUp;
    }
    public function loadCredentials():array
@@ -108,8 +119,16 @@
    protected function logError(string $method, string $message, array $context = []):void
    {
        $context['service'] = $this->service_name;
        Errors::getInstance()->log('['.self::class.']'.$method, $message, $context);
        Errors::log($message, array_merge(
            [
                'service'   => $this->service_name,
                'method'    => $method,
                'class'     => static::class,
            ],
            $context
        ));
    }
    protected function getReturnURL():string
@@ -131,7 +150,7 @@
    {
        if (is_admin()) {
            $icons = IconsManager::for('admin');
            return $icons->getRawSvg($icon);
            return $icons->getRawSvg($icon)??'';
        }
        return jvbDashIcon($icon);
    }
inc/integrations/services/Cloudflare.php
@@ -31,10 +31,15 @@
        $site_key = $this->addField('site_key', 'Site Key', 'text', 'manage_options');
            $site_key->setRequired();
            $site_key->setHint('Public key for Turnstile Widget');
            $site_key->makeSetUpField();
            $this->updateField('site_key', $site_key);
        $secret_key = $this->addField('secret_key', 'Secret Key', 'text', 'manage_options');
            $secret_key->setRequired();
            $secret_key->setSubType('password');
            $secret_key->setHint('Secret key for server-side verification of Turnstile Widget');
            $secret_key->makeSetUpField();
            $this->updateField('secret_key', $secret_key);
    }
    protected function defineInstructions():void
@@ -210,4 +215,8 @@
            return;
        }
    }
    protected function defineActions(): void
    {
    }
}
inc/integrations/services/Facebook.php
@@ -52,7 +52,6 @@
        $this->defineFields();
        $this->defineInstructions();
        $this->defineOAuth();
        $this->defineActions();
        $this->defineContentTypes();
        $this->baseUrl = 'https://graph.facebook.com/'.$this->apiVersion;
inc/integrations/services/GoogleMaps.php
@@ -26,7 +26,6 @@
        $this->apiBase = 'https://maps.googleapis.com/maps/api/';
        $this->defineFields();
        $this->defineInstructions();
        $this->defineActions();
        parent::__construct();
        // Auto-enqueue maps script when needed
inc/integrations/services/GoogleMyBusiness.php
@@ -56,6 +56,8 @@
            );
                $client_id->setRequired();
                $client_id->setHint('From your Google Cloud Console OAuth credentials.');
                $client_id->makeSetupField();
                $this->updateField('client_id', $client_id);
            $secret = $this->addField(
                'client_secret',
@@ -65,6 +67,8 @@
            );
                $secret->setSubType('password');
                $secret->setRequired();
                $secret->makeSetupField();
                $this->updateField('client_secret', $client_id);
            if ($this->getAccessToken()) {
                $accs = $this->addField('accounts', 'Accounts', 'select', [$this, 'testUserCanConnect']);
@@ -101,6 +105,25 @@
                'Add redirect URI: <code>'.esc_html($this->getRedirectUri()).'</code>',
                'Use the "Authorize" button below to complete OAuth flow'
            ];
            $this->instructions = [
                'Go to <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console</a>',
                'Create OAuth 2.0 Client ID credentials',
                'Enable the following APIs in your project:',
                [
                    '<a href="https://console.cloud.google.com/apis/library/mybusinessaccountmanagement.googleapis.com" target="_blank">My Business Account Management API</a>',
                    '<a href="https://console.cloud.google.com/apis/library/mybusinessbusinessinformation.googleapis.com" target="_blank">My Business Business Information API</a>',
                    '<a href="https://console.cloud.google.com/apis/library/mybusiness.googleapis.com" target="_blank">Google My Business API</a> (legacy v4, required for reviews)',
                ],
                'On the OAuth consent screen, add these scopes:',
                [
                    '<code>https://www.googleapis.com/auth/business.manage</code>',
                    '<code>https://www.googleapis.com/auth/userinfo.email</code>',
                    '<code>https://www.googleapis.com/auth/userinfo.profile</code>',
                ],
                'Add redirect URI: <code>'.esc_html($this->getRedirectUri()).'</code>',
                'Use the "Authorize" button below to complete OAuth flow'
            ];
        }
        protected function defineOAuth():void
        {
@@ -119,11 +142,13 @@
        }
        protected function defineActions():void
        {
            $this->addAction(
                'Update Location',
                [$this, 'handleUpdateLocation'],
                'update_location',
            );
            if ($this->hasOAuthCredentials($this->userID)) {
                $this->addAction(
                    'Update Location',
                    [$this, 'handleUpdateLocation'],
                    'update_location',
                );
            }
        }
            public function handleUpdateLocation(array $data):array
            {
inc/integrations/services/Helcim.php
@@ -28,10 +28,11 @@
 */
class Helcim extends Integrations
{
    use Admin, Instance, OAuth, Requests, UserConnection, SyncToPost;
    use Admin, Instance, Requests, UserConnection, SyncToPost;
    protected function __construct()
    {
        $this->canTest = true;
        $this->service_name = 'helcim';
        $this->title = 'Helcim';
        $this->icon = 'currency-dollar';
@@ -42,11 +43,11 @@
            'delete'    => true,
        ];
        $this->baseUrl = 'https://api.helcim.com/v2';
        $this->apiVersion = 'v2';
        $this->baseUrl = sprintf('https://api.helcim.com/%s/', $this->apiVersion);
        $this->defineFields();
        $this->defineInstructions();
        $this->defineActions();
        parent::__construct();
    }
@@ -61,6 +62,9 @@
                $api_token->setSubType('password');
                $api_token->setRequired();
                $api_token->setHint('Found in Helcim Dashboard → Settings → API Access');
                $api_token->makeSetupField();
                $api_token->setHideStored();
                $this->updateField('api_token', $api_token);
            $currency = $this->addField(
                'currency',
@@ -73,6 +77,7 @@
                    'USD'   => 'USD'
                ]);
                $currency->setDefault('CAD');
                $currency->makeAdditionalField();
            $feeSaver = $this->addField(
                'fee_saver',
@@ -81,6 +86,7 @@
                [$this, 'testUserSetup']
            );
                $feeSaver->setHint('Pass processing fees to customers? (Not compatible with Google Pay)');
                $feeSaver->makeAdditionalField();
            $allowACH = $this->addField(
                'allow_ach',
@@ -89,6 +95,7 @@
                [$this, 'testUserSetup']
            );
                $allowACH->setHint('Enable bank account payments alongside credit card');
                $allowACH->makeAdditionalField();
        }
            public function testUserCanConnect():bool
            {
@@ -113,11 +120,21 @@
        protected function defineActions():void
        {
            $this->addAction(
                'Initialize Checkout',
                [$this, 'initializeCheckout'],
                'initialize_checkout'
            );
            if ($this->isSetUp()) {
                $this->addAction(
                    'Sync to Helcim',
                    [$this, 'syncToHelcim'],
                    'sync_to_helcim',
                    'upload-simple'
                );
                $this->addAction(
                    'Sync to Site',
                    [$this, 'syncToSite'],
                    'sync_to_site',
                    'download-simple'
                );
            }
        }
    protected function initialize(): void
@@ -125,30 +142,84 @@
        // TODO: Implement initialize() method.
    }
    protected function refreshAccessToken(OAuthCredentials $credentials): array
    {
        // TODO: Implement refreshAccessToken() method.
    }
    protected function getRequestHeaders(): array
    {
        // TODO: Implement getRequestHeaders() method.
    }
    protected function handleResponse(WP_Error|array $response, string $method, string $endpoint): array
    {
        // TODO: Implement handleResponse() method.
        return empty($this->getApiToken()) ? [] : [
            'api-token'     => $this->getApiToken(),
            'Accept'        => 'application/json',
        ];
    }
    protected function setContentTypes(): void
    {
        // TODO: Implement setContentTypes() method.
        $this->contentTypes = [
            'recurring' => $this->buildRecurringPaymentFields(),
            'product'   => $this->buildPaymentFields(),
            'customer'  => $this->buildCustomerFields(),
            'invoice'   => $this->buildInvoiceFields(),
        ];
    }
        protected function buildRecurringPaymentFields(): array
        {
            return [];
        }
        protected function buildPaymentFields(): array
        {
            return [];
        }
        protected function buildCustomerFields():array
        {
            return [];
        }
        protected function buildInvoiceFields():array
        {
            return [];
        }
    protected function formatForService(int $itemID, string $type = 'post'): array
    {
        return [];
        // TODO: Implement formatForService() method.
    }
    public function initializeCheckout():void
    {
    }
    public function syncToHelcim():void
    {
    }
    public function syncToSite():void
    {
    }
    /*************************************************************************
     * REQUESTS
    *************************************************************************/
    protected function handleResponse(WP_Error|array $response, string $method, string $endpoint): array
    {
        if (is_wp_error($response) && $response->get_error_message() === 'rate_limit_exceeded') {
            return $this->response(false, 'Rate limit exceeded');
        }
        return match ($endpoint) {
            'connection-test' => $this->response(true,'Connection successful', $response),
            default => $this->response(false, 'Unhandled response for '.$endpoint),
        };
    }
    protected function performConnectionTest(): bool
    {
        $response = $this->getRequest(
            'connection-test'
        );
        error_log('Helcim connection test: '.print_r($response, true));
        return $response['success']??false;
    }
    /*************************************************************************
     * UTILITY
    *************************************************************************/
inc/integrations/services/PostMark.php
@@ -34,25 +34,32 @@
        $this->defineInstructions();
        parent::__construct();
        $this->defineActions();
    }
        protected function defineFields():void
        {
            $token = $this->addField('service_token', 'Server API Token', 'text', 'manage_options');
                $token->setSubType('password');
                $token->makeSetupField();
                $this->updateField('service_token', $token);
            $stream = $this->addField('message_stream', 'Message Stream ID', 'text', 'manage_options');
                $stream->setDefault('outbound');
                $stream->setHint('Message stream to use (default: outbound)');
                $stream->makeAdditionalField();
                $this->updateField('message_stream', $stream);
            $from = $this->addField('from_email', 'From Email', 'email', 'manage_options');
                $from->setPlaceholder('noreply@yourdomain.com');
                $from->setDefault(get_option('admin_email'));
                $from->makeAdditionalField();
                $this->updateField('from_email', $from);
            $name = $this->addField('from_name', 'From Name', 'text', 'manage_options');
                $name->setDefault(get_bloginfo('name'));
                $name->setHint('Default sender name');
                $name->makeAdditionalField();
                $this->updateField('from_name', $from);
            $opens = $this->addField('track_opens', 'Track Opens', 'select', 'manage_options');
                $opens->setOptions([
@@ -61,6 +68,8 @@
                ]);
                $opens->setDefault('false');
                $opens->setHint('Track when emails are opened');
                $opens->makeAdvancedField();
                $this->updateField('track_opens', $opens);
            $links = $this->addField('track_links', 'Track Links', 'select', 'manage_options');
                $links->setOptions([
@@ -70,6 +79,8 @@
                ]);
                $links->setDefault('None');
                $links->setHint('Track link clicks in emails');
                $links->makeAdvancedField();
                $this->updateField('track_links', $links);
        }
        protected function defineInstructions():void
        {
inc/integrations/services/SquareOld.php
@@ -237,9 +237,9 @@
            ? 'https://connect.squareup.com'
            : 'https://connect.squareupsandbox.com';
        $this->oauth['authorize'] = $baseUrl . '/oauth2/authorize';
        $this->oauth['token'] = $baseUrl . '/oauth2/token';
        $this->oauth['revoke'] = $baseUrl . '/oauth2/revoke';
        $this->oauth->getAuthorize() = $baseUrl . '/oauth2/authorize';
        $this->oauth->getToken() = $baseUrl . '/oauth2/token';
        $this->oauth->getRevoke() = $baseUrl . '/oauth2/revoke';
        // Set up API endpoints with version
        $this->apiEndpoints = [
@@ -408,7 +408,7 @@
            'redirect_uri' => $this->getRedirectUri()
        ];
        $response = wp_remote_post($this->oauth['token'], [
        $response = wp_remote_post($this->oauth->getToken(), [
            'body' => json_encode($body),
            'headers' => [
                'Content-Type' => 'application/json',
@@ -464,7 +464,7 @@
            return false;
        }
        $response = wp_remote_post($this->oauth['token'], [
        $response = wp_remote_post($this->oauth->getToken(), [
            'body' => [
                'client_id' => $this->credentials['client_id'],
                'client_secret' => $this->credentials['client_secret'],
@@ -823,7 +823,7 @@
        try {
            // Revoke the token with Square
            if (!empty($this->credentials['access_token'])) {
                wp_remote_post($this->oauth['revoke'], [
                wp_remote_post($this->oauth->getRevoke(), [
                    'body' => [
                        'client_id' => $this->credentials['client_id'] ?? '',
                        'access_token' => $this->credentials['access_token']
inc/integrations/services/Umami.php
@@ -29,15 +29,21 @@
    protected function defineFields(): void
    {
        $id = $this->addField('website_id', 'Website ID', 'text', 'manage_options');
        $id->setHint('Found in your Umami dashboard under Settings->Websites');
        $id->setRequired();
            $id->setHint('Found in your Umami dashboard under Settings->Websites');
            $id->setRequired();
            $id->makeSetupField();
            $this->updateField('website_id', $id);
        $api_url = $this->addField('api_url', 'API URL', 'text', 'manage_options');
        $api_url->setHint('Your Umami server URL. Leave blank if using umami.is cloud service.');
            $api_url->setHint('Your Umami server URL. Leave blank if using umami.is cloud service.');
            $api_url->makeAdvancedField();
            $this->updateField('api_url', $api_url);
        $api_token = $this->addField('api_token', 'API Token (Optional)', 'text', 'manage_options');
        $api_token->setSubType('password');
        $api_token->setHint('Required only if you want to retrieve analytics data for dashboards.');
            $api_token->setSubType('password');
            $api_token->setHint('Required only if you want to retrieve analytics data for dashboards.');
            $api_token->makeAdvancedField();
            $this->updateField('api_token', $api_token);
    }
    protected function defineInstructions():void
    {
@@ -220,4 +226,9 @@
            return implode(' ', $strings);
        }
    protected function defineActions(): void
    {
        // TODO: Implement defineActions() method.
    }
}
inc/managers/CustomTable.php
@@ -2,6 +2,7 @@
namespace JVBase\managers;
use Exception;
use JVBase\inc\managers\ErrorHelper;
use wpdb;
if (!defined('ABSPATH')) {
@@ -20,7 +21,7 @@
 */
class CustomTable
{
    use ErrorHelper;
    private static ?wpdb $wpdb = null;
    private static string $prefix;
    protected string $tableName;
@@ -507,7 +508,7 @@
        );
        if ($result === false) {
            $this->logError('insert', $data);
            $this->logError(self::$wpdb->last_error, 'insert', $data);
            return false;
        }
@@ -608,7 +609,7 @@
        $result = self::$wpdb->query(self::$wpdb->prepare($query, $values));
        if ($result === false) {
            $this->logError('bulkInsert', ['rows' => count($rows)]);
            $this->logError(self::$wpdb->last_error, 'bulkInsert', ['rows' => count($rows)]);
            return false;
        }
@@ -755,7 +756,7 @@
            $key,
            function() use ($column, $where, $orderby, $order, $limit) {
                if (!empty($this->columns) && !array_key_exists($column, $this->columns)) {
                    $this->logError('pluck', ['reason' => "Column '{$column}' not in definition"]);
                    $this->logError("Column '{$column}' not in definition", 'pluck');
                    return [];
                }
                $args = ['where' => $where];
@@ -851,7 +852,7 @@
        );
        if ($result === false) {
            $this->logError('update', ['data' => $data, 'where' => $where]);
            $this->logError(self::$wpdb->last_error,'update', ['data' => $data, 'where' => $where]);
        }
        $this->cache->flush();
@@ -881,7 +882,7 @@
        );
        if ($result === false) {
            $this->logError('delete', ['where' => $where]);
            $this->logError(self::$wpdb->last_error, 'delete', ['where' => $where]);
        }
        $this->cache->flush();
@@ -1005,7 +1006,7 @@
            return $result;
        } catch (Exception $e) {
            $this->rollback();
            $this->logError('transaction', ['error' => $e->getMessage()]);
            $this->logError(self::$wpdb->last_error,'transaction', ['error' => $e->getMessage()]);
            return false;
        }
    }
@@ -1192,22 +1193,6 @@
        return isset($cache[$this->tableName][$column]);
    }
    /**
     * Log database errors
     */
    private function logError(string $operation, array $context = []): void
    {
        if (function_exists('JVB')) {
            Errors::getInstance()->log(
                $this->tableName,
                "CustomTable {$operation} failed: " . self::$wpdb->last_error,
                $context
            );
        } else {
            error_log("[CustomTable:{$this->tableName}] {$operation} failed: " . self::$wpdb->last_error);
        }
    }
    public function sum(string $column, array $where):float
    {
        $rows = $this->pluck($column, $where);
inc/managers/Dashboard/Dashboard.php
@@ -378,7 +378,14 @@
                $registrar = Registrar::getInstance($c);
                $page = $this->addPage($registrar->getPlural(), $c, $registrar->getIcon());
                $page->setPermission(Roles::getPermissionName('edit', $c));
                $this->sections[$c] = new Section($registrar->getPlural(), $c, $registrar->getIcon(), 'content');
                $page->setSection($c);
                if ($registrar->hasFeature('verify_entry')) {
                }
                //TODO: add artist management and invitations, if applicable
            }
        }
inc/managers/DirectoryManager.php
@@ -72,7 +72,7 @@
            'public'              => true,
            'menu_icon'     => jvbCSSIcon('list-dashes'),
            'publicly_queryable'    => true,
            'show_in_menu'          => true,
            'show_in_menu'          => false,
            'show_in_admin_bar'     => false,
            'has_archive'           => true,
            'hierarchical'          => true,
inc/managers/ErrorHelper.php
New file
@@ -0,0 +1,27 @@
<?php
namespace JVBase\inc\managers;
use JVBase\managers\Errors;
trait ErrorHelper
{
    /**
     * Log errors with proper context
     */
    protected function logError(string $message, string $caller, array $context = []): void
    {
        Errors::log($message, array_merge([
            'class' => static::class,
            'method' => $caller,
        ], $context));
    }
    protected static function staticLogError(string $message, string $caller, array $context = []): void
    {
        Errors::log($message, array_merge([
            'class' => static::class,
            'method' => $caller,
        ], $context));
    }
}
inc/managers/Errors.php
@@ -13,503 +13,30 @@
class Errors
{
    use Instance;
    protected int $notification_threshold = 5; // Critical errors within 1 hour
    protected array $error_levels = [
        'debug' => 0,
        'info' => 1,
        'warning' => 2,
        'error' => 3,
        'critical' => 4
    ];
    protected CustomTable $table;
    private function __construct()
    {
        $this->defineTables();
//        global $wpdb;
//        $this->wpdb = $wpdb;
//      $this->tableName = $wpdb->prefix . BASE . 'error_log';
        add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
        // Hook our send function to the cron event
        add_action('jvb_daily_error_summary', [$this, 'sendErrorSummary']);
        jvb_register_do_once('error_admin_action_registered', [$this, 'registerAdminAction']);
//        add_filter(BASE.'admin_action_filter', [$this, 'adminActionFilter'], 10, 3);
    }
    public function defineTables():void
    /**
     * @param string $message
     * @param array $context
     * @return void
     */
    public static function log(string $message, array $context = []):void
    {
        $table = CustomTable::for('error_log');
        $table->setColumns([
            'id'            => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
            'error_type'    => 'varchar(50) NOT NULL',
            'component'     => 'varchar(100) NOT NULL',
            'method'        => 'varchar(100) DEFAULT NULL',
            'page_url'      => 'varchar(255) DEFAULT NULL',
            'message'       => 'text NOT NULL',
            'context'       => 'JSON',
            'severity'      => 'ENUM(\'high\',\'normal\',\'low\') DEFAULT \'normal\'',
            'user_id'       => $table->getUserIDType().' DEFAULT NULL',
            'user_was_logged_in'    => 'tinyint(1) NOT NULL',
            'source'        => 'ENUM(\'frontend\', \'backend\') NOT NULL',
            'created_at'    => 'timestamp DEFAULT CURRENT_TIMESTAMP',
        ]);
        $table->setKeys([
            ['key' => 'PRIMARY', 'value' => 'id'],
            '`created_at` (`created_at`)',
            '`component_severity_date` (`component`, `severity`, `created_at`)',
            '`error_type_date` (`error_type`, `created_at`)',
            '`severity_date` (`severity`, `created_at`)'
        ]);
        $table->defineTable();
        $this->table = $table;
    }
    public function registerAdminAction():void
    {
        $admin = AdminPages::getInstance();
        $admin->registerSubpage(
            'Error Log',
            'Errors',
            'manage_options',
            'jvb-error-log',
            [$this, 'renderAdminPage'],
        );
    }
    /**
     * Process error log operations queued through OperationQueue
     *
     * @param WP_Error|array $result The result we are filtering
     * @param object $operation The operation information
     * @param array $data The current chunk of data to process
     *
     * @return WP_Error|array Either the unfiltered WP_Error, or the result of the operation
     */
    public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array
    {
        if ($operation->type !== 'error_log') {
            return $result;
        }
        try {
            // Extract error data
            $component = sanitize_text_field($data['component'] ?? '');
            $message = sanitize_textarea_field($data['message'] ?? '');
            $context = $data['context'] ?? [];
            $severity = sanitize_text_field($data['severity'] ?? 'error');
            // Validate severity level
            if (!array_key_exists($severity, $this->error_levels)) {
                $severity = 'error'; // Default to error if invalid severity provided
            }
            // Determine error type from context or use component as fallback
            $error_type = sanitize_text_field($context['error_type'] ?? $component);
            // Store context as JSON
            $context_json = json_encode($context);
            if (json_last_error() !== JSON_ERROR_NONE) {
                // If JSON encoding fails, store only basic info
                $context_json = json_encode([
                    'error' => 'Failed to encode original context',
                    'json_error' => json_last_error_msg()
                ]);
            }
            // Insert into database
            $result = $this->table->insert(
                [
                    'error_type' => $error_type,
                    'component' => $component,
                    'message' => $message,
                    'context' => $context_json,
                    'severity' => $severity,
                    'user_id' => get_current_user_id(),
                    'created_at' => current_time('mysql')
                ]
            );
            if (!$result) {
                // If insert fails, log to PHP error log as fallback
                error_log("[ErrorHandler] Database insert failed: " . $this->table->getLastError());
                return [
                    'success'   => false,
                    'message'   => "[ErrorHandler] Database insert failed: " . $this->table->getLastError()
                ];
            }
            // Potentially track error metrics or trigger additional actions for critical errors
            if ($severity === 'critical') {
                $this->checkErrorThreshold($error_type, $component);
            }
            return [
                'success' => true,
                'result'    => $result
            ];
        } catch (Exception $e) {
            // Fallback to PHP error log if our error handler itself fails
            error_log("[ErrorHandler Exception] " . $e->getMessage());
            return [
                'success'   => false,
                'message'   => $e->getMessage()
            ];
        }
    }
    /**
     * Check if error threshold has been reached for a specific error type/component
     * and take additional actions if needed
     *
     * @param string $error_type The type of error
     * @param string $component The component where the error occurred
     */
    protected function checkErrorThreshold(string $error_type, string $component)
    {
        // Get count of similar critical errors in the last hour
        $count = $this->table->count([
            'error_type' => $error_type,
            'component' => $component,
            'severity' => $this->error_levels[$error_type],
            'created_at' => ['>','DATE_SUB(NOW(), INTERVAL 1 HOUR)']
        ]);
        // If threshold reached, take additional actions (e.g., notify developers)
        if ($count >= $this->notification_threshold) {
            // You could send an urgent notification, Slack message, etc.
            $admin_email = get_option('admin_email');
            $subject = "[URGENT] Error Threshold Exceeded for {$component}";
            $message = "The error '{$error_type}' in component '{$component}' has occurred {$count} times in the last hour.";
            wp_mail($admin_email, $subject, $message);
        }
    }
    /**
     * @param string $component Where the error is being logged from
     * @param string $message The error message, usually extracted from $e->getMessage())
     * @param array $context Additional context (i.e: data that was attempted to be acted on)
     * @param string $severity
     *
     * @return array
     */
    public function log(string $component, string $message, array $context = [], string $severity = 'error'): array
    {
        try {
            // Validate severity
            if (!array_key_exists($severity, $this->error_levels)) {
                $severity = 'error';
            }
            // Extract info
            $error_type = sanitize_text_field($context['error_type'] ?? $component);
            $method = isset($context['method']) ? sanitize_text_field($context['method']) : null;
            $page_url = isset($context['url']) ? esc_url_raw($context['url']) : null;
            $user_id = get_current_user_id();
            $user_was_logged_in = $user_id > 0 || (!empty($context['isLoggedIn']));
            // Determine source from context
            $source = $context['source'] ?? (isset($context['url']) ? 'frontend' : 'backend');
            $result = $this->table->insert(
                [
                    'error_type' => $error_type,
                    'component' => $component,
                    'method' => $method,
                    'page_url' => $page_url,
                    'message' => sanitize_textarea_field($message),
                    'context' => json_encode($context),
                    'severity' => $severity,
                    'user_id' => $user_id ?: null,
                    'user_was_logged_in' => $user_was_logged_in ? 1 : 0,
                    'source' => $source,
                    'created_at' => current_time('mysql')
                ]);
            if (!$result) {
                error_log("[ErrorHandler] Database insert failed: " . $this->table->getLastError());
                return ['success' => false, 'message' => $this->table->getLastError()];
            }
            if ($severity === 'critical') {
                $this->checkErrorThreshold($error_type, $component);
            }
            return ['success' => true, 'id' => $result];
        } catch (Exception $e) {
            error_log("[ErrorHandler Exception] " . $e->getMessage());
            return ['success' => false, 'message' => $e->getMessage()];
        $msg = '{'.BASE.'}';
        if (array_key_exists('class', $context)) {
            $msg .= '['.$context['class'].']';
            unset($context['class']);
        }
    }
    /**
     * @param string $component What class or function logs the error
     * @param string $message Details on the error
     * @param array $context additional context of the data that was being processed
     *
     * @return bool Whether the notification is sent successfully
     */
    protected function notifyAdmin(string $component, string $message, array $context):bool
    {
        $admin_email = get_option('admin_email');
        $subject = "[" . get_bloginfo('name') . " Critical Error] {$component}";
        $body = Emails::getInstance()->alert(
            'A critical error has occurred and requires immediate attention',
            'error'
        );
        $body .= Emails::getInstance()->h2('Error Details');
        $body .= Emails::getInstance()->card(
            '<p><strong>Component:</strong> ' . esc_html($component) . '</p>' .
            '<p><strong>Message:</strong></p>' .
            Emails::getInstance()->codeBlock($message),
            'Error Information'
        );
        if (array_key_exists('method', $context)) {
            $msg .= '::'.$context['method'];
            unset($context['method']);
        }
        if (array_key_exists('function', $context)) {
            $msg .= $context['function'];
            unset($context['function']);
        }
        error_log($msg.' '.$message);
        if (!empty($context)) {
            $body .= Emails::getInstance()->h3('Additional Context');
            $body .= Emails::getInstance()->codeBlock(json_encode($context, JSON_PRETTY_PRINT));
            error_log('Context: '.print_r($context, true));
        }
        return Emails::getInstance()->sendEmail($admin_email, $subject, $body, 'CRITICAL ERROR');
    }
    /**
     * Gather summary of the most important errors
     * @param ?string $start_date Defaults to today
     * @param ?string $end_date Defaults to today
     * @return array
     */
    public function gatherErrorSummary(?string $start_date = null, ?string $end_date = null): array
    {
        if (!$start_date) {
            $start_date = gmdate('Y-m-d 00:00:00', strtotime('-1 day'));
        }
        if (!$end_date) {
            $end_date = gmdate('Y-m-d 23:59:59');
        }
        // Most frequent error patterns (deduplicated by component/method/message)
        $frequent = $this->table->getMany([
            'where' => [
                'created_at'    => ['BETWEEN', "{$start_date} AND {$end_date}"]
            ]
        ]);
        return [
            'errors'    => $frequent,
            'date_range' => ['start' => $start_date, 'end' => $end_date]
        ];
    }
    /**
     * Send daily error summary email to administrator
     * @return bool Whether email is sent
     */
    public function sendErrorSummary():bool
    {
        $summary = $this->gatherErrorSummary();
        if (empty($summary['frequent']) && empty($summary['critical'])) {
            return false;
        }
        $admin_email = get_option('admin_email');
        $site_name = get_bloginfo('name');
        $yesterday = date('Y-m-d', strtotime('-1 day'));
        $subject = "[{$site_name}] Daily Error Summary - " . date('Y-m-d');
        // Header with alert
        $body = Emails::getInstance()->h1('Daily Error Summary');
        $body .= sprintf('<p>Error summary for <strong>%s</strong></p>', $yesterday);
        // Summary stats in a grid
        if (!empty($summary['stats'])) {
            $stats = [
                Emails::getInstance()->stat($summary['stats']->total_errors, 'Total Errors'),
                Emails::getInstance()->stat($summary['stats']->critical_count, 'Critical', 'Requires attention'),
                Emails::getInstance()->stat($summary['stats']->error_count, 'Errors'),
                Emails::getInstance()->stat($summary['stats']->warning_count, 'Warnings')
            ];
            $body .= Emails::getInstance()->grid($stats, 4);
        }
        // Alert if critical errors exist
        if (!empty($summary['critical'])) {
            $body .= Emails::getInstance()->alert(
                sprintf('Found %d critical errors that need immediate attention', count($summary['critical'])),
                'error'
            );
        }
        $body .= Emails::getInstance()->spacer();
        // Frequent errors section
        if (!empty($summary['frequent'])) {
            $body .= Emails::getInstance()->h2('Most Frequent Errors');
            foreach ($summary['frequent'] as $error) {
                $cardContent = Emails::getInstance()->badge($error->count . 'x', 'warning') . ' ';
                $cardContent .= '<strong>' . esc_html($error->error_type) . '</strong>';
                $cardContent .= '<p style="margin:10px 0 5px 0;font-size:13px;">' . esc_html(wp_trim_words($error->message, 15)) . '</p>';
                $cardContent .= '<p style="margin:0;font-size:12px;color:' . Emails::getInstance()->colours['dark-200'] . ';">
                Source: ' . esc_html($error->source) . ' |
                Logged in: ' . $error->logged_in_count . ' |
                Logged out: ' . $error->logged_out_count . '
            </p>';
                $body .= Emails::getInstance()->card($cardContent, $error->component);
            }
        }
        // Critical errors section
        if (!empty($summary['critical'])) {
            $body .= Emails::getInstance()->spacer(30);
            $body .= Emails::getInstance()->h2('Recent Critical Errors');
            foreach ($summary['critical'] as $error) {
                $cardContent = '<p><strong>Time:</strong> ' . esc_html($error->created_at) . '</p>';
                $cardContent .= '<p><strong>Message:</strong></p>';
                $cardContent .= Emails::getInstance()->codeBlock($error->message);
                // Include context if available
                if (!empty($error->context)) {
                    $context = json_decode($error->context, true);
                    if (is_array($context)) {
                        $cardContent .= '<p><strong>Context:</strong></p>';
                        $cardContent .= Emails::getInstance()->codeBlock(json_encode($context, JSON_PRETTY_PRINT));
                    }
                }
                $body .= Emails::getInstance()->card($cardContent, $error->component . ': ' . $error->error_type);
            }
        }
        // Dashboard link
        $admin_url = admin_url('admin.php?page=jvb-error-logs');
        $body .= Emails::getInstance()->spacer(30);
        $body .= Emails::getInstance()->button($admin_url, 'View Detailed Logs');
        return Emails::getInstance()->sendEmail($admin_email, $subject, $body, 'ERROR SUMMARY');
    }
    /**
     * Get HTML version of the error summary for nicer emails
     *
     * @param array $summary The error summary data
     * @return string HTML formatted error summary
     */
    protected function getHtmlErrorSummary(array $summary):string
    {
        $html = '<div style="font-family: sans-serif; max-width: 800px; margin: 0 auto;">';
        $html .= '<h1 style="color:#FF0080;border-bottom:2px solid #1B1B1B;padding-bottom:10px;">Error Summary</h1>';
        // Frequent errors table
        if (!empty($summary['frequent'])) {
            $html .= '<h2>Most Frequent Errors</h2>';
            $html .= '<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">';
            $html .= '<tr style="background-color: #1B1B1B; color: #EFEFEF;">';
            $html .= '<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Component</th>';
            $html .= '<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Error Type</th>';
            $html .= '<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Message</th>';
            $html .= '<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Count</th>';
            $html .= '</tr>';
            foreach ($summary['frequent'] as $error) {
                $html .= '<tr>';
                $html .= '<td style="padding:8px;border:1px solid #ddd;">' . esc_html($error->component) . '</td>';
                $html .= '<td style="padding:8px;border:1px solid #ddd;">' . esc_html($error->error_type) . '</td>';
                $html .= '<td style="padding:8px;border:1px solid #ddd;">' . esc_html(wp_trim_words($error->message, 10, '...')) . '</td>';
                $html .= '<td style="padding:8px;border:1px solid #ddd;text-align:center;"><strong>' . esc_html($error->count) . '</strong></td>';
                $html .= '</tr>';
            }
            $html .= '</table>';
        }
        // Critical errors section
        if (!empty($summary['critical'])) {
            $html .= '<h2>Recent Critical Errors</h2>';
            foreach ($summary['critical'] as $error) {
                $html .= '<div style="margin-bottom: 20px; padding: 15px; border-left: 4px solid #FF0080; background-color: #fff9fb;">';
                $html .= '<h3 style="margin-top: 0; color: #1B1B1B;">' . esc_html($error->component) . ': ' . esc_html($error->error_type) . '</h3>';
                $html .= '<p><strong>Time:</strong> ' . esc_html($error->created_at) . '</p>';
                $html .= '<p><strong>Message:</strong> ' . esc_html($error->message) . '</p>';
                // Context display
                if (!empty($error->context)) {
                    $context = json_decode($error->context, true);
                    if (is_array($context)) {
                        $html .= '<div style="margin-top: 10px;">';
                        $html .= '<strong>Context:</strong>';
                        $html .= '<pre style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; overflow: auto; max-height: 200px;">';
                        $html .= esc_html(json_encode($context, JSON_PRETTY_PRINT));
                        $html .= '</pre>';
                        $html .= '</div>';
                    }
                }
                $html .= '</div>';
            }
        }
        // Dashboard link
        $admin_url = admin_url('admin.php?page=jvb-error-logs');
        $html .= '<p style="margin-top: 30px; padding-top: 15px; border-top: 1px solid #ddd;">';
        $html .= '<a href="' . esc_url($admin_url) . '" style="display: inline-block; background-color: #FF0080; color: white; padding: 10px 15px; text-decoration: none; border-radius: 4px;">View All Errors in Dashboard</a>';
        $html .= '</p>';
        $html .= '</div>';
        return $html;
    }
    public function renderAdminPage()
    {
        ?>
        <h1>Error Logs</h1>
        <?php
    }
    public function getErrors(WP_REST_Request $request) {
        $params = $this->buildParams($request);
    }
    protected function buildParams(WP_REST_Request $request):array {
        $allowedSeverity = [
            'all',
            'notice',
            'warning',
            'error',
            'critical',
        ];
        $allowedComponents = [
            'all',
            //Get all components
        ];
        $severity = $request->get_param('severity')??'all';
        $component = $request->get_param('component')??'all';
        return [
            'page'  => (int)$request->get_param('page'),
            'severity'  => (in_array($severity, $allowedSeverity)) ? $severity : 'all',
            'component' => (in_array($component, $allowedComponents)) ? $component : 'all',
        ];
    }
}
inc/managers/Favourites.php
@@ -2,6 +2,7 @@
namespace JVBase\managers;
use Exception;
use JVBase\inc\managers\ErrorHelper;
use JVBase\Instance;
use JVBase\registrar\Registrar;
use JVBase\rest\Rest;
@@ -13,7 +14,7 @@
}
class Favourites
{
    use Instance;
    use Instance, ErrorHelper;
    protected CustomTable $favourites;
    protected CustomTable $lists;
@@ -813,7 +814,7 @@
                'item_id'   => $post_id
            ])->deleteResults();
        } catch (Exception $e) {
            Errors::getInstance()->log('cleanupPostFavourites', $e->getMessage(), [
            $this->logError($e->getMessage(), 'cleanupPostFavourites',[
                'post_id' => $post_id
            ]);
        }
@@ -836,7 +837,7 @@
                'item_id'   => $term_id
            ])->deleteResults();
        } catch (Exception $e) {
            Errors::getInstance()->log('cleanupTermFavourites', $e->getMessage(), [
            $this->logError($e->getMessage(), 'cleanupTermFavourites',[
                'term_id' => $term_id,
                'taxonomy'=> $taxonomy
            ]);
inc/managers/InvitationsManager.php
@@ -2,6 +2,7 @@
namespace JVBase\managers;
use Exception;
use JVBase\inc\managers\ErrorHelper;
use JVBase\Instance;
use JVBase\managers\queue\executors\InvitationExecutor;
use JVBase\managers\queue\Queue;
@@ -18,7 +19,7 @@
class InvitationsManager
{
    use Instance;
    use Instance, ErrorHelper;
    protected array $inviteConfig;
    protected CustomTable $table;
    protected int $expiryDays = 14;
@@ -391,12 +392,7 @@
        } catch (Exception $e) {
            $this->table->rollback();
            Errors::getInstance()->log(
                'invitation_create',
                'Error processing invitations: ' . $e->getMessage(),
                ['user_id' => $userID],
            );
            $this->logError($e->getMessage(), 'processInvitations', ['user_id' => $userID]);
            return [
                'success' => false,
inc/managers/MagicLinks.php
@@ -3,6 +3,7 @@
use JetBrains\PhpStorm\NoReturn;
use JVBase\base\Site;
use JVBase\inc\managers\ErrorHelper;
use JVBase\Instance;
use JVBase\integrations\services\Cloudflare;
use JVBase\meta\Form;
@@ -23,7 +24,7 @@
 */
class MagicLinks
{
    use Instance;
    use Instance, ErrorHelper;
    protected Cache $cache;
    protected Cache $referral_cache;
    protected Cache $rateLimitCache;
@@ -382,7 +383,8 @@
    #[NoReturn] protected function processSignup(array $token_data): void
    {
        if (!array_key_exists('email', $token_data) || !array_key_exists('name', $token_data)) {
            Errors::getInstance()->log('MagicLinkManager', 'Could not process Signup');
            $this->logError('Could not process signup', 'processSignup', ['token_data' => $token_data]);
            $this->cleanURL();
        }
        $user_id = wp_create_user(
@@ -429,7 +431,7 @@
    protected function processReferralSignup(array $token_data): void
    {
        if (!array_key_exists('email', $token_data) || !array_key_exists('name', $token_data)) {
            Errors::getInstance()->log('[MagicLinkManager]Could not process Referral Signup');
            $this->logError('Could not process referral signup', 'processReferralSignup', ['token_data' => $token_data]);
            return;
        }
@@ -455,11 +457,8 @@
                exit;
            }
        } else {
            Errors::getInstance()->log(
                '[MagicLinkManager]',
                $user_id->get_error_message(),
                $token_data
            );
            $this->logError($user_id->get_error_message(), 'processReferralSignup');
            $this->cleanURL();
        }
    }
@@ -473,10 +472,7 @@
        $user = get_user_by('ID', $token_data['user_id']);
        if (!$user) {
            Errors::getInstance()->log(
                'MagicLinkManager',
                'Invalid user, could not reset password',
            );
            $this->logError('Invalid user - could not reset password', 'processPasswordReset', ['token_data' => $token_data]);
            $this->cleanURL();
        }
inc/managers/OperationQueue.php
File was deleted
inc/managers/Responses.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\managers;
use JVBase\inc\managers\ErrorHelper;
use JVBase\Instance;
use JVBase\registrar\Registrar;
use JVBase\rest\routes\ResponseRoutes;
@@ -16,7 +17,7 @@
 */
class Responses
{
    use Instance;
    use Instance, ErrorHelper;
    protected CustomTable $responses;
    protected KarmaManager $karma;
    protected ResponseRoutes $route;
@@ -378,16 +379,11 @@
        ], [
            'user_id'   => $user_id,
        ]);
        Errors::getInstance()->log(
            'Responses',
            'Anonymized responses for deleted user',
        $this->logError('Anonymized responses for deleted user','handleUserDeletion',
            [
                'user_id'   => $user_id,
                'count'     => $updated,
            ],
            'info'
        );
            ]);
    }
    protected function getNotifyableUsers(int $id):array
inc/managers/UploadManager.php
@@ -2,6 +2,7 @@
namespace JVBase\managers;
use JVBase\base\Site;
use JVBase\inc\managers\ErrorHelper;
use JVBase\Instance;
use Exception;
use Imagick;
@@ -41,7 +42,7 @@
 */
class UploadManager
{
    use Instance;
    use Instance, ErrorHelper;
    protected array $allowedTypes = [
        // Images
        'image/jpeg'    => 'image',
@@ -310,7 +311,7 @@
                'stored_at'     => current_time('mysql')
            ];
        } catch (Exception $e) {
            Errors::getInstance()->log('UploadManager','Failed to store uploaded file: ' . print_r($e->getMessage(), true));
            $this->logError('Failed to store uploaded file', 'secureUploadedFile', ['error' => $e->getMessage()]);
            return [];
        }
    }
@@ -350,11 +351,7 @@
            };
        } catch (Exception $e) {
            Errors::getInstance()->log(
                '[UploadManager]:processUpload',
                $e->getMessage(),
                ['temp_path' => $temp_path, 'context' => $context]
            );
            $this->logError($e->getMessage(), 'processUpload');
            return new WP_Error('upload_failed', $e->getMessage());
        }
@@ -548,7 +545,7 @@
                );
            } catch (Exception $e) {
                Errors::getInstance()->log('Uploader', 'Image conversion failed: ' . print_r($e->getMessage(), true));
                $this->logError($e->getMessage(), 'processImage');
                // Fallback to original
                $ext = pathinfo($original_path, PATHINFO_EXTENSION);
                $final_path = "{$full_dir}/{$filename}.{$ext}";
@@ -604,7 +601,7 @@
        try {
            $attachment_id = $this->createAttachment($final_path, $filename, $context, $post_id);
        } catch (Exception $e) {
            Errors::getInstance()->log('Uploader', 'Attachment creation failed: '.print_r($e->getMessage(), true));
            $this->logError($e->getMessage(), 'processVideo');
            return ['success' => false];
        }
@@ -642,7 +639,7 @@
        try {
            $attachment_id = $this->createAttachment($final_path, $filename, $context, $post_id);
        } catch (Exception $e) {
            Errors::getInstance()->log('Uploader','Conversion failed: '.print_r($e->getMessage(), true));
            $this->logError($e->getMessage(), 'processDocument');
            return ['success' => false];
        }
inc/managers/UserTermsManager.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\managers;
use JVBase\inc\managers\ErrorHelper;
use JVBase\Instance;
use JVBase\managers\queue\Queue;
use JVBase\registrar\Registrar;
@@ -13,7 +14,7 @@
}
class UserTermsManager
{
    use Instance;
    use Instance, ErrorHelper;
    protected CustomTable $index;
    private function __construct()
@@ -317,13 +318,7 @@
                'result'    => $results
            ];
        } catch (Exception $e) {
            Errors::getInstance()->log(
                '[UserTermsManager]:processOperation',
                "Exception during operation processing: " . $e->getMessage(),
                [
                    'operation' => $operation->id,
                ]
            );
            $this->logError($e->getMessage(), 'processOperation');
            return [
                'success'   => false,
                'result'    => $e->getMessage()
inc/managers/_setup.php
@@ -6,7 +6,6 @@
use JVBase\registrar\Registrar;
use JVBase\base\Site;
require(JVB_DIR . '/inc/managers/ScriptLoader.php');
require(JVB_DIR . '/inc/managers/Errors.php');
require(JVB_DIR . '/inc/managers/CustomTable.php');
//require(JVB_DIR . '/inc/managers/CacheManager.php');
require(JVB_DIR . '/inc/managers/Cache.php');
inc/managers/queue/Queue.php
@@ -5,6 +5,7 @@
}
use Exception;
use JVBase\inc\managers\ErrorHelper;
use JVBase\Instance;
use JVBase\managers\AdminPages;
use JVBase\managers\Errors;
@@ -16,7 +17,7 @@
class Queue
{
    use Instance;
    use Instance, ErrorHelper;
    private Storage $storage;
    private Processor $processor;
    private TypeRegistry $registry;
@@ -109,7 +110,8 @@
            ];
        } catch (Exception $e) {
            Errors::getInstance()->log('queue', $e->getMessage(), $data, 'high');
            $this->logError($e->getMessage(), 'add', $data);
            return new WP_Error('queue_failed', $e->getMessage());
        }
    }
inc/managers/queue/executors/ContentExecutor.php
@@ -3,6 +3,7 @@
use DateTime;
use JVBase\managers\queue\{Executor, Operation, Progress, Result};
use JVBase\inc\managers\ErrorHelper;
use JVBase\managers\Errors;
use JVBase\managers\Roles;
use JVBase\meta\Meta;
@@ -20,6 +21,7 @@
 */
final class ContentExecutor implements Executor
{
    use ErrorHelper;
    private const HANDLED_TYPES = [
        'content_update',
//      'batch_creation',
@@ -45,15 +47,11 @@
            };
        } catch (Exception $e) {
            Errors::getInstance()->log(
                '[ContentExecutor]:execute',
                $e->getMessage(),
                [
                    'operation_id'   => $operation->id,
                    'operation_type' => $operation->type,
                    'user_id'        => $operation->userId,
                ]
            );
            $this->logError($e->getMessage(), 'execute', [
                'operation_id'   => $operation->id,
                'operation_type' => $operation->type,
                'user_id'        => $operation->userId,
            ]);
            return new Result(
                outcome: 'failed',
inc/managers/queue/executors/IntegrationExecutor.php
@@ -3,6 +3,7 @@
use JVBase\managers\queue\{Executor, Operation, Progress, Result};
use Exception;
use JVBase\inc\managers\ErrorHelper;
use JVBase\integrations\ServiceMap;
use JVBase\managers\Errors;
@@ -30,7 +31,7 @@
 */
final class IntegrationExecutor implements Executor
{
    use ServiceMap;
    use ServiceMap, ErrorHelper;
    public function execute(Operation $operation, Progress $progress): Result
@@ -59,15 +60,11 @@
            };
        } catch (Exception $e) {
            Errors::getInstance()->log(
                '[IntegrationExecutor]:execute',
                $e->getMessage(),
                [
                    'operation_id'   => $operation->id,
                    'operation_type' => $operation->type,
                    'user_id'        => $operation->userId,
                ]
            );
            $this->logError($e->getMessage(),'execute', [
                'operation_id'   => $operation->id,
                'operation_type' => $operation->type,
                'user_id'        => $operation->userId,
            ]);
            return new Result(
                outcome: 'failed',
inc/managers/queue/executors/UploadExecutor.php
@@ -2,6 +2,7 @@
namespace JVBase\managers\queue\executors;
use JVBase\managers\queue\{Executor, Operation, Progress, Queue, Result};
use JVBase\inc\managers\ErrorHelper;
use JVBase\managers\Cache;
use JVBase\managers\Errors;
use JVBase\managers\UploadManager;
@@ -21,6 +22,7 @@
 */
final class UploadExecutor implements Executor
{
    use ErrorHelper;
    private const HANDLED_TYPES = [
        'image_upload',
        'video_upload',
@@ -58,15 +60,11 @@
            };
        } catch (Exception $e) {
            Errors::getInstance()->log(
                '[UploadExecutor]:execute',
                $e->getMessage(),
                [
                    'operation_id'   => $operation->id,
                    'operation_type' => $operation->type,
                    'user_id'        => $operation->userId,
                ]
            );
            $this->logError($e->getMessage(), 'execute', [
                'operation_id'   => $operation->id,
                'operation_type' => $operation->type,
                'user_id'        => $operation->userId,
            ]);
            return new Result(
                outcome: 'failed',
inc/meta/Storage.php
@@ -2,6 +2,7 @@
namespace JVBase\meta;
use Exception;
use JVBase\inc\managers\ErrorHelper;
use JVBase\managers\Cache;
use JVBase\managers\Errors;
use wpdb;
@@ -15,6 +16,7 @@
 */
class Storage
{
    use ErrorHelper;
    protected wpdb $wpdb;
    public function __construct()
@@ -171,11 +173,8 @@
        } catch (Exception $e) {
            $this->wpdb->query('ROLLBACK');
            Errors::getInstance()->log('meta_storage', $e->getMessage(), [
                'item_id' => $item->id,
                'object_type' => $item->objectType,
                'fields' => array_keys($dirty)
            ]);
            $this->logError($e->getMessage(), 'save');
            return false;
        }
    }
@@ -327,7 +326,7 @@
                $results[$meta->item()->id ?? 'options'] = false;
            }
            Errors::getInstance()->log('meta_storage', 'Bulk save failed: ' . $e->getMessage());
            self::staticLogError($e->getMessage(), 'save');
            return $results;
        }
inc/registrar/Posts.php
@@ -2,6 +2,7 @@
namespace JVBase\registrar;
use JVBase\forms\TaxonomySelector;
use JVBase\inc\managers\ErrorHelper;
use JVBase\managers\Errors;
use WP_Post;
@@ -9,6 +10,7 @@
    exit;
}
final class Posts {
    use ErrorHelper;
    public string $postType;
    public string $singular;
    public string $plural;
@@ -358,7 +360,7 @@
        }
        if (is_wp_error($registered)) {
            Errors::getInstance()->log('JVBase\registrar\Posts', 'Could not register post type', $registered->get_error_messages());
            $this->logError('Could not register post type', 'register', $registered->get_error_messages());
        }
    }
inc/registrar/helpers/AddCustomFields.php
@@ -99,10 +99,14 @@
            $schema = is_array($rawSchema) ? $this->schema($rawSchema) : $rawSchema;
            $default = $config['type'] === 'repeater' ?
                $this->determineDefault($schema['schema']['properties']??[]) :
                $config['default'] ?? MetaTypeManager::getType($config['type'])['default'] ?? '';
            register_post_meta($this->registrar->getBased(), jvbCheckBase($name), [
                'single'            => true,
                'type'              => $metaType,
                'default'           => $config['default'] ?? MetaTypeManager::getType($config['type'])['default'] ?? '',
                'default'           => $default,
                'sanitize_callback' => fn($value) => (Sanitizer::sanitize($value, $config)),
                'auth_callback'     => fn($allowed, $key, $postID) => current_user_can('edit_post', $postID),
                'show_in_rest'      => $schema,
@@ -285,4 +289,16 @@
    {
    }
    protected function determineDefault(array $properties):array
    {
        $out = [];
        foreach ($properties as $prop => $value) {
            $out[$prop] = match ($value) {
                'object'    => [],
                default     => ''
            };
        }
        return $out;
    }
}
inc/rest/Rest.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\rest;
use JVBase\inc\managers\ErrorHelper;
use JVBase\integrations\services\Cloudflare;
use JVBase\managers\Cache;
use JVBase\managers\Errors;
@@ -30,6 +31,7 @@
 */
abstract class Rest
{
    use ErrorHelper;
    protected string $namespace = 'jvb/v1';
    protected ?Cache $cache = null;
    protected string $cacheName = '';
@@ -645,24 +647,12 @@
        ]);
        try {
            Errors::getInstance()->log('security_audit', $event, $context, 'info');
            $this->logError($event,'auditLog', $context);
        } catch (Exception $e) {
            error_log("Security Audit: {$event} - " . json_encode($context));
        }
    }
    /**
     * Log errors with proper context
     */
    protected function logError(string $message, array $context = [], string $severity = 'error'): void
    {
        try {
            Errors::getInstance()->log(static::class, $message, $context, $severity);
        } catch (Exception $e) {
            error_log(static::class . " Error: {$message} - " . json_encode($context));
        }
    }
    /************************************************************
    SESSION FINGERPRINT
     ************************************************************/
inc/rest/RestRouteManager.php
File was deleted
inc/rest/routes/ContentTermsRoutes.php
@@ -411,8 +411,7 @@
            return $this->error('Failed to create request');
        } catch (Exception $e) {
            $this->logError('createRequest', [
                'error' => $e->getMessage(),
            $this->logError($e->getMessage(),'createRequest', [
                'user_id' => $userID,
                'term_id' => $termID
            ]);
@@ -478,8 +477,7 @@
            return $this->success(['message' => 'Request ' . $status]);
        } catch (Exception $e) {
            $this->logError('processRequest', [
                'error' => $e->getMessage(),
            $this->logError($e->getMessage(), 'processRequest', [
                'request_id' => $requestID,
                'action' => $action
            ]);
@@ -526,8 +524,7 @@
            return $this->error('Failed to update permissions');
        } catch (Exception $e) {
            $this->logError('updatePermissions', [
                'error' => $e->getMessage(),
            $this->logError($e->getMessage(),'updatePermissions', [
                'term_id' => $termID,
                'target_user' => $targetUserID,
                'role' => $role
inc/rest/routes/ErrorRoutes.php
@@ -54,11 +54,9 @@
        // Determine severity based on error type
        $severity = $this->getSeverityFromType($error_type);
        Errors::getInstance()->log(
            $contextData['component'] ?? 'client-js',
        Errors::log(
            $message,
            $contextData,
            $severity
        );
        return Response::success(['message'=>'Error logged']);
inc/rest/routes/FavouritesRoutes.php
@@ -407,8 +407,7 @@
            return ['added' => $added, 'errors' => $errors];
        } catch (Exception $e) {
            $this->logError('addItemsToList', [
                'error' => $e->getMessage(),
            $this->logError($e->getMessage(),'addItemsToList', [
                'user_id' => $user_id,
                'list_id' => $list_id,
            ]);
@@ -620,8 +619,7 @@
                ];
            } catch (Exception $e) {
                $this->logError('getListDetails', [
                    'error' => $e->getMessage(),
                $this->logError($e->getMessage(), 'getListDetails', [
                    'user_id' => $user_id,
                    'list_id' => $list_id
                ]);
@@ -750,8 +748,7 @@
            ];
        } catch (Exception $e) {
            $this->logError('updateList', [
                'error' => $e->getMessage(),
            $this->logError($e->getMessage(),'updateList', [
                'user_id' => $user_id,
                'list_id' => $list_id
            ]);
@@ -942,8 +939,7 @@
            ];
        } catch (Exception $e) {
            $this->logError('removeFromList', [
                'error' => $e->getMessage(),
            $this->logError($e->getMessage(), 'removeFromList', [
                'user_id' => $user_id,
                'list_id' => $list_id
            ]);
@@ -1181,8 +1177,7 @@
            ];
        } catch (Exception $e) {
            $this->logError('unshareList', [
                'error' => $e->getMessage(),
            $this->logError($e->getMessage(),'unshareList', [
                'user_id' => $user_id,
                'list_id' => $list_id,
                'email' => $email
@@ -1322,8 +1317,7 @@
            ];
        } catch (Exception $e) {
            $this->logError('acceptListInvitation', [
                'error' => $e->getMessage(),
            $this->logError($e->getMessage(),'acceptListInvitation', [
                'token' => $token,
                'email' => $email
            ]);
inc/rest/routes/FormRoutes.php
@@ -108,7 +108,7 @@
            return $this->success($result);
        } catch (Exception $e) {
            $this->logError('Form submission error', [
            $this->logError('Form submission error', 'submitForm', [
                'message' => $e->getMessage(),
                'trace' => $e->getTraceAsString()
            ]);
inc/rest/routes/ImporterRoutes.php
@@ -101,7 +101,7 @@
        $results = $importer->importFromCSV($file['tmp_name'], $options);
        if (is_wp_error($results)) {
            $this->logError('Client import failed', [
            $this->logError('Client import failed','importClients', [
                'error' => $results->get_error_message(),
                'file' => $file['name']
            ]);
@@ -163,7 +163,7 @@
        $results = $importer->importFromCSV($file['tmp_name'], $options);
        if (is_wp_error($results)) {
            $this->logError('Sales import failed', [
            $this->logError('Sales import failed', 'importSales', [
                'error' => $results->get_error_message(),
                'file' => $file['name']
            ]);
inc/rest/routes/IntegrationsHelcimRoutes.php
@@ -93,7 +93,7 @@
            return $this->success($result);
        } catch (Exception $e) {
            $this->logError('Helcim checkout init failed', ['error' => $e->getMessage()]);
            $this->logError('Helcim checkout init failed','handleInitializeCheckout', ['error' => $e->getMessage()]);
            return $this->error($e->getMessage());
        }
    }
inc/rest/routes/IntegrationsRoutes.php
@@ -58,19 +58,16 @@
        $theUserID = current_user_can('manage_options') ? null : get_current_user_id();
        if ($theUserID === false) {
            error_log('User not logged in');
            return $this->validationError(['message' => 'Not logged in']);
        }
        $integration = $this->getService($service);
        if (!method_exists($integration, 'switchUser')) {
            return $this->validationError(['message' => 'Cannot get user integration']);
        }
        $integration = $this->getServiceUser($service, $theUserID);
        if (!$integration || !method_exists($integration, 'processAction')) {
            error_log('No processAction method');
            return $this->validationError(['message'=>'Invalid service']);
        }
        $integration->switchUser($theUserID);
        $integration->getCredentials();
        // Handle the action
@@ -86,6 +83,7 @@
            return $this->success($result);
        } catch (Exception $e) {
            error_log(print_r($e->getMessage(), true));
            return $this->error($e->getMessage());
        }
    }
inc/rest/routes/IntegrationsSquareRoutes.php
@@ -145,7 +145,7 @@
            return $this->success($result);
        } catch (Exception $e) {
            $this->logError('Payment processing failed', [
            $this->logError('Payment processing failed', 'handlePaymentProcessing',[
                'error' => $e->getMessage(),
                'idempotency_key' => $data['idempotency_key']
            ]);
inc/rest/routes/LoginRoutes.php
@@ -263,7 +263,7 @@
        );
        if (is_wp_error($user_id)) {
            $this->logError('Registration failed', [
            $this->logError('Registration failed', 'handleRegister', [
                'email' => $email,
                'error' => $user_id->get_error_message(),
            ]);
@@ -351,7 +351,7 @@
            $result = MagicLinks::getInstance()->sendMagicLink($email, 'reset');
            if (is_wp_error($result)) {
                $this->logError('Magic link send failed', [
                $this->logError('Magic link send failed', 'handleLostPassword',[
                    'email' => $email,
                    'error' => $result->get_error_message(),
                ]);
@@ -360,7 +360,7 @@
            // Standard WordPress password reset
            $key = get_password_reset_key($user);
            if (is_wp_error($key)) {
                $this->logError('Reset key generation failed', [
                $this->logError('Reset key generation failed', 'handleLostPassword', [
                    'email' => $email,
                    'error' => $key->get_error_message(),
                ]);
@@ -468,7 +468,7 @@
        $result = MagicLinks::getInstance()->sendMagicLink($email, $type, $context);
        if (is_wp_error($result)) {
            $this->logError('Magic link send failed', [
            $this->logError('Magic link send failed', 'handleMagicLink', [
                'email' => $email,
                'type' => $type,
                'error' => $result->get_error_message(),
@@ -597,11 +597,11 @@
        try {
            Referrals::getInstance()->processReferralCode($user_id, $referral_code);
        } catch (Exception $e) {
            $this->logError('Referral processing failed', [
            $this->logError('Referral processing failed', 'processReferralCode', [
                'user_id' => $user_id,
                'code' => $referral_code,
                'error' => $e->getMessage(),
            ], 'warning');
            ]);
        }
    }
inc/rest/routes/NotificationsRoutes.php
@@ -610,10 +610,9 @@
        } catch (Exception $e) {
            global $wpdb;
            $wpdb->query('ROLLBACK');
            $this->logError('markContentNotificationRead exception', [
            $this->logError($e->getMessage(),'markContentNotificationRead', [
                'seen_id' => $seen_id,
                'user_id' => $user_id,
                'error' => $e->getMessage()
            ]);
            return [
                'success' => false,
@@ -1154,8 +1153,7 @@
            ]);
        } catch (Exception $e) {
            // Don't fail the request if metrics tracking fails
            $this->logError('trackMetrics failed', [
                'error' => $e->getMessage(),
            $this->logError($e->getMessage(),'trackMetrics', [
                'user_id' => $user_id,
                'action' => $action
            ]);
inc/rest/routes/TermRoutes.php
@@ -601,9 +601,9 @@
            ], 201); // 201 Created
        } catch (Exception $e) {
            Errors::getInstance()->log(
                'terms',
            $this->logError(
                'Term creation failed: ' . $e->getMessage(),
                'createTermRequest',
                ['user_id' => $user_id, 'taxonomy' => $taxonomy, 'name' => $name]
            );
inc/rest/routes/UploadRoutes.php
@@ -239,7 +239,7 @@
            $secured_files = $this->secureFiles($files, $args);
            if (empty($secured_files)) {
                $this->logError('No valid files to upload');
                $this->logError('No valid files to upload', 'handleUpload');
                return $this->error('No valid files to upload');
            }
@@ -250,15 +250,7 @@
        } catch (Exception $e) {
            // Error handling...
            Errors::getInstance()->log(
                '[UploadRoutes]:handleUploadRequest',
                $e->getMessage(),
                [
                    'request_data' => $request->get_params(),
                    'files_info' => $this->getFilesInfo($_FILES),
                    'trace' => $e->getTraceAsString()
                ]
            );
            $this->logError($e->getMessage(), 'handleUpload');
            return $this->error($e->getMessage());
        }
    }
@@ -485,14 +477,7 @@
            );
        } catch (Exception $e) {
            Errors::getInstance()->log(
                '[UploadRoutes]:handleMetadataUpdate',
                $e->getMessage(),
                [
                    'request_data' => $request->get_params(),
                    'trace' => $e->getTraceAsString()
                ]
            );
            $this->logError($e->getMessage(), 'handleMetadataUpdate');
            return $this->sendResponse(
                false,
@@ -710,14 +695,8 @@
            return $this->queued($ID['operation_id'], 'Files uploaded and posts queued for creation');
        } catch (Exception $e) {
            Errors::getInstance()->log(
                '[UploadRoutes]:handleGroupingRequest',
                $e->getMessage(),
                [
                    'request_data' => $request->get_params(),
                    'trace' => $e->getTraceAsString()
                ]
            );
            $this->logError($e->getMessage(), 'handleGroupingRequest');
            return $this->error('Grouping operation failed: '.$e->getMessage());
        }
    }
inc/rest/routes/UserRoutes.php
@@ -1,4 +1,5 @@
<?php
namespace JVBase\rest\routes;
use JVBase\Instance;
use JVBase\managers\Cache;
jvb.php
@@ -137,6 +137,9 @@
require(JVB_DIR.'/inc/Instance.php');
require(JVB_DIR.'/base/_setup.php');
require(JVB_DIR . '/inc/managers/Errors.php');
require(JVB_DIR . '/inc/managers/ErrorHelper.php');
require(JVB_DIR.'/inc/utility/setup.php');
require(JVB_DIR.'/checks.php');
@@ -404,12 +407,14 @@
use JVBase\rest\routes\ErrorRoutes;
use JVBase\rest\routes\FeedRoutes;
use JVBase\rest\routes\FormRoutes;
use JVBase\rest\routes\IntegrationsRoutes;
use JVBase\rest\routes\NewsRoutes;
use JVBase\rest\routes\OptionsRoutes;
use JVBase\rest\routes\SEORoutes;
use JVBase\rest\routes\SettingsRoutes;
use JVBase\rest\routes\TermRoutes;
use JVBase\rest\routes\UploadRoutes;
use JVBase\rest\routes\UserRoutes;
function jvbLoadRoutes():void
{
@@ -421,6 +426,10 @@
    }
    UserRoutes::getInstance();
    if (Site::hasAnyIntegration()) {
        IntegrationsRoutes::getInstance();
    }
    if (Site::has('feed_block')) {
        FeedRoutes::getInstance();
    }