From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh

---
 inc/rest/routes/ContentRoutes.php | 1183 ++++++++++++----------------------------------------------
 1 files changed, 248 insertions(+), 935 deletions(-)

diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index 46c7d45..7d5ef42 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -2,46 +2,56 @@
 
 namespace JVBase\rest\routes;
 
-use JVBase\JVB;
-use JVBase\managers\queue\executors\ContentExecutor;
-use JVBase\managers\queue\TypeConfig;
-use JVBase\rest\RestRouteManager;
+use JVBase\Instance;
 use JVBase\managers\Cache;
-use JVBase\meta\MetaManager;
-use JVBase\utility\Features;
+use JVBase\managers\queue\executors\ContentExecutor;
+use JVBase\managers\queue\Queue;
+use JVBase\managers\queue\TypeConfig;
+use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
+use JVBase\rest\PermissionHandler;
+use JVBase\rest\Response;
+use JVBase\rest\Rest;
+use JVBase\rest\Route;
 use WP_Post;
 use WP_Query;
-use WP_Error;
 use WP_REST_Request;
 use WP_REST_Response;
-use Exception;
+use WP_Term;
+use WP_Term_Query;
 
 if (!defined('ABSPATH')) {
 	exit; // Exit if accessed directly
 }
 
-class ContentRoutes extends RestRouteManager
+class ContentRoutes extends Rest
 {
+	use Instance;
 	protected array $fields = [];
 	protected array $taxonomies = [];
-	protected MetaManager $meta;
 	protected string $post_type = '';
 	protected string $user_id = '';
 
 	//For Timeline-specific posts
 	protected array $timelineSharedFields = [];
 	protected array $timelineUniqueFields = [];
+	protected static ?string $action = 'dash-';
+	protected Meta $meta;
+	protected Cache $termCache;
 
 	public function __construct()
 	{
-		$this->cache_name = 'user_content_' . get_current_user_id();
+		$this->cacheName = 'user_content';
 		parent::__construct();
+		$this->cache = Cache::for($this->cacheName, 'post', DAY_IN_SECONDS);
+		$this->cache->user();
+		$this->termCache = Cache::for($this->cacheName.'_terms', 'term', DAY_IN_SECONDS);
+		$this->termCache->user();
+
+
 		if (JVB_TESTING) {
 			$this->cache->flush();
 		}
-
-		$this->action = 'dash-';
-		$this->operation_type = 'content_update';
 		add_action('init', [$this, 'registerContentExecutors'], 5);
 	}
 
@@ -50,7 +60,7 @@
 	 */
 	public function registerContentExecutors(): void
 	{
-		$registry = JVB()->queue()->registry();
+		$registry = Queue::getInstance()->registry();
 		$executor = new ContentExecutor();
 
 		// Content updates - chunked at 10 posts
@@ -60,10 +70,10 @@
 			chunkSize: 10
 		));
 
-		// Batch creation (from uploads)
-		$registry->register('batch_creation', new TypeConfig(
-			executor: $executor
-		));
+		// Batch creation (from uploads) TODO: I believe this is all handled by UploadExecutor
+//		$registry->register('batch_creation', new TypeConfig(
+//			executor: $executor
+//		));
 	}
 
 	/**
@@ -72,45 +82,41 @@
 	 */
 	public function registerRoutes(): void
 	{
-		// Base content endpoint
-		register_rest_route($this->namespace, "/content", [
-			[
-				'methods' => 'GET',
-				'callback' => [$this, 'handleContentRequest'],
-				'permission_callback' => [$this, 'checkPermission'],
-			],
-			[
-				'methods' => 'POST',
-				'callback' => [$this, 'handleContentUpdate'],
-				'permission_callback' => [$this, 'checkPermission']
-			]
-		]);
-
-		//TODO: consolidate create/batch in with create? I don't think we are ever creating a single item
-		register_rest_route($this->namespace, "/create", [
-			[
-				'methods' => 'POST',
-				'callback' => [$this, 'handleContentCreate'],
-				'permission_callback' => [$this, 'checkPermission']
-			]
-		]);
-		register_rest_route($this->namespace, "/create/batch", [
-			[
-				'methods' => 'POST',
-				'callback' => [$this, 'handleBatchCreation'],
-				'permission_callback' => [$this, 'checkPermission']
-			]
-		]);
+		Route::for('content')
+			->get([$this, 'getContent'])
+			->auth(PermissionHandler::combine(['user', 'nonce', ['actionNonce'=>'dash-']]))
+			->args([
+				'content' => 'string|required',
+				'status' => 'string|default:all',
+				'page' => 'integer|default:1|min:1',
+				'per_page' => 'integer|default:30|min:1|max:100',
+				'orderby' => 'string|enum:date,alphabetical|default:date',
+				'order' => 'string|enum:asc,desc|default:desc',
+				'search' => 'string',
+				'date-filter' => 'string',
+				'dateFrom' => 'string',
+				'dateTo' => 'string',
+			])
+			->rateLimit(30)
+			->post([$this, 'postContent'])
+			->auth(PermissionHandler::combine(['user', 'nonce', ['actionNonce'=>'dash-']]))
+			->rateLimit()
+			->args([
+				'posts' => 'required',
+				'content' => 'string',
+			])
+			->register();
 	}
 
 	protected function initTimelineFields(string $content): void
 	{
 		$content = jvbNoBase($content);
-		if (!Features::forContent($content)->has('is_timeline')) {
+
+		$config = Registrar::getInstance($content);
+		if (!$config || !$config->hasFeature('is_timeline')) {
 			return;
 		}
-		$config = Features::getConfig($content);
-		$this->fields = $config['fields'];
+		$this->fields = $config->getFields();
 
 		$this->timelineSharedFields = $this->getTimelineSharedFields($content);
 		array_unshift($this->timelineSharedFields, 'post_thumbnail');
@@ -123,11 +129,12 @@
 	public function getTimelineUniqueFields(string $content): array
 	{
 		$content = jvbNoBase($content);
-		if (!Features::forContent($content)->has('is_timeline')) {
+		$registrar = Registrar::getInstance($content);
+		if (!$registrar || !$registrar->hasFeature('is_timeline')) {
 			return [];
 		}
-		$config = Features::getConfig($content);
-		$allFields = $config['fields'];
+
+		$allFields = $registrar->getFields();
 
 		return array_keys(array_filter($allFields, function ($field) {
 			if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
@@ -140,17 +147,15 @@
 	public function getTimelineSharedFields(string $content): array
 	{
 		$content = jvbNoBase($content);
-		if (!Features::forContent($content)->has('is_timeline')) {
+		$registrar = Registrar::getInstance($content);
+		if (!$registrar || !$registrar->hasFeature('is_timeline')) {
 			return [];
 		}
-		$config = Features::getConfig($content);
-		if (!$config || empty($config)) {
-			return [];
-		}
-		$allFields = $config['fields'] ?? [];
+
+		$allFields = $registrar->getFields()??[];
 
 		return array_keys(array_filter($allFields, function ($field) {
-			if (!array_key_exists('for_all', $field) || $field['for_all'] === false) {
+			if (!array_key_exists('for_all', $field) || is_null($field['for_all']) || $field['for_all'] === false) {
 				return true;
 			}
 			return false;
@@ -163,86 +168,32 @@
 	 *
 	 * @return WP_REST_Response
 	 */
-	public function handleContentUpdate(WP_REST_Request $request): WP_REST_Response
+	public function postContent(WP_REST_Request $request): WP_REST_Response
 	{
 		$data = $request->get_params();
-		$user_id = $data['user'];
-
-		if (!$this->userCheck($user_id)) {
-			return new WP_REST_Response([
-				'success' => true,
-				'message' => 'You for real?'
-			]);
-		}
+		$user_id = get_current_user_id();
 
 		if (!array_key_exists('posts', $data) || !is_array($data['posts'])) {
-			return new WP_REST_Response([
-				'success' => true,
-				'message' => 'No posts found'
-			]);
+			return Response::success(['message'=>'No posts found in request']);
 		}
 
+
 		$count = count($data['posts']);
 		$operationId = $data['id'];
-		unset($data['user']);
 		unset($data['id']);
 
-		$queue = JVB()->queue();
+		error_log('[CONTENT]:'.print_r($data, true));
+		$queue = Queue::getInstance();
 		$queue->queueOperation(
 			'content_update',
 			$user_id,
 			$data,
 			[
-				'count' => $count,
-				'chunk_key' => 'posts',
-				'chunk_size' => 10,
 				'operation_id' => $operationId
 			]
 		);
-		return new WP_REST_Response([
-			'success' => true,
-			'message' => 'Queued for processing',
-			'operation' => $operationId
-		]);
-	}
 
-	/**
-	 * Handle content creation
-	 * @param WP_REST_Request $request
-	 *
-	 * @return WP_REST_Response
-	 */
-	public function handleContentCreate(WP_REST_Request $request): WP_REST_Response
-	{
-		$data = $request->get_json_params();
-		$user_id = $data['user'];
-
-		if (!isset($data['posts']) || !is_array($data['posts'])) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Invalid request format'
-			]);
-		}
-
-		$count = count($data['posts']);
-		$operationId = $data['id'];
-		unset($data['user']);
-		unset($data['id']);
-		JVB()->queue()->queueOperation(
-			'batch_creation',
-			$user_id,
-			$data,
-			[
-				'count' => $count,
-				'operation_id' => $operationId,
-			]
-		);
-
-		return new WP_REST_Response([
-			'success' => true,
-			'message' => 'Queued for processing',
-			'operation' => $operationId
-		]);
+		return Response::queued($operationId);
 	}
 
 
@@ -252,18 +203,29 @@
 	 *
 	 * @return WP_REST_Response
 	 */
-	public function handleContentRequest(WP_REST_Request $request): WP_REST_Response
+	public function getContent(WP_REST_Request $request): WP_REST_Response
 	{
 		$params = $request->get_params();
-		$user_id = $params['user'];
+		error_log('getContent::params '.print_r($params, true));
 
-		if (!$this->userCheck($user_id)) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'User does not match up. Are you a bot?',
-			]);
+		$registrar = Registrar::getInstance($params['content']);
+		switch ($registrar->getType()) {
+			case 'term':
+				return $this->getTerms($request, $params, $registrar);
+			case 'user':
+				//TODO maybe do something?
+				break;
+			case 'post':
+				return $this->getPosts($request, $params, $registrar);
 		}
 
+		return $this->error('Something went wrong, this does not appear to have a proper content type');
+	}
+
+	public function getPosts(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response
+	{
+		$user_id = $params['user'];
+
 		$post_status = $params['status'];
 		if ($post_status === 'all') {
 			$post_status = ['publish', 'draft'];
@@ -272,7 +234,6 @@
 		}
 		$post_type = str_replace('-', '_', jvbCheckBase($params['content']));
 
-
 		// Build query args
 		$args = [
 			'post_type' => $post_type,
@@ -283,13 +244,15 @@
 			'author' => $user_id,
 			'post_status' => $post_status
 		];
+
+
 		//Only top level posts for timeline types
-		if (Features::forContent($post_type)->has('is_timeline')) {
+		if ($registrar?->hasFeature('is_timeline')) {
 			$args['post_parent'] = 0;
 		}
 
 		//Calendar filters
-		if (Features::forContent($post_type)->has('is_calendar')) {
+		if ($registrar?->hasFeature('is_calendar')) {
 			$args = $this->applyCalendarFilters($args, $params);
 		}
 		$taxonomies = array_filter($params, function ($param) {
@@ -316,23 +279,14 @@
 			$args['s'] = sanitize_text_field($params['search']);
 		}
 
-
 		$key = $this->cache->generateKey($args);
-		// Check HTTP cache headers with the specific content type
-		$cache_check = $this->checkHeaders($request, $key);
-		if ($cache_check) {
-			return $cache_check;
-		}
-
-
-		$cache = $this->cache->get($key);
-		if ($cache) {
-			$response = new WP_REST_Response($cache);
-			return $this->addCacheHeaders($response);
+		$cached = $this->cache->get($key);
+		if ($cached) {
+			return $cached;
 		}
 
 		$this->post_type = jvbCheckBase($params['content'] ?? $params['type']);
-		// Only expand search to taxonomies if we're actually going to query
+
 		if (array_key_exists('s', $args)) {
 			$args = $this->applySearchFilters($args, $params);
 		}
@@ -340,22 +294,104 @@
 		// Run query
 		$query = new WP_Query($args);
 
-
-		$this->fields = jvbGetFields(str_replace('-', '_', $this->post_type));
+		$registrar = Registrar::getInstance($this->post_type);
+		$this->fields = $registrar->getFields()??[];
 		$this->taxonomies = $this->getTaxonomies($this->post_type);
-		$posts = array_map([$this, 'prepareItem'], $query->posts);
+
+		$posts = array_map([$this, 'preparePost'], $query->posts);
 
 		$data = [
 			'items' => $posts,
 			'total' => $query->found_posts,
-			'total_pages' => $query->max_num_pages
+			'total_pages' => $query->max_num_pages,
+			'has_more'	=> $args['paged']??1 < $query->max_num_pages,
 		];
 
 
 		$this->cache->set($key, $data);
 
-		$response = new WP_REST_Response($data);
-		return $this->addCacheHeaders($response);
+		return Response::success($data);
+	}
+	public function getTerms(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response
+	{
+		// Build query args
+		$args = [
+			'taxonomy' 		=> jvbCheckBase($params['content']),
+			'number'		=> $params['per_page'] ?? 30,
+			'orderby' 		=> 'name',
+			'order' 		=> 'DESC',
+			'hide_empty'	=> false,
+		];
+		$paged = $params['page']??1;
+		$args['page'] = $paged;
+		if ($paged > 1) {
+			$args['offset'] = ($paged-1) * $args['number'];
+		}
+
+		//TODO
+//		if (array_key_exists('taxonomies', $params)) {
+//			$args = $this->applyTaxonomyFilters($args, $params);
+//		}
+//		if (array_key_exists('date-filter', $params) || array_key_exists('dateFrom', $params)) {
+//			$args = $this->applyDateFilters($args, $params);
+//		}
+		if (array_key_exists('orderby', $params) || array_key_exists('order', $params)) {
+			$args = $this->applyOrderFilters($args, $params);
+		}
+
+		if (array_key_exists('search', $params)) {
+			$args['s'] = sanitize_text_field($params['search']);
+		}
+
+		$key = $this->cache->generateKey($args);
+		// Check HTTP cache headers with the specific content type
+//		$cache_check = $this->checkHeaders($request, $key);
+//		if ($cache_check) {
+//			return $cache_check;
+//		}
+
+		$cache = $this->cache->get($key);
+		if ($cache) {
+			return Response::success($cache);
+		}
+		$this->post_type = jvbCheckBase($params['content'] ?? $params['type']);
+		// Only expand search to taxonomies if we're actually going to query
+		if (array_key_exists('s', $args)) {
+			$args = $this->applySearchFilters($args, $params);
+		}
+
+		// Run query
+		$query = new WP_Term_Query($args);
+
+		$terms = $query->get_terms();
+		$data = [
+			'total'			=> 0,
+			'total_pages' 	=> 0,
+			'has_more' 		=> false
+		];
+
+		if (!is_wp_error($terms) && !empty($terms))
+		{
+			$total = get_terms([
+				'taxonomy'		=> $args['taxonomy'],
+				'hide_empty'	=> false,
+				'fields'		=> 'count'
+			]);
+			$data['total'] = $total;
+			$data['total_pages'] = max($total/$args['number'], 1);
+			$data['has_more'] = ($args['page'] * $args['number']) < $total;
+		} else {
+			$terms = [];
+		}
+
+		$this->fields = $registrar->getFields()??[];
+
+		$this->taxonomies = [];
+		$data['items'] =array_map([$this, 'prepareTerm'], $terms);
+
+		$this->cache->set($key, $data);
+
+		return Response::success($data);
 	}
 
 	protected function applySearchFilters(array $args, array $params): array
@@ -447,465 +483,23 @@
 	 */
 	protected function getTaxonomies(string $content): array
 	{
-		$taxonomy_for = jvbGlobalTaxonomyFor();
-		$out = [];
-		foreach ($taxonomy_for as $tax => $postTypes) {
-			if (in_array($content, $postTypes)) {
-				$out[BASE . $tax] = [
-					'label' => JVB_CONTENT[$content]['plural'],
-					'icon' => $tax,
-				];
-			}
+		$registrar = Registrar::getInstance($content);
+		if (!$registrar || $registrar->getType()!== 'post') {
+			return [];
 		}
-		return $out;
-	}
-
-	/**
-	 * Processes operation from queue
-	 * @param object $operation
-	 * @param array $data
-	 *
-	 * @return array
-	 */
-	protected function processBatches(object $operation, array $data): array
-	{
-		$this->user_id = $operation->user_id;
-		$posts = $data['posts'];
-
-		if (empty($posts)) {
-			return [
-				'success' => false,
-				'message' => 'No posts to update'
+		$out = [];
+		foreach ($registrar->registrar->taxonomies as $tax) {
+			$taxReg = Registrar::getInstance($tax);
+			$out[jvbCheckBase($tax)] = [
+				'label'	=> $taxReg->getPlural(),
+				'icon'	=> $taxReg->getIcon()??jvbDefaultIcon()
 			];
 		}
 
-		$results = [];
-
-		foreach ($posts as $ID => $post_data) {
-			if (Features::forContent($post_data['content'])->has('is_timeline') && array_key_exists('timeline', $post_data)) {
-				// Handle timeline posts - ensure we have a valid integer ID
-				$parent_id = (int)$ID;
-
-				// Skip if ID is invalid (0, 'null', etc would become 0)
-				if ($parent_id === 0) {
-					error_log('Invalid timeline parent ID: ' . $ID);
-					$results[$ID] = [
-						'success' => false,
-						'message' => 'Invalid parent post ID for timeline'
-					];
-					continue;
-				}
-
-				$results[$ID] = $this->processTimelinePost($parent_id, $post_data);
-				continue;
-			}
-			if (str_starts_with($ID, 'new')) {
-
-				error_log('New post detected. Creating... with: ' . print_r([
-						'post_author' => $this->user_id,
-						'post_type' => jvbCheckBase($post_data['content']),
-						'post_title' => $post_data['post_title'] ?? '',
-						'post_status' => $post_data['status'] ?? 'draft',
-					], true));
-				error_log('Recieved Data: ' . print_r($post_data, true));
-				$ID = wp_insert_post([
-					'post_author' => $this->user_id,
-					'post_type' => jvbCheckBase($post_data['content']),
-					'post_title' => $post_data['post_title'] ?? '',
-					'post_status' => $post_data['status'] ?? 'draft',
-				]);
-				if (!$ID || is_wp_error($ID)) {
-					$results[$ID] = [
-						'success' => false,
-						'message' => 'Couldn\'t Create Post'
-					];
-					continue;
-				}
-				$fields = jvbGetFields($post_data['content']);
-				$allowedFields = array_filter($post_data, function ($key) use ($fields) {
-					return array_key_exists($key, $fields);
-				}, ARRAY_FILTER_USE_KEY);
-
-				$meta = new MetaManager($ID, 'post');
-				$success = $meta->setAll($allowedFields);
-				$results[$ID] = [
-					'success' => $success
-				];
-			} else {
-				if (!$this->verifyOwnership($ID)) {
-					$results[$ID] = [
-						'success' => false,
-						'message' => 'No permission to modify this post'
-					];
-					continue;
-				}
-				error_log('Saving post data: ' . print_r($post_data, true));
-
-				if (array_key_exists('post_status', $post_data)) {
-					switch ($post_data['post_status']) {
-						case 'publish':
-							unset($post_data['post_status']);
-							if (user_can($this->user_id, 'manage_options') || user_can($this->user_id, 'skip_moderation')) {
-								$result = wp_update_post(['ID' => $ID, 'post_status' => 'publish']);
-							}
-							break;
-						case 'draft':
-							$result = wp_update_post([
-								'ID' => $ID,
-								'post_status' => 'draft'
-							]);
-							break;
-						case 'trash':
-							$result = wp_trash_post($ID);
-							break;
-						case 'delete':
-							$result = wp_delete_post($ID, true);
-							return ['success' => (bool)$result];
-					}
-				}
-				error_log('Updating data: ' . print_r($post_data, true));
-				$fields = jvbGetFields($post_data['content']);
-				$allowedFields = array_filter($post_data, function ($key) use ($fields) {
-					return array_key_exists($key, $fields);
-				}, ARRAY_FILTER_USE_KEY);
-
-				error_log('Allowed Fields: ' . print_r($allowedFields, true));
-				$meta = new MetaManager($ID, 'post');
-				$success = $meta->setAll($allowedFields);
-				$results[$ID] = [
-					'success' => $success
-				];
-
-			}
-		}
-
-		if (jvbSiteHasNotifications()) {
-			$this->notifications = JVB()->notification();
-			$this->notifications->addNotification(
-				$this->user_id,
-				'content_update_complete',
-				null,
-				'Content updates completed!'
-			);
-		}
-
-
-		return [
-			'success' => true,
-			'result' => $results
-		];
+		return $out;
 	}
 
-	/**
-	 * Extracts the postdata for timeline post child posts from the pseudo-repeater element
-	 * @param int $parent_id
-	 * @param array $post_data
-	 * @return array|true[]
-	 */
-	protected function processTimelinePost(int $parent_id, array $post_data): array
-	{
-		if (!$this->verifyOwnership($parent_id)) {
-			return ['success' => false, 'message' => 'No permission'];
-		}
 
-		error_log('[Processing Timeline Post...');
-
-		$ignore = ['content', 'user'];
-		$this->fields = jvbGetFields($post_data['content']);
-		$this->initTimelineFields($post_data['content']);
-
-		// Get parent post details
-		$parent_post = get_post($parent_id);
-		$parent_title = $parent_post->post_title;
-		$parent_is_published = ($parent_post->post_status === 'publish');
-
-		// Extract shared data from top level (excluding post_thumbnail which is unique per post)
-		$sharedData = array_filter($post_data, function ($key) use ($ignore) {
-			return in_array($key, $this->timelineSharedFields)
-				&& !in_array($key, $ignore)
-				&& $key !== 'post_thumbnail';
-		}, ARRAY_FILTER_USE_KEY);
-
-		// If no shared post_title at top level, extract from first timeline entry
-		if (!isset($sharedData['post_title']) && isset($post_data['timeline'][0]['post_title'])) {
-			$sharedData['post_title'] = $post_data['timeline'][0]['post_title'];
-		}
-		$clearParent = false;
-		if (array_key_exists('timeline', $post_data) && is_array($post_data['timeline'])) {
-			// Remove post_title and post_thumbnail from shared taxonomies
-			$sharedTaxonomies = array_filter($sharedData, function ($key) {
-				return $key !== 'post_title' && $key !== 'post_thumbnail';
-			}, ARRAY_FILTER_USE_KEY);
-
-			// Ensure the parent post exists and is still first in the array
-			$index = array_search((string)$parent_id, array_column($post_data['timeline'], 'id'));
-
-			if ($index === false) {
-				return [
-					'success' => false,
-					'message' => 'Missing parent id. This should not have happened'
-				];
-			}
-
-			if ($index !== 0) {
-				$new_parent_id = $post_data['timeline'][0]['id'];
-
-				if (is_numeric($new_parent_id) && (int)$new_parent_id > 0) {
-					$new_parent_id = (int)$new_parent_id;
-					wp_update_post([
-						'ID' => $new_parent_id,
-						'post_parent' => 0
-					]);
-
-					wp_update_post([
-						'ID' => $parent_id,
-						'post_parent' => $new_parent_id
-					]);
-
-					$existing_children = get_children([
-						'post_parent' => $parent_id,
-						'fields' => 'ids'
-					]);
-
-					foreach ($existing_children as $child_id) {
-						if ($child_id !== $new_parent_id) {
-							wp_update_post([
-								'ID' => $child_id,
-								'post_parent' => $new_parent_id
-							]);
-						}
-					}
-
-					// Update parent references
-					$parent_id = $new_parent_id;
-					$parent_post = get_post($parent_id);
-					$parent_title = $parent_post->post_title;
-					$parent_is_published = ($parent_post->post_status === 'publish');
-				} else {
-					$item = $post_data['timeline'][$index];
-					unset($post_data['timeline'][$index]);
-					array_unshift($post_data['timeline'], $item);
-				}
-			}
-
-			$errors = [];
-			$success = [];
-			$existing_children = get_children([
-				'post_parent' => $parent_id,
-				'orderby' => 'menu_order',
-				'post_status' => ['publish', 'draft'],
-				'fields' => 'ids'
-			]);
-
-			$prevDate = null;
-			$latest_date = null;
-			$earliest_date = null;
-			foreach ($post_data['timeline'] as $order => $timeline) {
-				// Get unique fields for this specific timeline entry
-				$allowedFields = array_filter($timeline, function ($key) use ($ignore) {
-					return in_array($key, $this->timelineUniqueFields) && !in_array($key, $ignore);
-				}, ARRAY_FILTER_USE_KEY);
-
-				// Determine the post title
-				$is_parent = ((int)$timeline['id'] === $parent_id);
-				$provided_title = $timeline['post_title'] ?? '';
-				$auto_generated_pattern = '/^.+Treatment #?\d+$/'; // Matches "Title - Treatment #1" or "Title - Treatment 1"
-
-				if ($is_parent) {
-					// Parent keeps its own title or uses shared title
-					$allowedFields['post_title'] = $provided_title ?: ($sharedData['post_title'] ?? $parent_title);
-				} else {
-					// For child posts, auto-generate if:
-					// 1. No title provided, OR
-					// 2. Title matches auto-generated pattern (meaning it wasn't customized)
-					if (empty($provided_title) || preg_match($auto_generated_pattern, $provided_title)) {
-						$allowedFields['post_title'] = 'Treatment ' . $order;
-					} else {
-						// Keep custom title
-						$allowedFields['post_title'] = $provided_title;
-					}
-				}
-
-				// Merge with shared taxonomies AFTER setting unique fields
-				$allowedFields = array_merge($sharedTaxonomies, $allowedFields);
-
-				// Handle post creation if needed
-				if (!array_key_exists('id', $timeline) || !is_numeric($timeline['id'])) {
-					$newChild = wp_insert_post([
-						'post_author' => $this->user_id,
-						'post_type' => jvbCheckBase($post_data['content']),
-						'post_title' => $allowedFields['post_title'],
-						'post_parent' => $parent_id,
-						'menu_order' => $order,
-						'post_status' => $parent_is_published ? 'publish' : 'draft'
-					]);
-					if (!$newChild || is_wp_error($newChild)) {
-						$errors[] = [
-							'message' => 'Could not create child post',
-							'data' => $timeline
-						];
-						continue;
-					}
-					$timeline['id'] = $newChild;
-				}
-
-				if (in_array((int)$timeline['id'], $existing_children)) {
-					unset($existing_children[array_search((int)$timeline['id'], $existing_children)]);
-				}
-
-				// Update post status and menu order
-				$post_updates = ['ID' => $timeline['id']];
-
-				if (!$is_parent) {
-					$post_updates['menu_order'] = $order;
-
-					// Auto-publish child if parent is published
-					if ($parent_is_published) {
-						$current_post = get_post($timeline['id']);
-						if ($current_post && $current_post->post_status !== 'publish') {
-							$post_updates['post_status'] = 'publish';
-						}
-					}
-				}
-
-				if (count($post_updates) > 1) {
-					$result = wp_update_post($post_updates);
-					error_log('Updated post ' . $timeline['id'] . ' with: ' . print_r($post_updates, true) . ' Result: ' . $result);
-					$clearParent = true;
-				}
-
-				// Update metadata
-				$meta = new MetaManager($timeline['id'], 'post');
-				$oldValues = $meta->getAll(array_keys($allowedFields));
-
-//				// Set number taxonomy to menu_order (always update for reordering)
-//				if (!$is_parent) {
-//					$number_value = $order;
-//					$term = get_term_by('name', (string)$number_value, BASE . 'number');
-//					if (!$term) {
-//						$result = wp_insert_term((string)$number_value, BASE . 'number');
-//						if ($result && !is_wp_error($result)) {
-//							$term = $result['term_id'];
-//						}
-//					} else {
-//						$term = $term->term_id;
-//					}
-//					$allowedFields['number'] = $term;
-//				}
-
-				// Auto-timeline logic
-				if ($prevDate) {
-					$newDate = array_key_exists('date', $oldValues) ? $oldValues['date'] : ((array_key_exists('date', $allowedFields)) ? $allowedFields['date'] : null);
-					if ($newDate) {
-						$date1 = new \DateTime($prevDate);
-						$date2 = new \DateTime($newDate);
-						$weeks = floor($date1->diff($date2)->days / 7);
-						if ($weeks > 0) {
-							$termToCheck = $weeks . ' Weeks';
-							$term = get_term_by('name', $termToCheck, BASE . 'timeline');
-							if (!$term) {
-								$result = wp_insert_term($termToCheck, BASE . 'timeline');
-								if ($result && !is_wp_error($result)) {
-									$term = $result['term_id'];
-								}
-							} else {
-								$term = $term->term_id;
-							}
-							$allowedFields['timeline'] = $term;
-						}
-					}
-				}
-				$prevDate = array_key_exists('date', $oldValues) ? $oldValues['date'] : ((array_key_exists('date', $allowedFields)) ? $allowedFields['date'] : $prevDate);
-
-				$updateValues = array_filter($allowedFields, function ($value, $key) use ($oldValues) {
-					return (!array_key_exists($key, $oldValues) || $value !== $oldValues[$key]);
-				}, ARRAY_FILTER_USE_BOTH);
-
-
-				$meta->setAll($updateValues);
-				$timeline['id'] = (int)$timeline['id'];
-
-				$success[] = $timeline['id'];
-			}
-		}
-
-		// Delete any remaining children that no longer exist
-		if (!empty($existing_children)) {
-			foreach ($existing_children as $ID) {
-				wp_delete_post($ID);
-			}
-		}
-
-		if ($clearParent) {
-			$this->cache->flush();
-			Cache::onPostChange($parent_id, $parent_post);
-		}
-
-
-		return ['success' => true, 'data' => [
-			'success' => $success,
-			'errors' => $errors
-		]];
-	}
-
-	/**
-	 * Handle batch content creation from uploads
-	 * @param WP_REST_Request $request
-	 *
-	 * @return WP_REST_Response
-	 */
-	public function handleBatchCreation(WP_REST_Request $request): WP_REST_Response
-	{
-		//Operation has two parts
-		//First, queue image processing
-		//Then queue post creation from the stored IDs, depending on mode
-		//if direct, each image becomes a new post
-		//if selection, each group becomes its own post,
-		// and ungrouped items each become their own post
-		if (!isset($_FILES['files'])) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'No files uploaded...',
-			]);
-		}
-
-		$data = $request->get_params();
-
-
-		$user_id = $data['user'];
-		if (!$this->userCheck($user_id)) {
-			return new WP_REST_Response([
-				'success' => 'false',
-				'message' => 'Invalid user match... are you a bot?'
-			]);
-		}
-		$operation_id = $data['id'];
-		$response = new WP_REST_Response([
-			'success' => true,
-			'message' => 'Successfully sent to server. Added to queue.',
-			'operation_id' => $operation_id,
-			'status' => 'pending'
-		]);
-		$this->queue = JVB()->queue();
-		JVB()->routes('uploads')->handleUploadRequest($request, false);
-		$this->queue->queueOperation(
-			'batch_creation',
-			$user_id,
-			[
-				'content' => $request->get_param('content'),
-				'mode' => $request->get_param('mode') ?: 'direct',
-				'files_data' => $request->get_param('files_data')
-			],
-			[
-				'operation_id' => $operation_id,
-				'priority' => 'high',
-				'notification' => true,
-				'depends_on' => $operation_id . '_upload'
-			]
-		);
-
-		return $response;
-	}
 
 	/**
 	 * Generates a post title, based on content type
@@ -926,13 +520,15 @@
 	 *
 	 * @return array
 	 */
-	protected function prepareItem(WP_Post $post, bool $skip = false, bool $fields = true): array
+	protected function preparePost(WP_Post $post, bool $skip = false, bool $fields = true): array
 	{
-		if (!$skip && Features::forContent($post->post_type)->has('is_timeline')) {
+		$registrar = Registrar::getInstance($post->post_type);
+		if (!$skip && $registrar && $registrar->hasFeature('is_timeline')) {
 			$this->initTimelineFields($post->post_type);
 			return $this->formatTimeline($post);
 		}
-		$this->meta = new MetaManager($post->ID, 'post');
+		$this->meta = Meta::forPost($post->ID);
+		$fields = ($fields) ? $this->meta->getAll() : [];
 		$data = [
 			'id' => $post->ID,
 			'title' => $post->post_title,
@@ -941,371 +537,88 @@
 			'modified' => $post->post_modified,
 			'thumbnail' => get_the_post_thumbnail_url($post->ID),
 			'alt' => get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true),
-			'icon' => $this->post_type,
+			'icon' => $registrar->getIcon(),
 			'taxonomies' => [],
-			'fields' => ($fields) ? $this->meta->getAll() : [],
+			'fields' => $fields,
 			'images' => [],
 		];
 
-		// Add taxonomy terms
-		foreach ($this->taxonomies as $taxonomy => $options) {
-			$tax = str_replace(BASE, '', $taxonomy);
-			$terms = wp_get_object_terms(
-				$post->ID,
-				$taxonomy,
-				['fields' => 'id=>name']
-			);
-			$data['taxonomies'][$tax] = [
-				'terms' => (is_wp_error($terms)) ? [] : $terms,
-				'name' => $options['label'],
-				'icon' => $tax
-			];
-		}
-
-		$images = $this->extractImages();
-
-
+		$images = $this->extractImages($fields, $this->meta);
 		if (!empty($images)) {
 			$data['images'] = $images;
 		}
 
+
+		$taxonomies = $this->extractTerms($fields, $this->meta);
+		if (!empty($taxonomies)) {
+			$data['taxonomies'] = $taxonomies;
+		}
 		return $data;
 	}
 
-	protected function extractImages(array $fields = []): array
+	/**
+	 * @param WP_Term $post the post object
+	 *
+	 * @return array
+	 */
+	protected function prepareTerm(WP_Term $post, bool $fields = true): array
 	{
-		//Extract images
-		$images = [];
-		$get = [];
-		$fields = (empty($fields)) ? $this->fields : $fields;
-		foreach ($fields as $field => $config) {
-			if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') {
-				$get[] = $field;
-			}
+		$registrar = Registrar::getInstance($post->taxonomy);
+
+		$this->meta = Meta::forTerm($post->term_id);
+		$fields = ($fields) ? $this->meta->getAll() : [];
+		$data = [
+			'id' => $post->term_id,
+			'title' => $post->name,
+			'date' => $fields['created_date']??'',
+			'modified' => $fields['modified_date']??'',
+			'thumbnail' => '',
+			'icon' => $registrar->getIcon(),
+			'taxonomies' => [],
+			'fields' => $fields,
+			'images' => [],
+		];
+
+		$images = $this->extractImages($fields, $this->meta);
+		if (!empty($images)) {
+			$data['images'] = $images;
 		}
 
-		if (!empty($get)) {
-			$allImages = $this->meta->getAll($get);
-			foreach ($allImages as $k => $imgs) {
-				$temp = explode(',', $imgs);
-				foreach ($temp as $img) {
-					if (is_numeric($img) && !array_key_exists($img, $images)) {
-						$images[$img] = jvbImageData((int)$img);
-					}
-				}
-			}
+		$taxonomies = $this->extractTerms($fields, $this->meta);
+		if (!empty($taxonomies)) {
+			$data['taxonomies'] = $taxonomies;
 		}
-		return $images;
+
+		error_log('Term data: '.print_r($data, true));
+		return $data;
 	}
 
+
 	public function formatTimeline(WP_Post $post): array
 	{
-		$item = $this->prepareItem($post, true, false);
+		$item = $this->preparePost($post, true, false);
 		//Step 1: Get the fields that apply to all posts
-		$mainMeta = new MetaManager($post->ID, 'post');
+		$mainMeta = Meta::forPost($post->ID);
 		$item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
 
 		//Step 2: Get the fields for each individual posts
-		$children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish', 'draft'], 'fields' => 'ids']);
+		$children = jvbTimelinePoints($post->ID, $post->post_type,['publish', 'draft']);
 		array_unshift($children, $post->ID);
 
 		$subFields = [];
 		$images = [];
 		foreach ($children as $child) {
-			$meta = new MetaManager($child, 'post');
+			$meta = Meta::forPost($child);
 			$f = $meta->getAll($this->timelineUniqueFields);
 			$f = ['id' => $child] + $f;
 			$subFields[] = $f;
 
 			$images[$f['post_thumbnail']] = jvbImageData((int)$f['post_thumbnail']);
 		}
-		$item['fields']['timeline'] = $subFields;
+		$item['fields']['timeline_gallery'] = $subFields;
 		$item['images'] = $item['images'] + $images;
-		$item['number'] = $mainMeta->getValue('number');
+		$item['number'] = $mainMeta->get('number');
 
 		return $item;
 	}
-
-	/**
-	 * Builds the taxonomy query
-	 * @param array $taxonomies
-	 *
-	 * @return array|string[]
-	 */
-	protected function buildTaxQuery(array $taxonomies): array
-	{
-		$tax_query = [];
-		error_log('Taxonomies in query: ' . print_r($taxonomies, true));
-
-		foreach ($taxonomies as $taxonomy => $terms) {
-			if (!empty($terms)) {
-				$tax_query[] = [
-					'taxonomy' => jvbCheckBase($taxonomy),
-					'field' => 'term_id',
-					'terms' => array_map('absint', (array)$terms)
-				];
-			}
-		}
-
-
-		return count($tax_query) > 1
-			? array_merge(['relation' => 'AND'], $tax_query)
-			: $tax_query;
-	}
-
-	/**
-	 * Builds the date query
-	 * @param array $date_params
-	 *
-	 * @return array
-	 */
-	protected function buildDateQuery(array $date_params): array
-	{
-		$query = [];
-
-		if (!empty($date_params['after'])) {
-			$query['after'] = sanitize_text_field($date_params['after']);
-		}
-
-		if (!empty($date_params['before'])) {
-			$query['before'] = sanitize_text_field($date_params['before']);
-		}
-
-		if (isset($date_params['inclusive'])) {
-			$query['inclusive'] = (bool)$date_params['inclusive'];
-		}
-
-		return empty($query) ? [] : [$query];
-	}
-
-	/**
-	 * @param int $post_id
-	 *
-	 * @return bool
-	 */
-	protected function verifyOwnership(int $post_id): bool
-	{
-		$post = get_post($post_id);
-		return $post && $post->post_author == $this->user_id;
-	}
-
-	/**
-	 * Processes operation from Operation Queue
-	 * @param WP_Error|array $result
-	 * @param object $operation
-	 * @param array $data
-	 *
-	 * @return array|WP_Error
-	 */
-	public function processOperation(WP_Error|array $result, object $operation, array $data): array|WP_Error
-	{
-		if ($operation->type === 'batch_creation') {
-			$JVB = JVB();
-			$queue = $JVB->queue();
-
-			$images = $queue->getOperationValue($operation->id . '_upload', 'result') ?? false;
-
-			$this->user_id = $operation->user_id;
-			$this->post_type = BASE . $data['content'];
-			try {
-				$results = [];
-				if ($images) {
-					if ($data['mode'] == 'selection') {
-						$total = count($images);
-						foreach ($images as $group => $files) {
-							$settings = json_decode($data['files_data'][$group]);
-
-							switch ($settings->type) {
-								case 'group':
-									$featuredIndex = $settings->metadata->featuredFile ?? 0;
-									$title = $settings->metadata->title ?? $this->generatePostTitle($data['content']);
-									$new = wp_insert_post([
-										'post_type' => BASE . $data['content'],
-										'post_title' => $title,
-										'post_status' => 'draft',
-										'post_author' => $operation->user_id
-									]);
-									if ($new && !is_wp_error($new)) {
-										set_post_thumbnail($new, $files[$featuredIndex]['attachment_id']);
-										unset($files[$featuredIndex]);
-										if (!empty($files)) {
-											$meta = new MetaManager($new, 'post');
-											$IDs = array_column($files, 'attachment_id');
-											$meta->updateValue('gallery', implode(',', $IDs));
-										}
-										$results[] = $new;
-//                                        $queue->updateOperationProgress($operation->id, $group + 1, $total);
-									}
-									break;
-								default:
-									foreach ($files as $img) {
-										$new = wp_insert_post([
-											'post_type' => BASE . $data['content'],
-											'post_title' => $this->generatePostTitle($data['content']),
-											'post_status' => 'draft',
-											'post_author' => $operation->user_id
-										]);
-
-										if ($new && !is_wp_error($new)) {
-											set_post_thumbnail($new, $img['attachment_id']);
-											$results[] = $new;
-//                                            $queue->updateOperationProgress($operation->id, $group + 1, $total);
-										}
-									}
-									break;
-							}
-						}
-					} else {
-						$total = count($images);
-						foreach ($images as $key => $img) {
-							$new = wp_insert_post([
-								'post_type' => BASE . $data['content'],
-								'post_title' => $this->generatePostTitle($data['content']),
-								'post_status' => 'draft',
-								'post_author' => $operation->user_id
-							]);
-							if ($new && !is_wp_error($new)) {
-								set_post_thumbnail($new, $img['attachment_id']);
-							}
-							$results[] = $new;
-//                            $queue->updateOperationProgress($operation->id, $key + 1, $total);
-						}
-					}
-				}
-
-				return [
-					'success' => true,
-					'result' => $results
-				];
-			} catch (Exception $e) {
-				$JVB->error()->log(
-					'[ContentRoutes]:processOperation',
-					$e->getMessage()
-				);
-			}
-
-			return $results;
-		} elseif ($operation->type == 'content_update') {
-			$result = $this->processBatches($operation, $data);
-		}
-
-		return $result;
-	}
-	// Add to ContentRoutes.php
-
-	/**
-	 * One-time migration: Set latest_date meta for all timeline posts
-	 * Call this once via WP-CLI or a temporary admin page
-	 *
-	 * Usage: add_action('admin_init', function() {
-	 *     if (current_user_can('manage_options')) {
-	 *         JVB()->routes('content')->migrateTimelineLatestDates();
-	 *     }
-	 * });
-	 */
-	public function migrateTimelineLatestDates(): array
-	{
-		global $wpdb;
-
-		$results = [
-			'processed' => 0,
-			'updated' => 0,
-			'skipped' => 0,
-			'errors' => []
-		];
-
-		// Get all timeline post types
-		$timeline_types = [];
-		foreach (JVB_CONTENT as $type => $config) {
-			if (Features::forContent($type)->has('is_timeline')) {
-				$timeline_types[] = BASE . $type;
-			}
-		}
-
-		if (empty($timeline_types)) {
-			return $results;
-		}
-
-		// Get all parent timeline posts
-		$args = [
-			'post_type' => $timeline_types,
-			'post_status' => ['publish', 'draft'],
-			'post_parent' => 0,
-			'posts_per_page' => -1,
-			'fields' => 'ids'
-		];
-
-		$parent_ids = get_posts($args);
-
-		foreach ($parent_ids as $parent_id) {
-			$results['processed']++;
-
-			try {
-				// Get all children including the parent
-				$children = get_children([
-					'post_parent' => $parent_id,
-					'post_status' => ['publish', 'draft'],
-					'orderby' => 'menu_order',
-					'order' => 'ASC',
-					'fields' => 'ids'
-				]);
-
-				// Add parent to the list
-				array_unshift($children, $parent_id);
-
-				// Find latest date among all posts
-				$latest_timestamp = 0;
-
-				foreach ($children as $post_id) {
-					$date = get_post_meta($post_id, BASE . 'date', true);
-
-					if ($date) {
-						$timestamp = strtotime($date);
-						if ($timestamp > $latest_timestamp) {
-							$latest_timestamp = $timestamp;
-						}
-					}
-				}
-
-				// Update parent with latest date
-				if ($latest_timestamp > 0) {
-					update_post_meta($parent_id, BASE . 'latest_date', $latest_timestamp);
-					$results['updated']++;
-					error_log("Updated post {$parent_id} with latest_date: {$latest_timestamp}");
-				} else {
-					// Fallback to parent post's post_date
-					$parent_post = get_post($parent_id);
-					$fallback_timestamp = strtotime($parent_post->post_date);
-
-					if ($fallback_timestamp > 0) {
-						update_post_meta($parent_id, BASE . 'latest_date', $fallback_timestamp);
-						$results['updated']++;
-						error_log("Updated post {$parent_id} with fallback latest_date: {$fallback_timestamp} (from post_date)");
-					} else {
-						$results['skipped']++;
-						error_log("No dates found for post {$parent_id}");
-					}
-				}
-
-			} catch (Exception $e) {
-				$results['errors'][] = [
-					'post_id' => $parent_id,
-					'error' => $e->getMessage()
-				];
-			}
-		}
-
-		error_log('Timeline migration complete: ' . print_r($results, true));
-		return $results;
-	}
 }
-
-
-//add_action('init', function() {
-////	delete_option('jvb_timeline_migrated');
-//	if (get_option('jvb_timeline_migrated')) {
-//		return;
-//	}
-//	JVB()->routes('content')->migrateTimelineLatestDates();
-//	update_option('jvb_timeline_migrated', true);
-//});

--
Gitblit v1.10.0