From a6413da67a4579866f0fd0323ddb23a7b6b9654a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 18 Sep 2026 17:00:53 +0000
Subject: [PATCH] =Cleaning up the Integrations setup a bit, started the Helcim integration work too

---
 inc/managers/CustomTable.php                         |   31 
 jvb.php                                              |    9 
 inc/rest/routes/NotificationsRoutes.php              |    6 
 inc/rest/routes/TermRoutes.php                       |    4 
 inc/integrations/_Base.php                           |   25 
 inc/managers/_setup.php                              |    1 
 inc/rest/routes/ImporterRoutes.php                   |    4 
 inc/rest/routes/IntegrationsSquareRoutes.php         |    2 
 inc/integrations/Field.php                           |   41 
 inc/managers/queue/executors/ContentExecutor.php     |   16 
 inc/registrar/helpers/AddCustomFields.php            |   18 
 inc/integrations/Integrations.php                    |   18 
 inc/integrations/services/Facebook.php               |    1 
 inc/managers/queue/executors/IntegrationExecutor.php |   17 
 inc/integrations/services/SquareOld.php              |   12 
 inc/integrations/ServiceMap.php                      |    6 
 inc/managers/Responses.php                           |   12 
 inc/integrations/services/GoogleMaps.php             |    1 
 inc/managers/DirectoryManager.php                    |    2 
 inc/integrations/services/Umami.php                  |   21 
 inc/rest/routes/IntegrationsHelcimRoutes.php         |    2 
 inc/rest/routes/FormRoutes.php                       |    2 
 inc/integrations/Auth.php                            |   12 
 inc/managers/UserTermsManager.php                    |   11 
 inc/helpers/renderFields.php                         |    6 
 inc/meta/Storage.php                                 |   11 
 inc/blocks/_setup.php                                |    3 
 inc/integrations/services/GoogleMyBusiness.php       |   35 
 inc/registrar/Posts.php                              |    4 
 inc/integrations/services/Cloudflare.php             |    9 
 inc/rest/routes/ErrorRoutes.php                      |    4 
 inc/rest/routes/FavouritesRoutes.php                 |   18 
 inc/rest/routes/ContentTermsRoutes.php               |    9 
 inc/integrations/services/PostMark.php               |   13 
 inc/managers/Errors.php                              |  513 -----------
 inc/rest/routes/UploadRoutes.php                     |   31 
 inc/managers/queue/Queue.php                         |    6 
 inc/managers/ErrorHelper.php                         |   27 
 inc/managers/UploadManager.php                       |   17 
 inc/integrations/services/Helcim.php                 |  109 ++
 inc/integrations/Admin.php                           |  119 ++
 inc/integrations/OAuth.php                           |   83 +
 inc/rest/routes/IntegrationsRoutes.php               |   10 
 inc/integrations/SyncDefaults.php                    |    1 
 inc/managers/queue/executors/UploadExecutor.php      |   16 
 inc/managers/Dashboard/Dashboard.php                 |    7 
 inc/managers/MagicLinks.php                          |   20 
 inc/admin/Integrations.php                           |   29 
 /dev/null                                            | 1058 -------------------------
 inc/rest/Rest.php                                    |   16 
 inc/integrations/SyncHelpers.php                     |    2 
 inc/rest/routes/LoginRoutes.php                      |   12 
 inc/managers/InvitationsManager.php                  |   10 
 inc/managers/Favourites.php                          |    7 
 inc/rest/routes/UserRoutes.php                       |    1 
 55 files changed, 612 insertions(+), 1,868 deletions(-)

diff --git a/inc/admin/Integrations.php b/inc/admin/Integrations.php
index f61289c..3462c18 100644
--- a/inc/admin/Integrations.php
+++ b/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
 	}
diff --git a/inc/blocks/_setup.php b/inc/blocks/_setup.php
index 091429f..761ad88 100644
--- a/inc/blocks/_setup.php
+++ b/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
diff --git a/inc/helpers/renderFields.php b/inc/helpers/renderFields.php
index 44af18d..4a36140 100644
--- a/inc/helpers/renderFields.php
+++ b/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) {
diff --git a/inc/integrations/Admin.php b/inc/integrations/Admin.php
index a243bb3..cb9efdf 100644
--- a/inc/integrations/Admin.php
+++ b/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);
+	}
 
 }
diff --git a/inc/integrations/Auth.php b/inc/integrations/Auth.php
index 85dc232..231c07b 100644
--- a/inc/integrations/Auth.php
+++ b/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 = [];
diff --git a/inc/integrations/Field.php b/inc/integrations/Field.php
index 61cfc55..1687d9f 100644
--- a/inc/integrations/Field.php
+++ b/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;
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index f2e8147..d6ff18f 100644
--- a/inc/integrations/Integrations.php
+++ b/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;
+	}
+
 }
diff --git a/inc/integrations/OAuth.php b/inc/integrations/OAuth.php
index df742fa..5750df4 100644
--- a/inc/integrations/OAuth.php
+++ b/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
diff --git a/inc/integrations/ServiceMap.php b/inc/integrations/ServiceMap.php
index 7cc2069..7fa64c2 100644
--- a/inc/integrations/ServiceMap.php
+++ b/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());
 	}
 }
diff --git a/inc/integrations/SyncDefaults.php b/inc/integrations/SyncDefaults.php
index 4c9ad2b..271d4b0 100644
--- a/inc/integrations/SyncDefaults.php
+++ b/inc/integrations/SyncDefaults.php
@@ -128,6 +128,7 @@
 				->description($options['description'])
 				->content($this->getContentTypeContent($allowed));
 		}
+
 		$tabs->render(false);
 	}
 	public function getContentTypeContent(array $content):string
diff --git a/inc/integrations/SyncHelpers.php b/inc/integrations/SyncHelpers.php
index 84f5303..db97940 100644
--- a/inc/integrations/SyncHelpers.php
+++ b/inc/integrations/SyncHelpers.php
@@ -13,7 +13,7 @@
 }
 
 trait SyncHelpers {
-	use _Base;
+	use _Base, SyncDefaults;
 	/**
 	 * Queue types
 	 * These types match with IntegrationExecutor
diff --git a/inc/integrations/_Base.php b/inc/integrations/_Base.php
index 3c4f266..d381877 100644
--- a/inc/integrations/_Base.php
+++ b/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);
 	}
diff --git a/inc/integrations/services/Cloudflare.php b/inc/integrations/services/Cloudflare.php
index b8b9278..fb4d405 100644
--- a/inc/integrations/services/Cloudflare.php
+++ b/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
+	{
+	}
 }
diff --git a/inc/integrations/services/Facebook.php b/inc/integrations/services/Facebook.php
index 3c54f91..8725303 100644
--- a/inc/integrations/services/Facebook.php
+++ b/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;
diff --git a/inc/integrations/services/GoogleMaps.php b/inc/integrations/services/GoogleMaps.php
index adfa358..2910bac 100644
--- a/inc/integrations/services/GoogleMaps.php
+++ b/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
diff --git a/inc/integrations/services/GoogleMyBusiness.php b/inc/integrations/services/GoogleMyBusiness.php
index 694285e..44002c7 100644
--- a/inc/integrations/services/GoogleMyBusiness.php
+++ b/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
 			{
diff --git a/inc/integrations/services/Helcim.php b/inc/integrations/services/Helcim.php
index f388c96..a54f0ef 100644
--- a/inc/integrations/services/Helcim.php
+++ b/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
 	*************************************************************************/
diff --git a/inc/integrations/services/PostMark.php b/inc/integrations/services/PostMark.php
index 5dd9ea8..ac996a1 100644
--- a/inc/integrations/services/PostMark.php
+++ b/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
 		{
diff --git a/inc/integrations/services/SquareOld.php b/inc/integrations/services/SquareOld.php
index d2c4611..b5fd010 100644
--- a/inc/integrations/services/SquareOld.php
+++ b/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']
diff --git a/inc/integrations/services/Umami.php b/inc/integrations/services/Umami.php
index b67bc45..bd83cc2 100644
--- a/inc/integrations/services/Umami.php
+++ b/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.
+	}
 }
diff --git a/inc/managers/CustomTable.php b/inc/managers/CustomTable.php
index d86a631..3bb0114 100644
--- a/inc/managers/CustomTable.php
+++ b/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);
diff --git a/inc/managers/Dashboard/Dashboard.php b/inc/managers/Dashboard/Dashboard.php
index 8a1e1d8..3482b02 100644
--- a/inc/managers/Dashboard/Dashboard.php
+++ b/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
 			}
 		}
diff --git a/inc/managers/DirectoryManager.php b/inc/managers/DirectoryManager.php
index 4f4b009..c3e2010 100644
--- a/inc/managers/DirectoryManager.php
+++ b/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,
diff --git a/inc/managers/ErrorHelper.php b/inc/managers/ErrorHelper.php
new file mode 100644
index 0000000..498a616
--- /dev/null
+++ b/inc/managers/ErrorHelper.php
@@ -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));
+	}
+}
diff --git a/inc/managers/Errors.php b/inc/managers/Errors.php
index e45050c..40d0252 100644
--- a/inc/managers/Errors.php
+++ b/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',
-        ];
-    }
 }
diff --git a/inc/managers/Favourites.php b/inc/managers/Favourites.php
index 23ce3be..06e8a42 100644
--- a/inc/managers/Favourites.php
+++ b/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
 			]);
diff --git a/inc/managers/InvitationsManager.php b/inc/managers/InvitationsManager.php
index cc3078d..de21889 100644
--- a/inc/managers/InvitationsManager.php
+++ b/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,
diff --git a/inc/managers/MagicLinks.php b/inc/managers/MagicLinks.php
index 7ec979b..ec8efd8 100644
--- a/inc/managers/MagicLinks.php
+++ b/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();
 		}
 
diff --git a/inc/managers/OperationQueue.php b/inc/managers/OperationQueue.php
deleted file mode 100644
index 8dba76a..0000000
--- a/inc/managers/OperationQueue.php
+++ /dev/null
@@ -1,2183 +0,0 @@
-<?php
-namespace JVBase\managers;
-
-use Exception;
-use WP_Error;
-use WP_REST_Response;
-use WP_REST_Request;
-use wpdb;
-
-if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
-}
-
-//# Every minute - main queue processing
-//* * * * * cd /path/to/wordpress && wp cron event run jvb_process_queue
-//
-//# Every hour - maintenance tasks
-//0 * * * * cd /path/to/wordpress && wp cron event run jvb_queue_maintenance
-//
-//# Daily at 2 AM - metrics report
-//0 2 * * * cd /path/to/wordpress && wp cron event run jvbEmailDailyMetricsReport
-//
-//
-//
-//Click "Create" to save the cron job
-class OperationQueue
-{
-    protected string $table = BASE.'_operation_queue';
-    protected string $metricsTable = BASE.'stats__operation_queue';
-    protected int $max_attempts = 3;
-	protected wpdb $wpdb;
-
-    // System load thresholds
-    protected array $load_thresholds = [
-        'cpu'       => 80, // Max CPU usage percentage
-        'memory'    => 85, // Max memory usage percentage
-        'load_avg'  => 1.6 // Max load average (80% of 2 cores)
-    ];
-
-    protected int $maxPerBatch = 25;
-
-    // Queue configuration
-    protected array $config = [
-        'max_operations_per_batch'  => 25,  // Reduced from 100 for better control
-        'max_concurrent_operations' => 100, // Adjusted for 2 CPU cores
-
-    ];
-
-	protected ?Cache $cache = null;
-	protected int $ttl = 300;
-	// Cache keys for different data types
-	private const CACHE_QUEUE_STATUS = 'status';
-	private const CACHE_HAS_ITEMS = 'has_items';
-	private const CACHE_OPERATION_PREFIX = 'op_';
-	private const CACHE_USER_QUEUE_PREFIX = 'user_queue_';
-	private const CACHE_QUEUE_SIZE = 'queue_size';
-
-
-    public function __construct()
-    {
-		global $wpdb;
-		$this->wpdb = $wpdb;
-		$this->cache = Cache::for('queue', 'user',DAY_IN_SECONDS)->user();
-        add_action('jvb_process_queue', [ $this, 'checkQueue' ]);
-		add_action('jvb_queue_maintenance', [$this, 'hourlyMaintenance']);
-        add_action('jvbEmailDailyMetricsReport', [$this, 'emailDailyMetricsReport']);
-
-        jvb_register_do_once('queue_admin_action_registered', [$this, 'registerAdminAction']);
-        add_filter(BASE.'admin_action_filter', [$this, 'adminActionFilter'], 10, 3);
-//		add_filter('jvbAdminSubpages', [$this, 'addSubpage'], 10, 1);
-
-		if ( ! wp_next_scheduled( 'jvb_process_queue' ) ) {
-			wp_schedule_event( time(), 'every-minute', 'jvb_process_queue' );
-		}
-		if ( ! wp_next_scheduled( 'jvb_queue_maintenance' ) ) {
-			wp_schedule_event( time(), 'hourly', 'jvb_queue_maintenance' );
-		}
-    }
-
-	public function addSubpage(array $subpages):array
-	{
-		$subpages[] = [
-			'page_title' => 'Operation Queue',           // page_title
-			'menu_title' => 'Queue',           // menu_title
-			'capability' => 'manage_options',         // capability
-			'menu_slug'	 => 'operation-queue',           // menu_slug (will become BASE.'integrations')
-			'callback'	 => [$this, 'renderAdminPage'],    // callback
-		];
-
-		return $subpages;
-	}
-    public function registerAdminAction():void
-    {
-        $admin = AdminPages::getInstance();
-        $admin->registerAction(
-            'Restart Stuck Operations',
-            'restart-stuck-operations',
-            'manage_options',
-            'all'
-        );
-        $admin->registerAction(
-            'Unlock Queue',
-            'unlock-operation-queue',
-            'manage_options',
-            'all'
-        );
-    }
-
-    /**
-     * @param WP_REST_Response $response
-     * @param string $action
-     *
-     * @return bool|WP_REST_Response
-     */
-    public function adminActionFilter(WP_REST_Response $response, WP_REST_Request $request, string $action):WP_REST_Response|bool
-    {
-        switch ($action) {
-            case 'unlock-operation-queue':
-                $this->unlockQueue();
-                return new WP_REST_Response([
-                    'success'   => true,
-                    'message'   => 'Unlocked Queue'
-                ]);
-            case 'restart-stuck-operations':
-                $this->restartStuckOperations();
-                return new WP_REST_Response([
-                    'success'   => true,
-                    'message'   => 'Restarted Stuck Operations'
-                ]);
-            default:
-                return $response;
-        }
-    }
-
-	protected function getQueueInfo(): array
-	{
-		$cacheKey = 'queue_info';
-		$cached = $this->cache->get($cacheKey);
-		if ($cached !== false) {
-			return $cached;
-		}
-
-		$table = $this->wpdb->prefix . $this->table;
-		$current_time = current_time('mysql');
-
-		// Get both count and existence in one query
-		$result = $this->wpdb->get_row($this->wpdb->prepare("
-        SELECT
-            COUNT(*) as total,
-            SUM(IF(status IN ('pending', 'processing'), 1, 0)) as active,
-            SUM(IF(status = 'scheduled' AND scheduled_at <= %s, 1, 0)) as ready_scheduled
-        FROM $table
-        WHERE status IN ('pending', 'processing', 'scheduled')
-    ", $current_time));
-
-		$info = [
-			'total' => (int)$result->total,
-			'active' => (int)$result->active,
-			'ready' => (int)$result->active + (int)$result->ready_scheduled,
-			'has_items' => ((int)$result->active + (int)$result->ready_scheduled) > 0
-		];
-
-		$this->cache->set($cacheKey, $info, 30);
-		return $info;
-	}
-	protected function hasItemsInQueue(): bool
-	{
-		return $this->getQueueInfo()['has_items'];
-	}
-
-    /**
-     * Cron job that checks if the queue needs to be processed
-     * @return void
-     */
-	public function checkQueue(): void
-	{
-		if (!$this->hasItemsInQueue()) {
-			return;
-		}
-
-		if ($this->isQueueLocked()) {
-			return;
-		}
-
-
-		// Peek at what operations we might process
-		$batch_size = $this->getAdaptiveBatchSize();
-		$operations = $this->getOperations($batch_size);
-
-		// Only check server load for heavy operations
-		if ($this->shouldCheckServerLoad($operations) && !$this->isServerIdle()) {
-			error_log('Server not idle for heavy operations');
-			return;
-		}
-
-		$this->lockQueue();
-		$this->applyServerSettings();
-		$this->processQueue($operations);
-		$this->unlockQueue();
-	}
-
-	public function hourlyMaintenance(): void
-	{
-		if ($this->isQueueLocked()) {
-			return; // Don't run maintenance while processing
-		}
-
-		$this->cleanupStuckOperations();
-		$this->restartStuckOperations();
-//		$this->pruneOldMetrics(); // Optional: clean old metrics
-	}
-
-	protected function isHeavyOperation(string $type): bool
-	{
-		$heavyOperations = [
-			'file_upload' => true,
-			'image_processing' => true,
-			'rebuild_user_term_index' => true,
-			'sync_content_taxonomy_tables' => true,
-			'email_notification_digest' => true,
-			'taxonomy_relationships' => true,
-		];
-
-		return isset($heavyOperations[$type]);
-	}
-
-	protected function shouldCheckServerLoad(array $operations): bool
-	{
-		// Check if any operations are heavy
-		foreach ($operations as $op) {
-			if ($this->isHeavyOperation($op->type)) {
-				return true;
-			}
-		}
-		return false;
-	}
-
-    /**
-     * Check if the queue is currently locked for processing
-     * @return bool
-     */
-	protected function isQueueLocked(): bool
-	{
-		$lock_name = BASE . '_queue_lock';
-
-		// Check if lock exists and is recent
-		$result = $this->wpdb->get_var($this->wpdb->prepare(
-			"SELECT IS_USED_LOCK(%s)",
-			$lock_name
-		));
-
-		return $result !== null;
-	}
-
-    /**
-     * Check if server is relatively idle (very low load)
-     * @return bool
-     */
-    private function isServerIdle():bool
-    {
-        // Consider server idle if load is below 50% of our threshold
-        if (function_exists('sys_getloadavg')) {
-            $load = sys_getloadavg();
-            if ($load[0] > ($this->load_thresholds['load_avg'] * 0.5)) {
-                return false;
-            }
-        }
-
-        // Consider memory idle if usage is below 60% of our threshold
-        $memory_usage = $this->getMemoryUsage();
-        if ($memory_usage > ($this->load_thresholds['memory'] * 0.6)) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Acquire a lock on the queue
-     * @return bool
-     */
-	protected function lockQueue(): bool
-	{
-		$lock_name = BASE . '_queue_lock';
-
-		// Try to acquire lock with 0 timeout (non-blocking)
-		$result = $this->wpdb->get_var($this->wpdb->prepare(
-			"SELECT GET_LOCK(%s, 0)",
-			$lock_name
-		));
-
-		return $result === '1';
-	}
-
-    /**
-     * Release the queue lock
-     * @return void
-     */
-	protected function unlockQueue(): void
-	{
-		$lock_name = BASE . '_queue_lock';
-
-		$this->wpdb->query($this->wpdb->prepare(
-			"SELECT RELEASE_LOCK(%s)",
-			$lock_name
-		));
-	}
-
-
-    /**
-     * @return void
-     */
-    protected function applyServerSettings():void
-    {
-        // Set max execution time for background processes
-        set_time_limit(300); // 5 minutes
-
-        // Optimize garbage collection
-        gc_enable();
-
-        // Set process priority if possible
-        if (function_exists('proc_nice')) {
-            proc_nice(10); // Lower priority for background tasks
-        }
-    }
-
-    /**
-     * @param object $operation
-     *
-     * @return bool
-     */
-	protected function areDependenciesMet(object $operation): bool
-	{
-		if (empty($operation->dependencies)) {
-			return true;
-		}
-
-		// Add timeout check for stuck dependencies
-//		$created_time = strtotime($operation->created_at);
-//		$max_wait = 3600; // 1 hour max wait
-//
-//		if (time() - $created_time > $max_wait) {
-//			error_log("Operation {$operation->id} timed out waiting for dependencies");
-//			$this->markAsPermanentlyFailed($operation->id, 'Dependencies timeout after 1 hour');
-//			return false;
-//		}
-
-		$dependencies = is_string($operation->dependencies)
-			? json_decode($operation->dependencies, true)
-			: $operation->dependencies;
-
-		if (is_string($dependencies)) {
-			$dependencies = explode(',',$dependencies);
-		}
-
-		// Batch check all dependencies at once
-		$statuses = $this->getOperationStatuses($dependencies);
-
-		foreach ($dependencies as $dep_id) {
-			if (!isset($statuses[$dep_id]) || $statuses[$dep_id]->status !== 'completed') {
-				return false;
-			}
-		}
-
-		return true;
-	}
-
-	public function getOperationStatuses(array $operation_ids): array
-	{
-		if (empty($operation_ids)) {
-			return [];
-		}
-
-		$placeholders = implode(',', array_fill(0, count($operation_ids), '%s'));
-		$table = $this->wpdb->prefix . $this->table;
-
-		$results = $this->wpdb->get_results($this->wpdb->prepare(
-			"SELECT id, status FROM $table WHERE id IN ($placeholders)",
-			...$operation_ids
-		), OBJECT_K);
-
-		return $results ?: [];
-	}
-
-	/**
-	 * Queue or update an operation with intelligent batching
-	 *
-	 * @param string $type Operation type
-	 * @param int $user_id User ID
-	 * @param array $data Operation data
-	 * @param array $options  Operation options
-	 *        - operation_id: string,
-	 *        - priority: string, (high, normal, low) Operation priority
-	 *        - delay: int Time window in seconds before processing
-	 * 	      - scheduled: string, as formatted by sanitizeDateTime in MetaSanitizer.php
-	 *        - merge: string, //replace, append, merge
-	 * 		  - chunk_key: string The array key to split large operations by
-	 * 		  - chunk_size: int The batch size to process at once
-	 * @return array|WP_Error
-	 */
-	public function queueOperation(string $type, int $user_id, array $data, array $options = []): array|WP_Error
-	{
-		try {
-			$table = $this->wpdb->prefix . $this->table;
-
-			// Extract options
-			$priority = (array_key_exists('priority', $options) && in_array($options['priority'], ['low', 'normal', 'high'])) ? $options['priority'] : 'normal';
-			$delay = (array_key_exists('delay', $options)) ? (int) $options['delay'] : 0;
-			$scheduledFor = (array_key_exists('scheduled', $options)) ? $options['scheduled'] : '';
-			$merge = (array_key_exists('merge', $options) && in_array($options['merge'], ['merge', 'append', 'replace'])) ? $options['merge'] : 'merge';
-			$operation_id = $options['operation_id'] ?? false;
-			$chunk_key = $options['chunk_key'] ?? null;
-			$chunk_size = $options['chunk_size'] ?? null;
-
-			// Calculate operation count based on chunk_key if provided
-			$count = 1; // Default
-			if ($chunk_key && isset($data[$chunk_key])) {
-				if (is_array($chunk_key)) {
-					// Support multiple keys (like favourites_batch with 'adds' and 'removes')
-					$total = 0;
-					foreach ($chunk_key as $key) {
-						if (isset($data[$key]) && is_array($data[$key])) {
-							$total += count($data[$key]);
-						}
-					}
-					$count = max(1, $total);
-				} else {
-					// Single key
-					$count = max(1, count($data[$chunk_key]));
-				}
-			}
-
-			// Generate operation ID
-			$operation_id = $this->checkOperationId($operation_id, $type, $merge, $user_id);
-
-			// Check for existing operation
-			$existing = $this->getOperation($operation_id);
-
-			$dependencies = $options['depends_on'] ?? [];
-			if (is_string($dependencies)) {
-				$dependencies = explode(',',$dependencies);
-			}
-
-			$scheduled_at = $this->calculateScheduledTime($delay === 0 ? $scheduledFor : $delay);
-
-			// Prepare chunk config to store
-			$chunk_config = null;
-			if ($chunk_key && $chunk_size) {
-				$chunk_config = [
-					'key' => $chunk_key,
-					'size' => $chunk_size
-				];
-			}
-
-			if ($existing) {
-
-				if ($merge !== 'append' && ($existing->status === 'pending' || $existing->status === 'scheduled')) {
-
-					if ($merge === 'merge') {
-						$existing_data = json_decode($existing->request_data ?? '{}', true);
-						$data = $this->deepMerge($existing_data, $data);
-
-						$existing_dependencies = json_decode($existing->dependencies ?? '[]', true);
-						if (!is_array($existing_dependencies)) {
-							$existing_dependencies = [];
-						}
-						$dependencies = array_unique(array_merge($existing_dependencies, $dependencies));
-					}
-
-					// Keep earliest scheduled time
-					if ($delay > 0 || $scheduledFor !== '') {
-						$scheduled_at = $this->getEarliestScheduledTime(
-							$existing->scheduled_at ?? $existing->created_at,
-							$scheduled_at
-						);
-					}
-
-					// Store chunking config in metadata
-					$metadata = json_decode($existing->metadata ?? '{}', true);
-					if ($chunk_config) {
-						$metadata['chunk_config'] = $chunk_config;
-					}
-
-					$result = $this->wpdb->update(
-						$table,
-						[
-							'request_data' => json_encode($data),
-							'dependencies'	=> json_encode($dependencies),
-							'merge'	=> $merge,
-							'count' => $count,
-							'priority' => $priority,
-							'scheduled_at' => $scheduled_at,
-							'metadata' => json_encode($metadata),
-							'updated_at' => current_time('mysql'),
-						],
-						['id' => $operation_id],
-						['%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s'],
-						['%s']
-					);
-
-					if ($result === false) {
-						throw new Exception("Failed to update operation: " . $this->wpdb->last_error);
-					}
-
-				} else if ($merge === 'append') {
-					$operation_id = $operation_id . '_' . time() . '_' . substr(uniqid(), -4);
-					$existing = null;
-				}
-			}
-
-			if (!$existing) {
-				// Prepare metadata with chunk config
-				$metadata = $chunk_config ? ['chunk_config' => $chunk_config] : [];
-
-				$result = $this->wpdb->insert(
-					$table,
-					[
-						'id' => $operation_id,
-						'type' => $type,
-						'user_id' => $user_id,
-						'request_data' => json_encode($data),
-						'count' => $count,
-						'priority' => $priority,
-						'status' => $delay > 0 ? 'scheduled' : 'pending',
-						'dependencies' => json_encode($dependencies),
-						'metadata' => json_encode($metadata),
-						'merge'	=> $merge,
-						'scheduled_at' => $scheduled_at,
-						'created_at' => current_time('mysql'),
-						'updated_at' => current_time('mysql'),
-					],
-					['%s', '%s', '%d', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s']
-				);
-
-				if ($result === false) {
-					throw new Exception("Failed to insert operation: " . $this->wpdb->last_error);
-				}
-			}
-
-			$this->invalidateUserQueue($user_id);
-			$this->runQueueOnShutdown();
-
-			return [
-				'success' => true,
-				'operation_id' => $operation_id,
-				'updated_existing' => !empty($existing),
-				'scheduled_at' => $scheduled_at,
-				'queue_status' => $this->getQueueStatus()
-			];
-
-		} catch (Exception $e) {
-			Errors::getInstance()->log('queue', $e->getMessage(), $data, 'high');
-			return new WP_Error('queue_failed', $e->getMessage());
-		}
-	}
-
-	protected function deepMerge(array $existing, array $new): array
-	{
-		$merged = $existing;
-
-		if (!$this->isAssociativeArray($existing) && !$this->isAssociativeArray($new)) {
-			return array_merge($existing, $new);
-		}
-		foreach ($new as $key => $newValue) {
-			if (!array_key_exists($key, $existing)) {
-				$merged[$key] = $newValue;
-			} else {
-				$existingValue = $existing[$key];
-				if (is_array($existingValue) && is_array($newValue)) {
-					if ($this->isAssociativeArray($existingValue) || $this->isAssociativeArray($newValue)) {
-						// Recursive merge going deeper, if any of them are associative arrays
-						$merged[$key] = $this->deepMerge($existingValue, $newValue);
-					} else {
-						$containsComplex = $this->containsComplexData($existingValue) || $this->containsComplexData($newValue);
-
-						if ($containsComplex) {
-							// Just merge and re-index - preserves all items from chunks
-							$merged[$key] = array_values(array_merge($existingValue, $newValue));
-						} else {
-							// Simple scalar arrays - use unique merge
-							$merged[$key] = array_unique(array_merge($existingValue, $newValue), SORT_REGULAR);
-						}
-					}
-				} elseif (is_array($existingValue) && !is_array($newValue)) {
-					// The existing value is an array, but the new one isn't
-					// Check if it's safe to use in_array (only if existing doesn't contain arrays)
-					$containsArrays = false;
-					foreach ($existingValue as $item) {
-						if (is_array($item) || is_object($item)) {
-							$containsArrays = true;
-							break;
-						}
-					}
-
-					if (!$containsArrays) {
-						// Safe to use in_array with strict comparison
-						if (!in_array($newValue, $existingValue, true)) {
-							$merged[$key][] = $newValue;
-						}
-					} else {
-						// For arrays containing complex types, just append
-						// (avoiding comparison that might cause issues)
-						$merged[$key][] = $newValue;
-					}
-				} elseif (!is_array($existingValue) && is_array($newValue)) {
-					// The opposite check as above
-					$merged[$key] = array_unique(array_merge([$existingValue], $newValue), SORT_REGULAR);
-				} else {
-					// Override the existing with the new, both are scalars
-					$merged[$key] = $newValue;
-				}
-			}
-		}
-		return $merged;
-	}
-
-	/**
-	 * Check if an array is associative
-	 * @param array $arr
-	 * @return bool
-	 */
-	protected function isAssociativeArray(array $arr): bool
-	{
-		if (empty($arr)) {
-			return false;
-		}
-		return array_keys($arr) !== range(0, count($arr) - 1);
-	}
-
-	/**
-	 * Check if an array contains complex data (arrays or objects)
-	 * @param array $arr
-	 * @return bool
-	 */
-	protected function containsComplexData(array $arr): bool
-	{
-		foreach ($arr as $item) {
-			if (is_array($item) || is_object($item)) {
-				return true;
-			}
-		}
-		return false;
-	}
-
-	protected function getEarliestScheduledTime(string $existing, string $new): string
-	{
-		$existing_time = strtotime($existing);
-		$new_time = strtotime($new);
-		return $existing_time < $new_time ? $existing : $new;
-	}
-
-	protected function calculateScheduledTime(int|string $delay): string
-	{
-		if (is_int($delay) && $delay > 0) {
-			return date('Y-m-d H:i:s', current_time('timestamp') + $delay);
-		}
-		if ($delay !== '') {
-			return strtotime($delay);
-		}
-		return current_time('mysql');
-	}
-
-	protected function checkOperationId(?string $operation_id, string $type, string $merge, int $user_id): string
-	{
-		if ($merge === 'merge') {
-			$existing = $this->getUserOperations(
-				$user_id,
-				[
-					'status' => 'pending',
-					'type'	 => $type,
-					'merge'	 => $merge,
-					'limit' => 1
-				]);
-			if ($existing) {
-				$operation_id = $existing[0]->id;
-			}
-		}
-
-		if (!$operation_id) {
-			$operation_id = uniqid('op_');
-		}
-
-		if (!str_starts_with($operation_id, 'u')) {
-			$operation_id = 'u' . $user_id . '_' . $operation_id;
-		}
-
-		return $operation_id;
-	}
-
-
-	protected function runQueueOnShutdown(): void
-	{
-		// Only add if not already scheduled AND if operation is high priority or small
-		if (!has_action('shutdown', [$this, 'processQueueOnShutdown'])) {
-			add_action('shutdown', [$this, 'processQueueOnShutdown'], 100);
-		}
-	}
-
-    /**
-     * Process queue after request is complete
-     */
-    public function processQueueOnShutdown(): void
-    {
-        // Remove so it only runs once per request
-        remove_action('shutdown', [$this, 'processQueueOnShutdown']);
-
-        // Send response to user first if using PHP-FPM
-        if (function_exists('fastcgi_finish_request')) {
-            fastcgi_finish_request();
-        }
-
-        // Now process the queue
-        $this->checkQueue();
-    }
-
-	/**
-	 * @param string $operation_id
-	 * @return object|false
-	 */
-	public function getOperation(string $operation_id, bool $skipCache = false): object|false
-	{
-		try {
-			$table = $this->wpdb->prefix . $this->table;
-
-			$operation = $this->wpdb->get_row($this->wpdb->prepare(
-				"SELECT * FROM $table WHERE id = %s",
-				$operation_id
-			));
-
-			return $operation ?: false;
-		} catch (Exception) {
-			return false;
-		}
-	}
-
-    /**
-     * Process queue with adaptive batch sizing
-     * @return void
-     */
-	public function processQueue(array $operations = []): void
-	{
-		try {
-			// If no operations passed, fetch them
-			if (empty($operations)) {
-				$batch_size = $this->getAdaptiveBatchSize();
-				$operations = $this->getOperations($batch_size);
-			}
-
-			// Pre-warm cache for dependencies check
-			$this->prewarmDependencyCache($operations);
-
-			foreach ($operations as $operation) {
-				if (!$this->canProcessOperation($operation)) {
-					continue;
-				}
-
-				if (!$this->areDependenciesMet($operation)) {
-					continue;
-				}
-
-				$this->processOperation($operation);
-
-				// Invalidate operation cache after processing
-				$this->cache->forget(self::CACHE_OPERATION_PREFIX . $operation->id);
-				$this->cache->forget(self::CACHE_USER_QUEUE_PREFIX . $operation->user_id);
-			}
-
-			// Batch invalidate caches at the end
-			$this->invalidateQueueCache();
-			$this->runQueueOnShutdown();
-
-		} catch (Exception $e) {
-			error_log('Error processing queue: ' . $e->getMessage());
-		}
-	}
-
-	public function cancelOperation(string $operation_id): bool
-	{
-		$table = $this->wpdb->prefix . $this->table;
-
-		// Only cancel if pending or scheduled
-		$result = $this->wpdb->update(
-			$table,
-			[
-				'status' => 'cancelled',
-				'completed_at' => current_time('mysql'),
-				'updated_at' => current_time('mysql'),
-				'error_message' => 'Cancelled by user'
-			],
-			[
-				'id' => $operation_id,
-				'status' => ['pending', 'scheduled']
-			]
-		);
-
-		if ($result) {
-			$this->invalidateQueueCache('status');
-			return true;
-		}
-
-		return false;
-	}
-
-	/**
-	 * Pre-warm cache for dependency checks
-	 */
-	protected function prewarmDependencyCache(array $operations): void
-	{
-		$allDependencyIds = [];
-		foreach ($operations as $op) {
-			$deps = json_decode($op->dependencies ?? '[]', true);
-			if (!is_array($deps)) {
-				$deps = [$deps];
-			}
-			$allDependencyIds = array_merge($allDependencyIds, $deps);
-		}
-
-		if (empty($allDependencyIds)) {
-			return;
-		}
-
-		// Get all statuses at once
-		$statuses = $this->getOperationStatuses(array_unique($allDependencyIds));
-
-		// Cache them individually
-		foreach ($statuses as $id => $status) {
-			$this->cache->set(self::CACHE_OPERATION_PREFIX . $id, $status, 300);
-		}
-	}
-
-	/**
-	 * Get operations with optimized query and caching
-	 */
-	protected function getOperations(int $batch_size): array
-	{
-		$table = $this->wpdb->prefix . $this->table;
-
-
-		$current_time = current_time('mysql');
-		$operations = $this->wpdb->get_results($this->wpdb->prepare("
-        SELECT * FROM $table
-        WHERE (
-            status = 'pending' OR
-            (status = 'scheduled' AND scheduled_at <= %s)
-        )
-        AND retries < %d
-        ORDER BY
-            FIELD(priority, 'high', 'normal', 'low'),
-            COALESCE(scheduled_at, created_at)
-        LIMIT %d
-    ", $current_time, $this->max_attempts, $batch_size));
-
-		return $operations ?: [];
-	}
-
-	protected function canProcessOperation(object $operation): bool
-	{
-		if ($operation->retries >= $this->max_attempts) {
-			$this->markAsPermanentlyFailed($operation->id, 'Exceeded maximum retry attempts');
-			return false;
-		}
-		return true;
-	}
-
-	protected function markAsPermanentlyFailed(string $operation_id, string $reason): void
-	{
-
-		$table = $this->wpdb->prefix . $this->table;
-
-		// Get the current operation to preserve original error
-		$operation = $this->wpdb->get_row($this->wpdb->prepare(
-			"SELECT error_message FROM $table WHERE id = %s",
-			$operation_id
-		));
-
-		// Preserve original error message, append cleanup reason
-		$final_error_message = $reason;
-		if (!empty($operation->error_message)) {
-			$final_error_message = $operation->error_message . " | " . $reason;
-		}
-
-		$result = $this->wpdb->update($table, [
-			'status' => 'failed',
-			'error_message' => $final_error_message,  // ✅ Preserves original error
-			'completed_at' => current_time('mysql'),
-			'updated_at' => current_time('mysql')
-		], ['id' => $operation_id]);
-
-		if ($result) {
-			$this->invalidateQueueCache('status');
-		}
-	}
-
-	/**
-	 * Get operations for a specific user with caching
-	 */
-	public function getUserOperations(int $user_id, array $filters = []): array
-	{
-		$cacheKey = self::CACHE_USER_QUEUE_PREFIX . $user_id . '_' . md5(serialize($filters));
-//
-//		// Check cache first
-//		$cached = $this->cache->get($cacheKey);
-//		if ($cached !== false) {
-//			return $cached;
-//		}
-
-
-		$table = $this->wpdb->prefix . $this->table;
-
-		$where = ['user_id = %d'];
-		$params = [$user_id];
-
-		if (!empty($filters['status'])) {
-			$where[] = 'status = %s';
-			$params[] = $filters['status'];
-		}
-
-		if (!empty($filters['type'])) {
-			$where[] = 'type = %s';
-			$params[] = $filters['type'];
-		}
-
-		if (!empty($filters['merge'])) {
-			$where[] = 'merge = %s';
-			$params[] = $filters['merge'];
-		}
-
-		$limit = $filters['limit'] ?? 50;
-
-		$query = "SELECT * FROM $table WHERE " . implode(' AND ', $where) .
-			" ORDER BY created_at DESC LIMIT %d";
-		$params[] = $limit;
-
-		$operations = $this->wpdb->get_results($this->wpdb->prepare($query, ...$params));
-
-		// Cache user operations
-		$this->cache->set($cacheKey, $operations);
-
-		return $operations ?: [];
-	}
-
-	public function getUserOperationStatuses(int $user_id): array
-	{
-		// Get user's operation IDs
-		$table = $this->wpdb->prefix . $this->table;
-		$operation_ids = $this->wpdb->get_col($this->wpdb->prepare(
-			"SELECT id FROM $table WHERE user_id = %d ORDER BY created_at DESC LIMIT 20",
-			$user_id
-		));
-
-		return $this->getOperationStatuses($operation_ids);
-	}
-
-	/**
-	 * Check if user's queue has been modified since given timestamp
-	 * Returns true if modified, false if not (can send 304)
-	 */
-	public function isUserQueueModified(int $user_id, int $since_timestamp): bool
-	{
-		return $this->cache::lastModified("user_{$user_id}") > $since_timestamp;
-	}
-	protected function invalidateUserQueue(int $user_id): void
-	{
-		// This automatically:
-		// 1. Updates HTTP timestamp for user_{$user_id}
-		// 2. Flushes user-specific caches
-		// 3. Triggers connected cache invalidation
-		Cache::for($user_id)->flush();
-	}
-
-	/**
-	 * Invalidate all queue-related caches
-	 */
-	protected function invalidateQueueCache(string $scope = 'all'): void
-	{
-		$cacheKeys = [
-			'status' => [self::CACHE_QUEUE_STATUS, self::CACHE_QUEUE_SIZE],
-			'items' => [self::CACHE_HAS_ITEMS, 'queue_info'],
-			'all' => [
-				self::CACHE_QUEUE_STATUS,
-				self::CACHE_HAS_ITEMS,
-				self::CACHE_QUEUE_SIZE,
-				'queue_info'
-			]
-		];
-
-		$keys = $cacheKeys[$scope] ?? $cacheKeys['all'];
-
-		foreach ($keys as $key) {
-			$this->cache->forget($key);
-		}
-
-		$this->cache->touch();
-
-		if ($scope === 'all') {
-			delete_transient('jvb_queue_status_counts');
-		}
-	}
-
-    /**
-     * @return void
-     */
-    protected function restartStuckOperations(): void
-    {
-
-        $table = $this->wpdb->prefix . $this->table;
-
-        // Find operations that have been processing for too long
-        $stuck_operations = $this->wpdb->get_results($this->wpdb->prepare("
-        SELECT * FROM $table
-        WHERE status = 'processing'
-        AND started_at < %s
-        LIMIT 5",
-            date('Y-m-d H:i:s', strtotime('-30 minutes'))
-        ));
-
-        foreach ($stuck_operations as $operation) {
-            $retries = (int) $operation->retries + 1 ?? 1;
-
-            if ($retries >= $this->max_attempts) {
-                // Too many retries, mark as permanently failed
-                $this->wpdb->update(
-                    $table,
-                    [
-                        'status'        => 'failed_permanent',
-                        'error_message' => 'Operation exceeded maximum retry attempts',
-                        'updated_at'    => current_time('mysql'),
-                        'completed_at'  => current_time('mysql')
-                    ],
-                    [ 'id' => $operation->id ]
-                );
-
-                // Log this as a critical error
-                Errors::getInstance()->log(
-                    '[OperationQueue]:restartStuckOperations',
-                    "Operation failed after maximum retries",
-                    [
-                        'operation_id'   => $operation->id,
-                        'type' => $operation->type,
-                        'user_id'        => $operation->user_id,
-                        'retries'        => $retries,
-                        'data'           => $operation->request_data
-                    ],
-                    'critical'
-                );
-
-                $this->notifyAdmin($operation);
-            } else {
-                // Reset to pending for another attempt with backoff
-                $this->wpdb->update(
-                    $table,
-                    [
-                        'status'        => 'pending',
-                        'updated_at'    => current_time('mysql'),
-                        'started_at'    => null,
-                        'retries'       => $retries,
-                    ],
-                    [ 'id' => $operation->id ]
-                );
-
-                // Log the backoff
-                Errors::getInstance()->log(
-                    '[OperationQueue]:restartStuckOperations',
-                    "Operation requeued with exponential backoff",
-                    [
-                        'operation_id'    => $operation->id,
-                        'retry_number'    => $retries,
-                    ],
-                    'warning'
-                );
-            }
-        }
-    }
-
-	/**
-	 * Implement intelligent retry delays with exponential backoff
-	 */
-	public function scheduleRetry(string $operation_id, int $attempt): void
-	{
-		// Calculate delay with exponential backoff and jitter
-		$base_delay = 5; // 5 seconds
-		$max_delay = 3600; // 1 hour max
-
-		$delay = min($base_delay * pow(2, $attempt), $max_delay);
-
-		// Add jitter to prevent thundering herd
-		$jitter = rand(0, $delay * 0.1);
-		$delay += $jitter;
-
-		$scheduled_at = date('Y-m-d H:i:s', time() + $delay);
-
-
-		$table = $this->wpdb->prefix . $this->table;
-
-		$this->wpdb->update(
-			$table,
-			[
-				'status' => 'scheduled',
-				'scheduled_at' => $scheduled_at,
-				'retries' => $attempt
-			],
-			['id' => $operation_id],
-			['%s', '%s', '%d'],
-			['%s']
-		);
-
-	}
-
-    /**
-     * Notify admin about repeatedly failed operation
-     *
-     * @param object $operation The failed operation
-     *
-     * @return bool
-     */
-    protected function notifyAdmin(object $operation):bool
-    {
-        $admin_email = get_option('admin_email');
-        $site_name   = get_bloginfo('name');
-
-        $subject = "[$site_name] Queue Operation Failed After Multiple Attempts";
-
-        $message = "A queue operation has failed after multiple retry attempts:\n\n";
-        $message .= "Operation ID: $operation->id\n";
-        $message .= "Operation Type: $operation->type\n";
-        $message .= "User ID: $operation->user_id\n";
-        $message .= "Created At: $operation->created_at\n";
-        $message .= "Retries: $operation->retries\n\n";
-
-        if (!empty($operation->error_message)) {
-            $message .= "Error Message: $operation->error_message\n\n";
-        }
-
-        $message .= "Please check the error logs for more details.";
-
-        return Emails::getInstance()->sendEmail($admin_email, $subject, $message);
-    }
-
-    /**
-     * @param object $operation
-     *
-     * @return array
-     */
-	protected function processOperation(object $operation): array
-	{
-		try {
-			$table = $this->wpdb->prefix . $this->table;
-
-			// Mark as processing...
-			$updated = $this->wpdb->query($this->wpdb->prepare(
-				"UPDATE $table
-             SET status = 'processing',
-                 started_at = %s,
-                 updated_at = %s
-             WHERE id = %s
-             AND status IN ('pending', 'scheduled')
-             AND (scheduled_at IS NULL OR scheduled_at <= %s)",
-				current_time('mysql'),
-				current_time('mysql'),
-				$operation->id,
-				current_time('mysql')
-			));
-
-			if (!$updated) {
-				throw new Exception('Operation no longer available for processing');
-			}
-			$this->invalidateUserQueue($operation->user_id);
-
-			$data = json_decode($operation->request_data, true);
-			$progress_count = (int) $operation->progress_count;
-			$count = (int) $operation->count;
-			$failed_items = json_decode($operation->failed_items ?: '[]', true);
-
-			// Process chunk
-			$chunk = $this->getNextChunk($operation);
-
-			// Apply filter - will return ['success' => bool, 'result' => mixed]
-			$filterResult = apply_filters(
-				BASE.'handle_bulk_operation',
-				['success' => false, 'result' => 'No handler for operation item'], // Default if no filter
-				$operation,
-				$chunk
-			);
-
-			// Ensure we have the expected format
-			if (!is_array($filterResult) || !isset($filterResult['success'])) {
-				// Log invalid filter response and quit early
-				$invalid_response = is_object($filterResult) ? get_class($filterResult) : gettype($filterResult);
-				Errors::getInstance()->log(
-					'[OperationQueue]:processOperation',
-					"Invalid filter response format - expected array with 'success' key",
-					[
-						'operation_id' => $operation->id,
-						'operation_type' => $operation->type,
-						'filter_response_type' => $invalid_response,
-						'filter_response' => $filterResult
-					],
-					'error'
-				);
-
-				// Mark operation as failed
-				$this->wpdb->update(
-					$table,
-					[
-						'status' => 'failed',
-						'error_message' => "Invalid filter response: expected array with 'success' key, got {$invalid_response}",
-						'result' => json_encode([
-							'success' => false,
-							'error' => "Invalid filter response format"
-						]),
-						'updated_at' => current_time('mysql'),
-						'completed_at' => current_time('mysql')
-					],
-					['id' => $operation->id]
-				);
-
-				$this->invalidateQueueCache('status');
-
-				return [
-					'success' => false,
-					'result' => "Invalid filter response format"
-				];
-			}
-
-			// Additional validation for WP_Error handling
-			if (is_wp_error($filterResult)) {
-				$filterResult = [
-					'success' => false,
-					'result' => $filterResult->get_error_message()
-				];
-			}
-
-			$newCount = $progress_count + $chunk['progress'];
-
-			if ($filterResult['success']) {
-				// Success path
-				$oldResult = json_decode($operation->result ?: '{}', true);
-				if (!is_array($oldResult)) {
-					$oldResult = [];
-				}
-
-				if (!is_array($filterResult['result'])) {
-					$filterResult['result'] = [$filterResult['result']];
-				}
-				// Store the result data
-				$resultToStore = $this->deepMerge($oldResult, $filterResult['result']);
-				$resultToStore['processed_at'] = current_time('mysql');
-
-				// Check if operation is complete
-				if ($newCount >= $count) {
-					$status = empty($failed_items) ? 'completed' : 'completed_with_errors';
-
-					$result = $this->wpdb->update(
-						$table,
-						[
-							'progress_count' => $newCount,
-							'result' => json_encode($resultToStore),
-							'status' => $status,
-							'completed_at' => current_time('mysql'),
-							'updated_at' => current_time('mysql')
-						],
-						['id' => $operation->id],
-						['%d', '%s', '%s', '%s', '%s'],
-						['%s']
-					);
-
-					// Now do post-completion tasks
-					$this->invalidateUserQueue($operation->user_id);
-
-					$this->trackOperationMetrics($operation->id);
-
-				} else {
-					// More work to do - just update progress and result
-					$this->wpdb->update(
-						$table,
-						[
-							'progress_count' => $newCount,
-							'result' => json_encode($resultToStore),
-							'status' => 'pending',  // Back to pending for next chunk
-							'updated_at' => current_time('mysql')
-						],
-						['id' => $operation->id],
-						['%d', '%s', '%s', '%s'],
-						['%s']
-					);
-
-					$this->invalidateUserQueue($operation->user_id);
-				}
-
-			} else {
-				// Error path
-				$error_message = is_string($filterResult['result'])
-					? $filterResult['result']
-					: json_encode($filterResult['result']);
-
-				$error_context = [
-					'index' => $progress_count,
-					'error' => $error_message,
-					'timestamp' => current_time('mysql'),
-					'retry_count' => $operation->retries,
-					'type' => $operation->type
-				];
-
-				$failed_items[] = $error_context;
-
-				if ($newCount >= $count) {
-					// Failed but complete
-					$this->wpdb->update(
-						$table,
-						[
-							'failed_items' => json_encode($failed_items),
-							'progress_count' => $newCount,
-							'result' => json_encode([
-								'success' => false,
-								'error' => $error_message,
-								'failed_items' => $failed_items
-							]),
-							'status' => 'failed',
-							'error_message' => $error_message,
-							'completed_at' => current_time('mysql'),
-							'updated_at' => current_time('mysql')
-						],
-						['id' => $operation->id],
-						['%s', '%d', '%s', '%s', '%s', '%s', '%s'],
-						['%s']
-					);
-
-					$this->invalidateUserQueue($operation->user_id);
-				} else {
-					// Failed but more to process - continue with next chunk
-					$this->wpdb->update(
-						$table,
-						[
-							'failed_items' => json_encode($failed_items),
-							'progress_count' => $newCount,
-							'status' => 'pending',
-							'error_message' => $error_message,
-							'updated_at' => current_time('mysql')
-						],
-						['id' => $operation->id],
-						['%s', '%d', '%s', '%s', '%s'],
-						['%s']
-					);
-
-				}
-			}
-			// Clear operation cache after any update
-			$this->invalidateUserQueue($operation->user_id);
-			return $filterResult;
-
-		} catch (Exception $e) {
-			// Handle exception...
-			Errors::getInstance()->log(
-				'[OperationQueue]:processOperation',
-				"Exception during operation processing: " . $e->getMessage(),
-				[
-					'operation_id' => $operation->id,
-					'type' => $operation->type,
-					'progress_count' => $progress_count ?? 0,
-					'user_id' => $operation->user_id,
-					'file' => $e->getFile(),
-					'line' => $e->getLine(),
-					'trace' => $e->getTraceAsString()
-				],
-				'error'
-			);
-
-			// Mark operation as failed
-			$this->wpdb->update(
-				$table,
-				[
-					'status' => 'failed',
-					'error_message' => $e->getMessage(),
-					'result' => json_encode([
-						'success' => false,
-						'error' => $e->getMessage()
-					]),
-					'updated_at' => current_time('mysql'),
-					'completed_at' => current_time('mysql')
-				],
-				['id' => $operation->id]
-			);
-
-			$this->invalidateQueueCache('status');
-
-
-			return [
-				'success' => false,
-				'result' => $e->getMessage()
-			];
-		}
-	}
-
-    /**
-     * Extract X amount of items from the operation data to process, based on complexity
-     *
-     * @param object $operation The current operation
-     *
-     * @return array false if not batched, the extracted data to process if batchable
-     */
-	protected function getNextChunk(object $operation): array
-	{
-		$data = json_decode($operation->request_data, true);
-
-		// Get chunk config from metadata
-		$metadata = json_decode($operation->metadata ?? '{}', true);
-		$chunk_config = $metadata['chunk_config'] ?? null;
-
-		// If no chunk config, return all data with progress of 1
-		if (!$chunk_config) {
-			return array_merge($data, ['progress' => 1]);
-		}
-
-		$chunk_key = $chunk_config['key'];
-		$chunk_size = $chunk_config['size'];
-		$current_progress = (int)$operation->progress_count;
-
-		// Handle single or multiple keys uniformly
-		$keys = (array)$chunk_key;
-		$batch = $this->extractBatch($data, $keys, $current_progress, $chunk_size);
-
-		// Merge non-chunked data with chunked batch
-		$result = array_filter($data, function ($k) use ($keys) {
-			return !in_array($k, $keys);
-		}, ARRAY_FILTER_USE_KEY);
-
-		// Add the batched data
-		foreach ($batch as $k => $v) {
-			$result[$k] = $v;
-		}
-
-		return $result;
-	}
-
-	protected function extractBatch(array $data, array $keys, int $currentProgress, int $batchSize): array
-	{
-		$result = ['progress' => 0];
-		$itemsExtracted = 0;
-
-		// Calculate total items across all keys
-		$totalItems = 0;
-		$keyData = [];
-
-		foreach ($keys as $key) {
-			if (isset($data[$key]) && is_array($data[$key])) {
-				$keyData[$key] = $data[$key];
-				$totalItems += count($data[$key]);
-			}
-		}
-
-		// If current progress >= total, we're done
-		if ($currentProgress >= $totalItems) {
-			return ['progress' => 0];
-		}
-
-		// Extract items starting from currentProgress
-		$globalIndex = 0;
-
-		foreach ($keyData as $key => $items) {
-			$keyItemCount = count($items);
-
-			// Skip this key if we haven't reached it yet
-			if ($globalIndex + $keyItemCount <= $currentProgress) {
-				$globalIndex += $keyItemCount;
-				continue;
-			}
-
-			// Calculate start position within this key
-			$startInKey = max(0, $currentProgress - $globalIndex);
-			$remainingInBatch = $batchSize - $itemsExtracted;
-
-			// Extract what we can from this key
-			$extracted = array_slice($items, $startInKey, $remainingInBatch, true);
-
-			if (!empty($extracted)) {
-				$result[$key] = $extracted;
-				$itemsExtracted += count($extracted);
-				$result['progress'] += count($extracted);
-			}
-
-			// Stop if we've filled the batch
-			if ($itemsExtracted >= $batchSize) {
-				break;
-			}
-
-			$globalIndex += $keyItemCount;
-		}
-
-		return $result;
-	}
-
-
-    protected function updateUserQueueTimestamp(int $user_id)
-    {
-
-		Cache::touch("user_{$user_id}");
-    }
-
-	/**
-	 * Track operation metrics with proper success checking
-	 * @param string $operationId
-	 * @return void
-	 */
-	protected function trackOperationMetrics(string $operationId): void
-	{
-		$metrics_table = $this->wpdb->prefix . $this->metricsTable;
-		$operation = $this->getOperation($operationId, true);
-		$today = date('Y-m-d');
-
-		// Calculate duration if available
-		$duration = null;
-		if (!empty($operation->started_at) && !empty($operation->completed_at)) {
-			$started = strtotime($operation->started_at);
-			$completed = strtotime($operation->completed_at);
-			$duration = $completed - $started;
-		}
-
-		// Determine success status from the result JSON
-		$success = false;
-		if (!empty($operation->result)) {
-			$result = json_decode($operation->result, true);
-			if (is_array($result) && isset($result['success'])) {
-				$success = (bool) $result['success'];
-			} else {
-				// Fallback: check status field
-				$success = ($operation->status === 'completed' || $operation->status === 'completed_with_errors');
-			}
-		}
-
-		// Get or create today's record for this operation type
-		$existing = $this->wpdb->get_row($this->wpdb->prepare(
-			"SELECT * FROM $metrics_table
-         WHERE date = %s AND type = %s",
-			$today,
-			$operation->type
-		));
-
-		if ($existing) {
-			// Update existing record
-			$this->wpdb->update(
-				$metrics_table,
-				[
-					'total_operations' => $existing->total_operations + 1,
-					'successful_operations' => $existing->successful_operations + ($success ? 1 : 0),
-					'failed_operations' => $existing->failed_operations + ($success ? 0 : 1),
-					'average_duration' => $duration ?
-						(($existing->average_duration * $existing->total_operations) + $duration) /
-						($existing->total_operations + 1) :
-						$existing->average_duration,
-					'total_items_processed' => $existing->total_items_processed + $operation->count,
-					'peak_queue_size' => max($existing->peak_queue_size, $this->getCurrentQueueSize()),
-					'peak_memory_usage' => max($existing->peak_memory_usage, memory_get_peak_usage(true)),
-					'peak_cpu_usage' => function_exists('sys_getloadavg') ?
-						max($existing->peak_cpu_usage, sys_getloadavg()[0]) :
-						$existing->peak_cpu_usage
-				],
-				['id' => $existing->id]
-			);
-		} else {
-			// Create new record
-			$this->wpdb->insert(
-				$metrics_table,
-				[
-					'date' => $today,
-					'type' => $operation->type,
-					'total_operations' => 1,
-					'successful_operations' => $success ? 1 : 0,
-					'failed_operations' => $success ? 0 : 1,
-					'average_duration' => $duration,
-					'total_items_processed' => $operation->count,
-					'peak_queue_size' => $this->getCurrentQueueSize(),
-					'peak_memory_usage' => memory_get_peak_usage(true),
-					'peak_cpu_usage' => function_exists('sys_getloadavg') ? sys_getloadavg()[0] : null
-				]
-			);
-		}
-	}
-
-	/**
-	 * Helper method to check if an operation was successful
-	 * @param object|array $operation Operation object or decoded result array
-	 * @return bool
-	 */
-	public function isOperationSuccessful($operation): bool
-	{
-		if (is_object($operation)) {
-			// Check the result field
-			if (!empty($operation->result)) {
-				$result = json_decode($operation->result, true);
-				if (is_array($result) && isset($result['success'])) {
-					return (bool) $result['success'];
-				}
-			}
-			// Fallback to status field
-			return in_array($operation->status, ['completed', 'completed_with_errors']);
-		}
-
-		if (is_array($operation)) {
-			// Direct result array
-			return isset($operation['success']) && $operation['success'];
-		}
-
-		return false;
-	}
-
-    /**
-     * Get current size of queue
-     * @return int
-     */
-	/**
-	 * Get current queue size with caching
-	 */
-	protected function getCurrentQueueSize(): int
-	{
-		return $this->getQueueInfo()['total'];
-	}
-
-    /**
-     * Send daily metrics report to admin
-     * @return void
-     */
-	public function emailDailyMetricsReport():void
-	{
-		$metrics_table = $this->wpdb->prefix . $this->metricsTable;
-		$yesterday = date('Y-m-d', strtotime('-1 day'));
-
-		$metrics = $this->wpdb->get_results($this->wpdb->prepare(
-			"SELECT * FROM $metrics_table WHERE date = %s",
-			$yesterday
-		));
-
-		if (empty($metrics)) {
-			return;
-		}
-
-		$admin_email = get_option('admin_email');
-		$site_name = get_bloginfo('name');
-		$subject = "[$site_name] Daily Queue Performance - " . $yesterday;
-
-		// Calculate totals
-		$total_ops = 0;
-		$total_success = 0;
-		$total_failed = 0;
-		$total_items = 0;
-
-		foreach ($metrics as $metric) {
-			$total_ops += $metric->total_operations;
-			$total_success += $metric->successful_operations;
-			$total_failed += $metric->failed_operations;
-			$total_items += $metric->total_items_processed;
-		}
-
-		$success_rate = round(($total_success / max(1, $total_ops)) * 100, 1);
-
-		$message = Emails::getInstance()->h1('Daily Queue Performance Report');
-		$message .= sprintf('<p>Report for <strong>%s</strong></p>', $yesterday);
-
-		// Summary stats in grid
-		$stats = [
-			Emails::getInstance()->stat($total_ops, 'Operations'),
-			Emails::getInstance()->stat($total_success, 'Successful', '✓'),
-			Emails::getInstance()->stat($total_failed, 'Failed', $total_failed > 0 ? '⚠' : ''),
-			Emails::getInstance()->stat($success_rate . '%', 'Success Rate')
-		];
-		$message .= Emails::getInstance()->grid($stats, 4);
-
-		$message .= Emails::getInstance()->spacer(20);
-
-		// Alert if success rate is low
-		if ($success_rate < 90) {
-			$message .= Emails::getInstance()->alert(
-				sprintf('Success rate of %s%% is below the 90%% threshold', $success_rate),
-				'warning'
-			);
-		}
-
-		$message .= Emails::getInstance()->h2('Details by Operation Type');
-
-		// Details for each operation type
-		foreach ($metrics as $metric) {
-			$details = [];
-			$details[] = ['label' => 'Total', 'value' => $metric->total_operations];
-			$details[] = ['label' => 'Success', 'value' => Emails::getInstance()->badge($metric->successful_operations, 'success')];
-			$details[] = ['label' => 'Failed', 'value' => $metric->failed_operations > 0 ? Emails::getInstance()->badge($metric->failed_operations, 'error') : '0'];
-
-			if ($metric->average_duration) {
-				$details[] = ['label' => 'Avg Duration', 'value' => round($metric->average_duration, 2) . 's'];
-			}
-
-			$details[] = ['label' => 'Items Processed', 'value' => number_format($metric->total_items_processed)];
-
-			if ($metric->peak_memory_usage) {
-				$memory_mb = round($metric->peak_memory_usage / 1024 / 1024, 2);
-				$details[] = ['label' => 'Peak Memory', 'value' => $memory_mb . ' MB'];
-			}
-
-			$message .= Emails::getInstance()->card(
-				Emails::getInstance()->table($details),
-				esc_html($metric->type)
-			);
-		}
-
-		// Current queue status
-		$pending_count = $this->getCurrentQueueSize();
-		if ($pending_count > 0) {
-			$message .= Emails::getInstance()->spacer(20);
-			$message .= Emails::getInstance()->notice(
-				sprintf('<strong>Current Queue:</strong> %d operations pending', $pending_count)
-			);
-		}
-
-		$message .= Emails::getInstance()->spacer(20);
-		$message .= Emails::getInstance()->button(admin_url('admin.php?page=jvb-queue'), 'View Queue Dashboard');
-
-		Emails::getInstance()->sendEmail($admin_email, $subject, $message, 'QUEUE REPORT');
-	}
-
-    /**
-     * @return int
-     */
-    protected function getCPUUsage():int
-    {
-        $cpu = @sys_getloadavg();
-        return $cpu ? ( $cpu[0] * 100 / 2 ) : 0; // Normalize for 2 cores
-    }
-
-    /**
-     * @return int
-     */
-    protected function getMemoryUsage():int
-    {
-        $memory_usage = memory_get_usage(true);
-        $memory_limit = $this->getMemoryLimitBytes();
-
-        return (int)( $memory_usage / $memory_limit ) * 100;
-    }
-
-    /**
-     * @return int
-     */
-    protected function getMemoryLimitBytes():int
-    {
-        $memory_limit = ini_get('memory_limit');
-        if (preg_match('/^(\d+)(.)$/', $memory_limit, $matches)) {
-            $limit = $matches[1];
-            $unit  = strtolower($matches[2]);
-            switch ($unit) {
-                case 'g':
-                    $limit *= 1024;
-                case 'm':
-                    $limit *= 1024;
-                case 'k':
-                    $limit *= 1024;
-            }
-
-            return $limit;
-        }
-
-        return 134217728; // 128MB default
-    }
-
-    /**
-     * @return array
-     */
-	/**
-	 * Get queue status with scheduled operations
-	 */
-	/**
-	 * Get queue status with caching
-	 */
-	public function getQueueStatus(): array
-	{
-		// Try cache first
-		$cached = $this->cache->get(self::CACHE_QUEUE_STATUS);
-		if ($cached !== false) {
-			return $cached;
-		}
-
-		// If not cached, fetch from database
-
-		$table = $this->wpdb->prefix . $this->table;
-		$current_time = current_time('mysql');
-
-		// Get counts by status with single optimized query
-		$status_counts = $this->wpdb->get_results("
-            SELECT
-                status,
-                COUNT(*) as count,
-                SUM(IF(status = 'scheduled' AND scheduled_at <= '$current_time', 1, 0)) as scheduled_ready
-            FROM $table
-            GROUP BY status
-        ", OBJECT_K);
-
-		$result = [
-			'pending' => $status_counts['pending']->count ?? 0,
-			'scheduled' => $status_counts['scheduled']->count ?? 0,
-			'scheduled_ready' => $status_counts['scheduled']->scheduled_ready ?? 0,
-			'processing' => $status_counts['processing']->count ?? 0,
-			'completed' => $status_counts['completed']->count ?? 0,
-			'failed' => $status_counts['failed']->count ?? 0,
-			'total' => array_sum(array_column($status_counts, 'count'))
-		];
-
-		// Cache the result
-		$this->cache->set(self::CACHE_QUEUE_STATUS, $result);
-
-		return $result;
-	}
-
-    /**
-     * Calculate server load factor
-     *
-     * @return float Load factor (1.0 = normal, >1.0 = high load)
-     * @return float
-     */
-    private function calculateServerLoadFactor():float
-    {
-        // Get memory usage percentage
-        $memory_usage = memory_get_usage(true) / $this->getMemoryLimitBytes();
-
-        // Get CPU load if available
-        if (function_exists('sys_getloadavg')) {
-            $load        = sys_getloadavg();
-            $cpu_cores   = 2; // Adjust for your server
-            $load_factor = $load[0] / $cpu_cores;
-        } else {
-            $load_factor = 1.0; // Default if not available
-        }
-
-        // Calculate combined factor
-        return ( $load_factor * 0.7 ) + ( $memory_usage * 0.3 );
-    }
-
-    /**
-     * Dynamically adjust batch size based on server load
-     * @return float
-     */
-    protected function getAdaptiveBatchSize():float
-    {
-        $load_factor = $this->calculateServerLoadFactor();
-
-        if ($load_factor > 1.5) {
-            // Server is under heavy load
-            return max(5, floor($this->maxPerBatch / 5));
-        } elseif ($load_factor > 1.0) {
-            // Moderate load
-            return max(10, floor($this->maxPerBatch / 2));
-        } else {
-            // Normal load
-            return $this->maxPerBatch;
-        }
-    }
-
-    /**
-     * Get a specific column value from an operation by ID
-     *
-     * @param string $operation_id The ID of the operation
-     * @param string $column The column name to retrieve (default: 'result')
-     * @param bool $decode_json Whether to JSON decode the retrieved value (default: true)
-     * @param string|null $status Filter by operation status (default: null - any status)
-     *
-     * @return mixed|false The requested value or false if not found
-     */
-    public function getOperationValue(string $operation_id, string $column, bool $decode_json = true, string|null $status = null):mixed
-    {
-        try {
-
-            $table = $this->wpdb->prefix . $this->table;
-
-            // Prepare the query with status filter if provided
-            $sql    = "SELECT {$this->wpdb->_escape($column)} FROM $table WHERE id = %s";
-            $params = [ $operation_id ];
-
-            // Get the column value
-            $value = $this->wpdb->get_var($this->wpdb->prepare($sql, $params));
-
-            if ($value === null) {
-                return false;
-            }
-
-            // Decode JSON if requested and column appears to be JSON
-            if ($decode_json && $value && (str_starts_with(trim($value), '{') || str_starts_with(trim($value), '['))) {
-                $decoded = json_decode($value, true);
-                // Return the decoded value only if decode was successful
-                if (json_last_error() === JSON_ERROR_NONE) {
-                    return $decoded;
-                }
-            }
-
-            return $value;
-        } catch (Exception $e) {
-            Errors::getInstance()->log(
-                '[OperationQueue]:getOperationValue',
-                'Error retrieving operation value: ' . $e->getMessage(),
-                [
-                    'operation_id' => $operation_id,
-                    'column'       => $column
-                ],
-                'error'
-            );
-
-            return false;
-        }
-    }
-
-    /**
-     * Get average processing time for an operation type
-     *
-     * @param string $type The type of operation
-     *
-     * @return float Average seconds per operation
-     */
-    private function getAverageProcessingTime(string $type):float
-    {
-        $stats = get_option(BASE . 'average_operation_time', []);
-        //Return stored stat, if available
-        if (!empty($stats) && array_key_exists($type, $stats)) {
-            return (float) $stats[$type];
-        }
-        //Build statistics if not available
-        $stats[$type] = $this->buildAverageProcessingTime($type);
-
-        return $stats[$type];
-    }
-
-    /**
-     * @return array
-     */
-    private function getAllAverageProcessingTimes():array
-    {
-        return get_option(BASE . 'average_operation_time', []);
-    }
-
-    /**
-     * @param string $type
-     *
-     * @return float
-     */
-    private function buildAverageProcessingTime(string $type): float
-    {
-        // Get existing stats from options table
-        $stats = get_option(BASE . 'average_operation_time', []);
-
-        // If there's already a saved value for this operation type, return it
-        if (isset($stats[$type]) && is_numeric($stats[$type])) {
-            return (float)$stats[$type];
-        }
-
-        // Query the database for average processing time
-
-        $table = $this->wpdb->prefix . $this->table;
-
-        $avgTime = $this->wpdb->get_var(
-            $this->wpdb->prepare(
-                "SELECT AVG(TIMESTAMPDIFF(SECOND, started_at, completed_at) / count)
-             FROM {$table}
-             WHERE type = %s
-             AND status = 'completed'",
-                $type
-            )
-        );
-
-        // If query returns null or 0, use default values based on operation type
-        if ($avgTime === null || $avgTime === false || (float)$avgTime <= 0) {
-            // Default times for different operation types
-            $defaultTimes = [
-                'email_notification_digest' => 20.0,
-                'image_upload'              => 10.0,
-                'image_processing'          => 5.0,
-                'batch_upload'              => 10.0,
-                'artist_bulk_approval'      => 15.0,
-                'taxonomy_relationships'    => 60.0,
-                'rebuild_user_term_index'   => 10.0,
-                'bio_update'                => 12.0,
-                'featured_image'            => 15.0,
-                'content_update'            => 8.0,
-                'batch_creation'            => 20.0,
-                'favourites_batch'          => 10.0,
-                'favourite_list_add'        => 5.0,
-                'favourite_list_remove'     => 5.0,
-                'notification_mark_all_as_read' => 15.0,
-                'sync_content_taxonomy_tables'          => 20.0,
-                'temporary_cleanup'         => 10.0,
-            ];
-
-            // Use type-specific default or general default
-            $avgTime = (array_key_exists($type, $defaultTimes)) ? $defaultTimes[$type] : 3.0;
-        }
-
-        // Ensure it's a float and save it
-        $avgTime = (float)$avgTime;
-        $stats[$type] = $avgTime;
-        update_option(BASE . 'average_operation_time', $stats);
-
-        return $avgTime;
-    }
-
-	public function cleanupStuckOperations(): void
-	{
-
-		$table = $this->wpdb->prefix . $this->table;
-
-		// Find operations that exceeded retry limits but weren't marked as failed
-		$stuck_operations = $this->wpdb->get_results($this->wpdb->prepare("
-            SELECT id, type, retries FROM $table
-            WHERE retries >= %d
-            AND status NOT IN ('failed', 'completed')
-        ", $this->max_attempts));
-
-		if (!empty($stuck_operations)) {
-			foreach ($stuck_operations as $operation) {
-				$this->markAsPermanentlyFailed(
-					$operation->id,
-					"Cleanup: Operation exceeded {$this->max_attempts} attempts (had {$operation->retries})"
-				);
-			}
-		}
-
-		// Also find operations stuck in processing for too long
-		$long_processing = $this->wpdb->get_results($this->wpdb->prepare("
-            SELECT id, type, started_at FROM $table
-            WHERE status = 'processing'
-            AND started_at < %s
-        ", date('Y-m-d H:i:s', strtotime('-1 hour'))));
-
-		if (!empty($long_processing)) {
-			foreach ($long_processing as $operation) {
-				$this->wpdb->update($table, [
-					'status' => 'pending',
-					'started_at' => null,
-					'retries' => $this->wpdb->get_var($this->wpdb->prepare(
-							"SELECT retries FROM $table WHERE id = %s",
-							$operation->id
-						)) + 1,
-					'updated_at' => current_time('mysql')
-				], ['id' => $operation->id]);
-			}
-		}
-	}
-
-	public function renderAdminPage(): void
-	{
-		$status = $this->getQueueStatus();
-		$health = $this->getQueueHealthReport();
-		?>
-		<div class="wrap">
-			<h1>Operation Queue Status</h1>
-
-			<div class="card">
-				<h2>Current Status</h2>
-				<ul>
-					<li>Queue is: <?= ($this->isQueueLocked()) ? 'locked' : 'unlocked'?>.</li>
-					<li>Queue: <?= ($this->hasItemsInQueue()) ? 'has items in queue' : 'has nothing in queue'?>.</li>
-					<li>Pending: <?php echo $status['pending']; ?></li>
-					<li>Processing: <?php echo $status['processing']; ?></li>
-					<li>Scheduled: <?php echo $status['scheduled']; ?>
-						(<?php echo $status['scheduled_ready']; ?> ready)</li>
-					<li>Completed: <?php echo $status['completed']; ?></li>
-					<li>Failed: <?php echo $status['failed']; ?></li>
-				</ul>
-			</div>
-
-			<?php if (!empty($health['stuck_operations'])): ?>
-				<div class="card">
-					<h2>⚠️ Stuck Operations</h2>
-					<table class="wp-list-table widefat fixed striped">
-						<thead>
-						<tr>
-							<th>ID</th>
-							<th>Type</th>
-							<th>Status</th>
-							<th>Age (minutes)</th>
-							<th>Error</th>
-						</tr>
-						</thead>
-						<tbody>
-						<?php foreach ($health['stuck_operations'] as $op): ?>
-							<tr>
-								<td><?php echo esc_html($op['id']); ?></td>
-								<td><?php echo esc_html($op['type']); ?></td>
-								<td><?php echo esc_html($op['status']); ?></td>
-								<td><?php echo esc_html($op['age_minutes']); ?></td>
-								<td><?php echo esc_html(substr($op['error_message'] ?? '', 0, 50)); ?></td>
-							</tr>
-						<?php endforeach; ?>
-						</tbody>
-					</table>
-				</div>
-			<?php endif; ?>
-		</div>
-		<?php
-	}
-
-	function getQueueHealthReport(): array
-	{
-
-		$table = $this->wpdb->prefix . BASE . '_operation_queue';
-
-		$report = [
-			'status_counts' => [],
-			'stuck_operations' => [],
-			'error_patterns' => [],
-			'performance_stats' => []
-		];
-
-		// Status counts
-		$status_counts = $this->wpdb->get_results("
-        SELECT status, COUNT(*) as count, AVG(retries) as avg_retries
-        FROM $table
-        GROUP BY status
-    ", OBJECT_K);
-
-		foreach ($status_counts as $status => $data) {
-			$report['status_counts'][$status] = [
-				'count' => intval($data->count),
-				'avg_retries' => round(floatval($data->avg_retries), 2)
-			];
-		}
-
-		// Stuck operations
-		$report['stuck_operations'] = $this->wpdb->get_results("
-        SELECT id, type, status, retries,
-               TIMESTAMPDIFF(MINUTE, created_at, NOW()) as age_minutes,
-               error_message
-        FROM $table
-        WHERE (
-            retries >= 3 OR
-            (status = 'processing' AND started_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE)) OR
-            (status = 'pending' AND created_at < DATE_SUB(NOW(), INTERVAL 2 HOUR))
-        )
-        ORDER BY created_at DESC
-        LIMIT 10
-    ", ARRAY_A);
-
-		// Error patterns
-		$report['error_patterns'] = $this->wpdb->get_results("
-        SELECT
-            SUBSTRING(error_message, 1, 100) as error_pattern,
-            COUNT(*) as frequency,
-            type
-        FROM $table
-        WHERE error_message IS NOT NULL
-        AND updated_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)
-        GROUP BY type, SUBSTRING(error_message, 1, 100)
-        HAVING frequency > 1
-        ORDER BY frequency DESC
-        LIMIT 5
-    ", ARRAY_A);
-
-		return $report;
-	}
-
-	protected function archiveToMetrics(int $days_to_keep): void
-	{
-
-		$table = $this->wpdb->prefix . $this->table;
-		$metricsTable = $this->wpdb->prefix . $this->metricsTable;
-
-		// Aggregate and insert metrics
-		$this->wpdb->query($this->wpdb->prepare("
-            INSERT INTO $metricsTable
-            (date, type, total_operations, successful_operations,
-             failed_operations, average_duration, total_items_processed)
-            SELECT
-                DATE(completed_at) as date,
-                type,
-                COUNT(*) as total_operations,
-                SUM(IF(status = 'completed', 1, 0)) as successful,
-                SUM(IF(status LIKE 'failed%', 1, 0)) as failed,
-                AVG(TIMESTAMPDIFF(SECOND, created_at, completed_at)) as avg_duration,
-                SUM(count) as items_processed
-            FROM $table
-            WHERE completed_at < DATE_SUB(NOW(), INTERVAL %d DAY)
-            AND completed_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
-            GROUP BY DATE(completed_at), type
-            ON DUPLICATE KEY UPDATE
-                total_operations = VALUES(total_operations),
-                successful_operations = VALUES(successful_operations),
-                failed_operations = VALUES(failed_operations),
-                average_duration = VALUES(average_duration),
-                total_items_processed = VALUES(total_items_processed)
-        ", $days_to_keep, $days_to_keep + 1));
-	}
-}
diff --git a/inc/managers/Responses.php b/inc/managers/Responses.php
index 3609d1e..c3051af 100644
--- a/inc/managers/Responses.php
+++ b/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
diff --git a/inc/managers/UploadManager.php b/inc/managers/UploadManager.php
index 4889dbc..bb39c39 100644
--- a/inc/managers/UploadManager.php
+++ b/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];
 		}
 
diff --git a/inc/managers/UserTermsManager.php b/inc/managers/UserTermsManager.php
index c3a02e8..f2b0b15 100644
--- a/inc/managers/UserTermsManager.php
+++ b/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()
diff --git a/inc/managers/_setup.php b/inc/managers/_setup.php
index baeb21d..ed9aa30 100644
--- a/inc/managers/_setup.php
+++ b/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');
diff --git a/inc/managers/queue/Queue.php b/inc/managers/queue/Queue.php
index 01bcb31..86ba4f9 100644
--- a/inc/managers/queue/Queue.php
+++ b/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());
 		}
 	}
diff --git a/inc/managers/queue/executors/ContentExecutor.php b/inc/managers/queue/executors/ContentExecutor.php
index 562a004..5d07be0 100644
--- a/inc/managers/queue/executors/ContentExecutor.php
+++ b/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',
diff --git a/inc/managers/queue/executors/IntegrationExecutor.php b/inc/managers/queue/executors/IntegrationExecutor.php
index 6a85296..a44a4d0 100644
--- a/inc/managers/queue/executors/IntegrationExecutor.php
+++ b/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',
diff --git a/inc/managers/queue/executors/UploadExecutor.php b/inc/managers/queue/executors/UploadExecutor.php
index 138d022..8a9b677 100644
--- a/inc/managers/queue/executors/UploadExecutor.php
+++ b/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',
diff --git a/inc/meta/Storage.php b/inc/meta/Storage.php
index 433b478..732a9bb 100644
--- a/inc/meta/Storage.php
+++ b/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;
 		}
diff --git a/inc/registrar/Posts.php b/inc/registrar/Posts.php
index bedacaa..ef9e3f3 100644
--- a/inc/registrar/Posts.php
+++ b/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());
 		}
 	}
 
diff --git a/inc/registrar/helpers/AddCustomFields.php b/inc/registrar/helpers/AddCustomFields.php
index 2be7fd2..1cccca9 100644
--- a/inc/registrar/helpers/AddCustomFields.php
+++ b/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;
+	}
 }
diff --git a/inc/rest/Rest.php b/inc/rest/Rest.php
index cc2caef..81c6fcb 100644
--- a/inc/rest/Rest.php
+++ b/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
 	 ************************************************************/
diff --git a/inc/rest/RestRouteManager.php b/inc/rest/RestRouteManager.php
deleted file mode 100644
index b714389..0000000
--- a/inc/rest/RestRouteManager.php
+++ /dev/null
@@ -1,1058 +0,0 @@
-<?php
-namespace JVBase\rest;
-
-use DateTime;
-use DateTimeZone;
-use JVBase\integrations\services\Cloudflare;
-use JVBase\managers\Errors;
-use JVBase\registrar\Registrar;
-use JVBase\managers\OperationQueue;
-use JVBase\managers\Cache;
-use JVBase\managers\NotificationManager;
-use JVBase\base\Site;
-use WP_REST_Request;
-use WP_Error;
-use Exception;
-use WP_REST_Response;
-
-if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
-}
-
-/**
- * @deprecated use Rest.php
- * Handles route registration and high-level coordination
- */
-abstract class RestRouteManager
-{
-    protected string $namespace = 'jvb/v1';
-
-    protected RateLimiter|null $rate_limiter;
-    protected array $types;
-    protected string $route;
-    protected string $base;
-    protected string $content_type; //the registered post type
-    protected string $type; //post, user, term, for Meta
-    protected string $action = ''; //optional additional nonce to check
-    protected array $callback; //route->callback array
-    protected string $operation_type; // from QueueManager.js and OperationQueue.php
-    protected OperationQueue $queue;
-    protected Cache $cache;
-    protected NotificationManager $notifications;
-    protected string $cache_name ='';
-    protected int $cache_ttl = 3600; //1 hour default
-	protected array $response_headers = [];
-
-    // Error code constants for consistency
-    const ERROR_MISSING_PARAMS = 'missing_parameters';
-    const ERROR_ACCESS_DENIED = 'access_denied';
-    const ERROR_NOT_FOUND = 'not_found';
-    const ERROR_INVALID_OPERATION = 'invalid_operation';
-    const ERROR_PROCESSING = 'processing_error';
-
-    public function __construct()
-    {
-        $this->base = BASE;
-        $this->rate_limiter = new RateLimiter();
-        if ($this->cache_name !== '') {
-            $this->cache = Cache::for($this->cache_name, $this->cache_ttl);
-        }
-        add_action('rest_api_init', [$this, 'registerRoutes']);
-    }
-
-    public function init()
-    {
-        //Replace in child classes, if necessary
-    }
-
-
-    /**
-     * @param WP_REST_Request $request Rest Request object
-     * @param string $nonceName nonce name to check
-     * @param string|null $nonce optional additional nonce to check
-     *
-     * @return bool|WP_Error
-	 */
-    public function verifyNonce(WP_REST_Request $request, string $nonceName = 'wp_rest', string|null $nonce = null): bool|WP_Error
-	{
-        $nonce = (!is_null($nonce)) ? $nonce :  $request->get_header('X-WP-Nonce');
-
-        if (!wp_verify_nonce($nonce, $nonceName)) {
-            return new WP_Error(
-                'invalid_nonce',
-                'Invalid nonce',
-                ['status' => 403]
-            );
-        }
-        return true;
-    }
-
-    abstract public function registerRoutes();
-
-
-    /**
-     * @param WP_REST_Request $request the Request Object
-     *
-     * @return bool|WP_Error Whether or not we can proceed
-     */
-    public function checkPermission(WP_REST_Request $request):bool|WP_Error
-    {
-        // Check rate limits first
-        if (!$this->rate_limiter->checkLimit($request)) {
-			error_log('Rate Limit Reached');
-            return new WP_Error(
-                'rate_limit_reached',
-                'Rate limit reached',
-                ['status' => 403]
-            );
-        }
-        $user_id = $request->get_param('user');
-        if (!empty($user_id) && !$this->userCheck($user_id)) {
-			error_log('Usercheck failed');
-            return false;
-        }
-        // Verify nonces
-        $this->verifyNonce($request);
-        if ($this->action!=='') {
-            $this->verifyNonce($request, $this->action . $user_id, $request->get_header('action_nonce'));
-        }
-        return true;
-    }
-
-	public function checkRateLimit(WP_REST_Request $request):bool|WP_Error
-	{
-		if (!$this->rate_limiter->checkLimit($request)) {
-			error_log('Rate Limit Reached');
-			return new WP_Error(
-				'rate_limit',
-				'Too many attempts. Please wait a moment before trying again.',
-				['status' => 429]
-			);
-		}
-		return true;
-	}
-
-	/**
-	 * Convert MySQL datetime to ISO 8601 timestamp with proper timezone
-	 */
-	public function formatTimestamp(?string $mysql_datetime): ?string
-	{
-		if (empty($mysql_datetime)) {
-			return null;
-		}
-
-		try {
-			// Get WordPress timezone - dates are stored in this timezone
-			$wp_timezone = wp_timezone();
-
-			// Parse the datetime in WordPress timezone
-			$date = new DateTime($mysql_datetime, $wp_timezone);
-
-			// Convert to UTC for API consistency
-			$date->setTimezone(new DateTimeZone('UTC'));
-
-			// Return ISO 8601 format
-			return $date->format('c');
-		} catch (Exception $e) {
-			return null;
-		}
-	}
-
-	protected function checkContent(string $content, bool $bool = false):string|bool
-	{
-		return (bool)Registrar::getInstance($content);
-	}
-
-
-    public function formatError($code, $message, $status = 400)
-    {
-        return new WP_Error($code, $message, ['status' => $status]);
-    }
-
-    /**
-     * @param int $userID the userID to check
-     *
-     * @return bool whether or not the $userID matches the currently logged in user
-     */
-    protected function userCheck(int $userID)
-    {
-        return $userID === get_current_user_id();
-    }
-
-    /**
-     * Makes sure the value is in the format it needs to be
-     * @param mixed $value
-     *
-     * @return mixed
-     */
-    protected function getMetaValues(mixed $value):mixed
-    {
-        //get the repeater out of there
-        $temp = json_decode($value);
-        if (is_array($temp)) {
-            $value = [];
-            foreach ($temp as $t) {
-                if (is_object($t)) {
-                    $t = (array)$t;
-                }
-                $value[] = $t;
-            }
-        }
-        return $value;
-    }
-    /**
-     * Log errors with proper context
-     * @param string $message The error message
-     * @param array $context Additional context
-     * @param string $severity
-     * @return void
-     */
-    protected function logError(string $message, array $context = [], string $severity = 'error')
-    {
-        try {
-            Errors::getInstance()->log(
-                'artist_invitations', // component
-                $message,
-                $context,
-                $severity
-            );
-        } catch (Exception $e) {
-            // Fallback if error handler fails
-            error_log("Invitations Error: {$message} - " . json_encode($context));
-        }
-    }
-
-	/**
-	 * Check if user exists (cached)
-	 */
-	protected function checkUser(int $userID): bool
-	{
-		$cache = Cache::for('checkUser', 'user',DAY_IN_SECONDS);
-
-		return $cache->remember($userID, function() use ($userID) {
-			return (bool)get_userdata($userID);
-		});
-	}
-
-	/**
-	 * Check if shop exists (cached)
-	 */
-	protected function checkShop(int $shopID): bool
-	{
-		$cache = Cache::for('checkShop', 'term',DAY_IN_SECONDS);
-
-		return $cache->remember($shopID, function() use ($shopID) {
-			return (bool)term_exists($shopID, BASE . 'shop');
-		});
-	}
-
-	/**
-	 * Check if term exists (cached)
-	 */
-	protected function checkTerm(array $args): bool
-	{
-		$termID = $args['to_term'] ?? $args['term_id'] ?? false;
-		if (!$termID) {
-			return false;
-		}
-
-		$taxonomy = $args['taxonomy'] ?? false;
-		if (!$taxonomy) {
-			return false;
-		}
-
-		$taxonomy = jvbCheckBase($taxonomy);
-		$cache = Cache::for('checkTerm', 'term', DAY_IN_SECONDS);
-
-		return $cache->remember($termID, function() use ($termID, $taxonomy) {
-			return (bool)term_exists($termID, $taxonomy);
-		});
-	}
-
-	/**
-	 * Check if an artist is verified
-	 */
-	public function isVerifiedUser(int $user_id): bool
-	{
-		$cache = Cache::for('verifiedUsers', 'user',DAY_IN_SECONDS);
-
-		return $cache->remember($user_id, function() use ($user_id) {
-			return user_can($user_id, 'skip_moderation');
-		});
-	}
-
-	/**
-	 * @deprecated
-	 * @param array $args
-	 * @param array $data
-	 * @return array
-	 */
-	protected function applyTaxonomyFilters(array $args, array $data):array
-	{
-		// Handle JSON-encoded taxonomy data
-		if (array_key_exists('taxonomy', $data) && is_string($data['taxonomy'])) {
-			$data['taxonomy'] = json_decode($data['taxonomy'], true);
-		}
-
-		$taxonomies = $data['taxonomies'] ?? $data['taxonomy'] ?? [];
-		$taxQuery = [];
-
-		foreach($taxonomies as $taxonomy => $terms) {
-			// Better validation: check if taxonomy actually exists
-			if (!taxonomy_exists(jvbCheckBase($taxonomy))) {
-				continue;
-			}
-
-			$taxQuery[] = [
-				'taxonomy'	=> jvbCheckBase($taxonomy),
-				'field'     => 'term_id',
-				'terms'		=> array_map(
-					'absint',
-					is_array($terms) ? $terms : explode(',', $terms)
-				),
-				'operator'  => 'IN'
-			];
-		}
-
-		if (!empty($taxQuery)) {
-			// Match 'all' = AND, anything else = OR
-			$relation = ($data['match'] ?? 'all') === 'all' ? 'AND' : 'OR';
-
-			$args['tax_query'] = array_merge([
-				'relation'  => $relation,
-			], $taxQuery);
-		}
-
-		// Keep existing author filtering logic
-		$authorQuery = [];
-		foreach (Registrar::withFeature('can_create', 'user') as $type) {
-			if (array_key_exists($type, $data)) {
-				$artist_ids = array_map(
-					'absint',
-					is_array($data[$type]) ?
-						$data[$type] :
-						explode(',', $data[$type])
-				);
-				$authorQuery = array_merge($authorQuery, $artist_ids);
-			}
-		}
-		if (!empty($authorQuery)) {
-			$args['author__in'] = array_unique($authorQuery);
-		}
-
-		return $args;
-	}
-
-    protected function applyOrderFilters(array $args, array $data):array
-    {
-		// Check for custom order first
-		$customArgs = $this->applyCustomOrder($args, $data);
-		if ($customArgs !== null) {
-			$order = (array_key_exists('order', $data)) ? strtoupper($data['order']) : 'DESC';
-			$customArgs['order'] = (in_array($order, ['ASC', 'DESC'])) ? $order : 'DESC';
-			return $customArgs;
-		}
-
-		//Handle random
-        if (array_key_exists('orderby', $data) && $data['orderby'] === 'random') {
-            $current_seed = floor(time() / 1800);
-            $args['orderby'] = 'RAND(' . $current_seed . ')';
-            unset($args['order']);
-            return $args;
-        }
-
-        if (in_array($data['orderby'], ['date', 'modified', 'title', 'alphabetical'])) {
-			if ($data['orderby'] === 'date' && $this->isTimeline($args, $data)) {
-				$args['meta_key'] = BASE . 'latest_date';
-				$args['orderby'] = 'meta_value_num';
-			} else {
-				$args['orderby'] = ($data['orderby'] === 'alphabetical') ? 'title' : $data['orderby'];
-			}
-
-        } else {
-            switch ($data['orderby']) {
-                case 'popularity':
-                    $args['meta_key'] = BASE.'upvotes';
-                    $args['orderby'] = 'meta_value_num';
-                    break;
-                case 'karma':
-                    $args['meta_key'] = BASE.'karma';
-                    $args['orderby'] = 'meta_value_num';
-                    break;
-				case 'unpopularity':
-					$args['meta_key'] = BASE.'downvotes';
-					$args['orderby'] = 'meta_value_num';
-					break;
-				case 'favourites':
-					$args['meta_key'] = BASE.'total_favourites';
-					$args['orderby'] = 'meta_value_num';
-					break;
-				case 'date':
-				default:
-					$args['orderby'] = 'date';
-					break;
-            }
-        }
-		$order = (array_key_exists('order', $data)) ? strtoupper($data['order']) : 'DESC';
-        $args['order'] = (in_array($order, ['ASC', 'DESC'])) ? $order : 'DESC';
-
-        return $args;
-    }
-
-	/**
-	 * Apply custom order if defined in content/taxonomy/user config
-	 *
-	 * @param array $args WP_Query args
-	 * @param array $data Request data
-	 * @return array|null Modified args if custom order found, null otherwise
-	 */
-	protected function applyCustomOrder(array $args, array $data): ?array
-	{
-		$orderby = $data['orderby'] ?? '';
-
-		// Skip if no orderby or it's a standard order
-		if (empty($orderby) || in_array($orderby, ['date', 'modified', 'title', 'alphabetical', 'random', 'popularity', 'karma', 'unpopularity', 'favourites'])) {
-			return null;
-		}
-
-		// Determine content type
-		$post_type = is_array($args['post_type']) ? $args['post_type'][0] : $args['post_type'];
-		$content = jvbNoBase($post_type);
-
-		// Get config for this content type
-		$config = Registrar::getInstance($content);
-		if (!$config) {
-			return null;
-		}
-
-		// Check if this orderby is a custom order
-		$customOrders = $config->custom_order ?? [];
-		if (empty($customOrders) || !isset($customOrders[$orderby])) {
-			return null;
-		}
-
-		// Get field definition
-		$fields = $config->getFields()??[];
-		if (!isset($fields[$orderby])) {
-			return null;
-		}
-
-		$field = $fields[$orderby];
-
-		// Set meta_key
-		$args['meta_key'] = BASE . $orderby;
-
-		// Determine orderby and meta_type based on field type
-		$fieldType = $field['type'] ?? 'text';
-		$subtype = $field['subtype'] ?? '';
-
-		switch ($fieldType) {
-			case 'number':
-				$args['orderby'] = 'meta_value_num';
-				break;
-
-			case 'text':
-				$args['orderby'] = ($subtype === 'number') ? 'meta_value_num' : 'meta_value';
-				break;
-
-			case 'date':
-				$args['orderby'] = 'meta_value';
-				$args['meta_type'] = 'DATE';
-				break;
-
-			case 'datetime':
-				$args['orderby'] = 'meta_value';
-				$args['meta_type'] = 'DATETIME';
-				break;
-
-			case 'true_false':
-			case 'checkbox':
-				$args['orderby'] = 'meta_value';
-				$args['meta_type'] = 'BINARY';
-				break;
-
-			default:
-				$args['orderby'] = 'meta_value';
-		}
-
-		return $args;
-	}
-
-	protected function isTimeline($args, $data):bool
-	{
-		$post_types = is_array($args['post_type']) ? $args['post_type'] : [$args['post_type']];
-		$areTimeline = array_map(function($type) { return BASE.$type; },Registrar::withFeature('is_timeline', 'post'));
-		return !empty(array_intersect($post_types, $areTimeline));
-	}
-
-    protected function applyDateFilters(array $args, array $data):array
-    {
-        if (!array_key_exists('date-filter', $data) && !array_key_exists('dateFrom', $data)) {
-            return $args;
-        }
-        if (array_key_exists('dateFrom', $data)) {
-            $dateFrom = strtotime(sanitize_text_field($data['dateFrom']));
-            $dateTo = strtotime(sanitize_text_field($data['dateTo']));
-            if ($dateFrom && $dateTo) {
-                $args['date_query'] = [
-                    [
-                        'after'     => date('c', $dateFrom),
-                        'before'    => date('c', $dateTo),
-                        'inclusive' => true,
-                    ]
-                ];
-            }
-        } else {
-            switch ($data['date-filter']) {
-                case 'today':
-                    $args['date_query'] = [['after' => '1 day ago']];
-                    break;
-                case 'week':
-                    $args['date_query'] = [['after' => '1 week ago']];
-                    break;
-                case 'month':
-                    $args['date_query'] = [['after' => '1 month ago']];
-                    break;
-                case 'year':
-                    $args['date_query'] = [['after' => '1 year ago']];
-                    break;
-            }
-        }
-        return $args;
-    }
-
-	protected function applyCalendarFilters(array $args, array $data):array
-	{
-		$meta_query = [];
-		$today = date('Y-m-d');
-		if (in_array('future', $args['post_status'])) {
-			$meta_query[] = [
-				'key'	=> 'jvb_start_date',
-				'value'	=> $today,
-				'compare'	=> '>=',
-				'type'	=> 'DATE'
-			];
-		}
-		if (in_array('past', $args['post_status'])) {
-			$meta_query[] = [
-				'key' => 'jvb_end_date',
-				'value' => $today,
-				'compare' => '<',
-				'type' => 'DATE'
-			];
-		}
-		if (in_array('recurring', $args['post_status'])) {
-			$meta_query[] = [
-				'key' => 'jvb_is_recurring',
-				'value' => true,
-				'compare' => '='
-			];
-		}
-		if (!empty($meta_query)) {
-			$args['meta_query'] = (array_key_exists('meta_query', $args)) ? array_merge($args['meta_query'], $meta_query) : $meta_query;
-		}
-		return $args;
-
-	}
-
-	protected function table_exists($tableName, $wpdb = null)
-	{
-		if (!$wpdb) {
-			global $wpdb;
-		}
-		return $wpdb->get_var("SHOW TABLES LIKE '{$tableName}'") !== $tableName;
-	}
-
-	// ========== HTTP CACHING METHODS ==========
-	/**
-	 * Check HTTP caching headers (ETag and If-Modified-Since)
-	 * Returns 304 Not Modified if content hasn't changed
-	 * @param WP_REST_Request $request
-	 * @param int|string|array $key Content type(s) to check timestamps for
-	 * @param string|array $group Additional params for ETag uniqueness (e.g., user_id, filters)
-	 * @return WP_REST_Response|false Returns 304 response if not modified, null to continue processing
-	 */
-	protected function checkHeaders(
-		WP_REST_Request $request,
-		int|string|array $key,
-		string|array $group = ''
-	): WP_REST_Response|false
-	{
-		$group = ($group!=='') ? $group : $this->cache_name;
-		$cache = $this->cache_name !== $group ? Cache::for($group) : $this->cache;
-		if (!$cache) {
-			return false;
-		}
-		if (is_array($key)) {
-			$key = $cache->generateKey($key);
-		}
-
-
-
-		$lastModified = $cache::lastModified($group);
-
-		if (!$lastModified) {
-			return false;
-		}
-
-
-		$etag = '"' . sha1($group . ':' . $key . ':' . $lastModified) . '"';
-
-		// ETag check
-		$ifNoneMatch = $request->get_header('if-none-match');
-		if ($ifNoneMatch && trim($ifNoneMatch) === $etag) {
-			return new WP_REST_Response(null, 304);
-		}
-
-		// Last-Modified check
-		$ifModifiedSince = $request->get_header('if-modified-since');
-		if ($ifModifiedSince && strtotime($ifModifiedSince) >= $lastModified) {
-			return new WP_REST_Response(null, 304);
-		}
-
-		// Store headers for response phase
-		$this->response_headers = [
-			'ETag' => $etag,
-			'Last-Modified' => gmdate('D, d M Y H:i:s', $lastModified) . ' GMT',
-		];
-
-		return false;
-	}
-
-	/**
-	 * Create 304 Not Modified response with proper headers
-	 *
-	 * @param string $etag ETag value
-	 * @param int $last_modified Last modified timestamp
-	 * @return WP_REST_Response 304 response
-	 */
-	private function createNotModifiedResponse(string $etag, int $last_modified): WP_REST_Response
-	{
-		$response = new WP_REST_Response(null, 304);
-		$response->set_headers($this->buildCacheHeaders($etag, $last_modified));
-		return $response;
-	}
-
-	/**
-	 * Build cache headers array
-	 *
-	 * @param string $etag ETag value
-	 * @param int $last_modified Last modified timestamp
-	 * @return array Headers array
-	 */
-	private function buildCacheHeaders(string $etag, int $last_modified): array
-	{
-		return [
-			'ETag' => $etag,
-			'Last-Modified' => gmdate('D, d M Y H:i:s', $last_modified) . ' GMT',
-			'Cache-Control' => 'private, max-age=60, must-revalidate'
-		];
-	}
-
-	/**
-	 * Add stored cache headers to a response
-	 * Call this on your final WP_REST_Response before returning
-	 *
-	 * @param WP_REST_Response $response The response to add headers to
-	 * @return WP_REST_Response The response with headers added
-	 */
-	protected function addCacheHeaders(WP_REST_Response $response): WP_REST_Response
-	{
-		if (!empty($this->response_headers)) {
-			foreach ($this->response_headers as $name => $value) {
-				$response->header($name, $value);
-			}
-			$this->response_headers = [];
-		}
-
-		return $response;
-	}
-
-	/**
-	 * Helper to return error response
-	 */
-	protected function error(string $message, string $code, int $status = 400, ?string $field = null): WP_REST_Response
-	{
-		$data = [
-			'success'	=> false,
-			'message' => $message,
-			'code' => $code
-		];
-
-		if ($field) {
-			$data['field'] = $field;
-		}
-
-		return new WP_REST_Response($data, $status);
-	}
-	/**
-	 * Helper to return success response
-	 */
-	protected function success(array $data, int $status = 200): WP_REST_Response
-	{
-		$data['success'] = true;
-		return new WP_REST_Response($data, $status);
-	}
-
-	/************************************************************
-		SESSION FINGERPRINT
-	 ************************************************************/
-	/**
-	 * Store session fingerprint for hijacking detection
-	 */
-	protected function storeSessionFingerprint(int $user_id, WP_REST_Request $request): void
-	{
-		if (!defined('JVB_SESSION_FINGERPRINT') || !JVB_SESSION_FINGERPRINT) {
-			return;
-		}
-
-		$fingerprint = $this->generateSessionFingerprint($request);
-		update_user_meta($user_id, BASE . 'session_fingerprint', $fingerprint);
-		update_user_meta($user_id, BASE . 'session_timestamp', time());
-	}
-	/**
-	 * Generate session fingerprint for hijacking detection
-	 *
-	 * @param WP_REST_Request $request The REST request
-	 * @return string Hashed fingerprint
-	 */
-	protected function generateSessionFingerprint(WP_REST_Request $request): string
-	{
-		return hash('sha256', implode('|', [
-			$request->get_header('User-Agent') ?? '',
-			// Use IP class instead of full IP to allow for mobile network changes
-			$this->getIPClass(
-				$request->get_header('X-Forwarded-For')
-					?: $request->get_header('X-Real-IP')
-					?: $_SERVER['REMOTE_ADDR'] ?? ''
-			)
-		]));
-	}
-
-	/**
-	 * Get IP class (first 3 octets) for session validation
-	 * This allows for minor IP changes (common with mobile networks)
-	 *
-	 * @param string $ip IP address
-	 * @return string First 3 octets
-	 */
-	protected function getIPClass(string $ip): string
-	{
-		$parts = explode('.', $ip);
-		return implode('.', array_slice($parts, 0, 3));
-	}
-
-	/**
-	 * Validate session fingerprint against stored value
-	 *
-	 * @param int $user_id User ID to validate
-	 * @param WP_REST_Request $request Current request
-	 * @return bool True if valid, false if potential hijacking
-	 */
-	protected function validateSessionFingerprint(int $user_id, WP_REST_Request $request): bool
-	{
-		// Only enforce if enabled in config
-		if (!defined('JVB_SESSION_FINGERPRINT') || !JVB_SESSION_FINGERPRINT) {
-			return true;
-		}
-
-		$stored = get_user_meta($user_id, BASE . 'session_fingerprint', true);
-		$current = $this->generateSessionFingerprint($request);
-
-		if (empty($stored)) {
-			// First request - store fingerprint
-			update_user_meta($user_id, BASE . 'session_fingerprint', $current);
-			update_user_meta($user_id, BASE . 'session_timestamp', time());
-			return true;
-		}
-
-		// Compare using timing-safe comparison
-		return hash_equals($stored, $current);
-	}
-
-	/**
-	 * Clear session fingerprint (call on logout)
-	 *
-	 * @param int $user_id User ID
-	 * @return void
-	 */
-	protected function clearSessionFingerprint(int $user_id): void
-	{
-		delete_user_meta($user_id, BASE . 'session_fingerprint');
-		delete_user_meta($user_id, BASE . 'session_timestamp');
-	}
-
-	/******************************************************************
-	 *  CSRF PROTECTION
-	******************************************************************/
-	/**
-	 * Generate CSRF token for a user
-	 *
-	 * @param int $user_id User ID
-	 * @return string CSRF token
-	 */
-	protected function generateCSRFToken(int $user_id): string
-	{
-		$token = wp_generate_password(32, false);
-		set_transient(BASE . 'csrf_' . $user_id, $token, HOUR_IN_SECONDS);
-		return $token;
-	}
-
-	/**
-	 * Validate CSRF token from request header
-	 *
-	 * @param WP_REST_Request $request The REST request
-	 * @return bool True if valid or not required
-	 */
-	protected function validateCSRFToken(WP_REST_Request $request): bool
-	{
-		// Only for authenticated requests
-		if (!is_user_logged_in()) {
-			return true;
-		}
-
-		// Only for state-changing operations
-		if (in_array($request->get_method(), ['GET', 'HEAD', 'OPTIONS'])) {
-			return true;
-		}
-
-		$user_id = get_current_user_id();
-		$token = $request->get_header('X-CSRF-Token');
-		$stored = get_transient(BASE . 'csrf_' . $user_id);
-
-		if (empty($stored) || empty($token)) {
-			return false;
-		}
-
-		return hash_equals($stored, $token);
-	}
-
-	/**
-	 * Get current CSRF token for user (for frontend to use)
-	 *
-	 * @param int $user_id User ID
-	 * @return string|null CSRF token or null
-	 */
-	protected function getCSRFToken(int $user_id): ?string
-	{
-		$token = get_transient(BASE . 'csrf_' . $user_id);
-
-		if (!$token) {
-			$token = $this->generateCSRFToken($user_id);
-		}
-
-		return $token;
-	}
-
-	/***********************************************************
-	 * REQUEST SIGNATURE VERIFICATION
-	***********************************************************/
-	/**
-	 * Verify request signature for sensitive operations
-	 * Adds an additional layer of security for critical endpoints
-	 *
-	 * @param WP_REST_Request $request The REST request
-	 * @param array $sensitive_params Params to include in signature
-	 * @return bool True if valid signature
-	 */
-	protected function verifyRequestSignature(WP_REST_Request $request, array $sensitive_params): bool
-	{
-		$signature = $request->get_header('X-Request-Signature');
-		if (empty($signature)) {
-			return false;
-		}
-
-		// Get user's signing key (rotated on each login)
-		$user_id = get_current_user_id();
-		if (!$user_id) {
-			return false;
-		}
-
-		$signing_key = get_user_meta($user_id, BASE . 'signing_key', true);
-		if (empty($signing_key)) {
-			// Generate new key if missing
-			$signing_key = wp_generate_password(64, false);
-			update_user_meta($user_id, BASE . 'signing_key', $signing_key);
-		}
-
-		// Build signature from sensitive params
-		ksort($sensitive_params);
-		$message = json_encode($sensitive_params);
-		$expected = hash_hmac('sha256', $message, $signing_key);
-
-		return hash_equals($expected, $signature);
-	}
-
-	/**
-	 * Rotate signing key (call on login)
-	 *
-	 * @param int $user_id User ID
-	 * @return string New signing key
-	 */
-	protected function rotateSigningKey(int $user_id): string
-	{
-		$new_key = wp_generate_password(64, false);
-		update_user_meta($user_id, BASE . 'signing_key', $new_key);
-		return $new_key;
-	}
-	/**********************************************************
-	 * AUDIT LOGGING
-	**********************************************************/
-	/**
-	 * Log security-relevant events
-	 *
-	 * @param string $event Event name
-	 * @param array $data Additional event data
-	 * @return void
-	 */
-	protected function auditLog(string $event, array $data = []): void
-	{
-		// Add standard context
-		$context = array_merge($data, [
-			'timestamp' => current_time('mysql'),
-			'user_id' => get_current_user_id() ?: 0,
-			'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
-			'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
-		]);
-
-		// Use existing error handler
-		try {
-			Errors::getInstance()->log(
-				'security_audit',
-				$event,
-				$context,
-				'info'
-			);
-		} catch (Exception $e) {
-			// Fallback to error_log if handler fails
-			error_log("Security Audit: {$event} - " . json_encode($context));
-		}
-	}
-
-	/***************************************************************
-	 * SANITIZATION HELPERS
-	***************************************************************/
-	/**
-	 * Sanitize array of IDs
-	 *
-	 * @param array $ids Array of IDs
-	 * @return array Sanitized array of integers
-	 */
-	protected function sanitizeIDs(array $ids): array
-	{
-		return array_map('absint', array_filter($ids, function($id) {
-			return is_numeric($id) && $id > 0;
-		}));
-	}
-
-	/***************************************************************
-	 * RESPONSE HELPERS
-	***************************************************************/
-	/**
-	 * Return validation error response
-	 *
-	 * @param array $errors Validation errors (field => message)
-	 * @return WP_REST_Response 422 response with errors
-	 */
-	protected function validationError(array $errors): WP_REST_Response
-	{
-		return new WP_REST_Response([
-			'success' => false,
-			'message' => 'Validation failed',
-			'errors' => $errors
-		], 422);
-	}
-
-	/**
-	 * Return unauthorized error response
-	 *
-	 * @param string $message Error message
-	 * @return WP_REST_Response 401 response
-	 */
-	protected function unauthorized(string $message = 'Unauthorized'): WP_REST_Response
-	{
-		return new WP_REST_Response([
-			'success' => false,
-			'message' => $message
-		], 401);
-	}
-
-	/**
-	 * Return forbidden error response
-	 *
-	 * @param string $message Error message
-	 * @return WP_REST_Response 403 response
-	 */
-	protected function forbidden(string $message = 'Forbidden'): WP_REST_Response
-	{
-		return new WP_REST_Response([
-			'success' => false,
-			'message' => $message
-		], 403);
-	}
-
-	/**
-	 * Return not found error response
-	 *
-	 * @param string $message Error message
-	 * @return WP_REST_Response 404 response
-	 */
-	protected function notFound(string $message = 'Not found'): WP_REST_Response
-	{
-		return new WP_REST_Response([
-			'success' => false,
-			'message' => $message
-		], 404);
-	}
-
-	/*****************************************************************
-	 * CONCURRENT CHECKS
-	*****************************************************************/
-	/**
-	 * Prevent concurrent requests for the same operation
-	 * Useful for preventing double-submissions
-	 *
-	 * @param string $operation_key Unique key for this operation
-	 * @param int $lock_duration Lock duration in seconds
-	 * @return bool True if lock acquired, false if already locked
-	 */
-	protected function acquireOperationLock(string $operation_key, int $lock_duration = 5): bool
-	{
-		$lock_key = 'op_lock_' . md5($operation_key);
-
-		// Try to acquire lock
-		$locked = get_transient($lock_key);
-		if ($locked) {
-			return false; // Already locked
-		}
-
-		// Set lock
-		set_transient($lock_key, true, $lock_duration);
-		return true;
-	}
-
-	/**
-	 * Release operation lock
-	 *
-	 * @param string $operation_key Unique key for this operation
-	 * @return void
-	 */
-	protected function releaseOperationLock(string $operation_key): void
-	{
-		$lock_key = 'op_lock_' . md5($operation_key);
-		delete_transient($lock_key);
-	}
-
-	protected function verifyTurnstile(string $token): bool
-	{
-		if (!Site::hasIntegration('cloudflare') || !Cloudflare::getInstance()->isSetUp()) {
-			return true;
-		}
-
-		if (empty($token)) {
-			return false;
-		}
-
-		return Cloudflare::getInstance()->verifyTurnstile($token);
-	}
-}
diff --git a/inc/rest/routes/ContentTermsRoutes.php b/inc/rest/routes/ContentTermsRoutes.php
index dbc191c..c42060c 100644
--- a/inc/rest/routes/ContentTermsRoutes.php
+++ b/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
diff --git a/inc/rest/routes/ErrorRoutes.php b/inc/rest/routes/ErrorRoutes.php
index 824f4bd..6fc7c06 100644
--- a/inc/rest/routes/ErrorRoutes.php
+++ b/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']);
diff --git a/inc/rest/routes/FavouritesRoutes.php b/inc/rest/routes/FavouritesRoutes.php
index 78cd3e0..795ddeb 100644
--- a/inc/rest/routes/FavouritesRoutes.php
+++ b/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
 			]);
diff --git a/inc/rest/routes/FormRoutes.php b/inc/rest/routes/FormRoutes.php
index baf5b9f..8cf61b9 100644
--- a/inc/rest/routes/FormRoutes.php
+++ b/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()
 			]);
diff --git a/inc/rest/routes/ImporterRoutes.php b/inc/rest/routes/ImporterRoutes.php
index 682fef7..3753a4e 100644
--- a/inc/rest/routes/ImporterRoutes.php
+++ b/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']
 			]);
diff --git a/inc/rest/routes/IntegrationsHelcimRoutes.php b/inc/rest/routes/IntegrationsHelcimRoutes.php
index f735618..98b662b 100644
--- a/inc/rest/routes/IntegrationsHelcimRoutes.php
+++ b/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());
 		}
 	}
diff --git a/inc/rest/routes/IntegrationsRoutes.php b/inc/rest/routes/IntegrationsRoutes.php
index 8df07a2..838cdd5 100644
--- a/inc/rest/routes/IntegrationsRoutes.php
+++ b/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());
 		}
 	}
diff --git a/inc/rest/routes/IntegrationsSquareRoutes.php b/inc/rest/routes/IntegrationsSquareRoutes.php
index 6a5cbc9..2653fa7 100644
--- a/inc/rest/routes/IntegrationsSquareRoutes.php
+++ b/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']
 			]);
diff --git a/inc/rest/routes/LoginRoutes.php b/inc/rest/routes/LoginRoutes.php
index 030cda6..9ff7f51 100644
--- a/inc/rest/routes/LoginRoutes.php
+++ b/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');
+			]);
 		}
 	}
 
diff --git a/inc/rest/routes/NotificationsRoutes.php b/inc/rest/routes/NotificationsRoutes.php
index ac1ea1a..3d6d1bb 100644
--- a/inc/rest/routes/NotificationsRoutes.php
+++ b/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
 			]);
diff --git a/inc/rest/routes/TermRoutes.php b/inc/rest/routes/TermRoutes.php
index ce0f578..aa18144 100644
--- a/inc/rest/routes/TermRoutes.php
+++ b/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]
 			);
 
diff --git a/inc/rest/routes/UploadRoutes.php b/inc/rest/routes/UploadRoutes.php
index 4b3128e..6d0e1bf 100644
--- a/inc/rest/routes/UploadRoutes.php
+++ b/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());
 		}
 	}
diff --git a/inc/rest/routes/UserRoutes.php b/inc/rest/routes/UserRoutes.php
index f517a62..41bdbcf 100644
--- a/inc/rest/routes/UserRoutes.php
+++ b/inc/rest/routes/UserRoutes.php
@@ -1,4 +1,5 @@
 <?php
+namespace JVBase\rest\routes;
 
 use JVBase\Instance;
 use JVBase\managers\Cache;
diff --git a/jvb.php b/jvb.php
index e74a588..e87b324 100644
--- a/jvb.php
+++ b/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();
 	}

--
Gitblit v1.10.0