From 75a097a018a0090f5902758353c578fce4aa2a25 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sat, 23 May 2026 18:43:42 +0000
Subject: [PATCH] =CustomBlocks.php overhaul relatively complete. Also refactored the gallery in gallery.min.js and the jvbRenderGallery.

---
 inc/managers/queue/executors/UploadExecutor.php |  596 ++++++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 428 insertions(+), 168 deletions(-)

diff --git a/inc/managers/queue/executors/UploadExecutor.php b/inc/managers/queue/executors/UploadExecutor.php
index 10accf8..5f244d6 100644
--- a/inc/managers/queue/executors/UploadExecutor.php
+++ b/inc/managers/queue/executors/UploadExecutor.php
@@ -2,10 +2,13 @@
 namespace JVBase\managers\queue\executors;
 
 use JVBase\managers\queue\{Executor, Operation, Progress, Result};
+use JVBase\managers\Cache;
 use JVBase\managers\UploadManager;
-use JVBase\meta\MetaManager;
+use JVBase\meta\Meta;
 use Exception;
-use JVBase\utility\Features;
+use JVBase\registrar\Registrar;
+use JVBase\base\Site;
+use WP_Error;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -14,7 +17,7 @@
 /**
  * Executor for upload-related queue operations.
  * Handles: image_upload, video_upload, document_upload,
- *          update_metadata, temporary_cleanup, attach_upload_to_content, process_upload_groups
+ *          update_image_meta, temporary_cleanup, attach_upload_to_content, process_upload_groups
  */
 final class UploadExecutor implements Executor
 {
@@ -22,7 +25,7 @@
 		'image_upload',
 		'video_upload',
 		'document_upload',
-		'update_metadata',
+		'update_image_meta',
 		'temporary_cleanup',
 		'attach_upload_to_content',
 		'process_upload_groups'
@@ -41,7 +44,7 @@
 				'image_upload'            => $this->processFileUpload($operation, $data, 'image', $progress),
 				'video_upload'            => $this->processFileUpload($operation, $data, 'video', $progress),
 				'document_upload'         => $this->processFileUpload($operation, $data, 'document', $progress),
-				'update_metadata'         => $this->processMetadataUpdate($operation, $data, $progress),
+				'update_image_meta'         => $this->processMetaUpdate($operation, $data, $progress),
 				'temporary_cleanup'       => $this->processTemporaryCleanup($operation, $data, $progress),
 				'attach_upload_to_content'=> $this->processAttachToContent($operation, $data, $progress),
 				'process_upload_groups'   => $this->processUploadGroups($operation, $data, $progress),
@@ -74,12 +77,16 @@
 		$uploader = new UploadManager();
 		$processedResults = [];
 		$errors = [];
+		$uploadIds = [];
 
 		$securedFiles = $data['secured_files'] ?? [];
 
+		// Phase 1: File I/O — no DB, no transactions
+		$prepared = [];
 		foreach ($securedFiles as $securedFile) {
+			$uploadIds[] = $securedFile['upload_id'];
 			try {
-				$result = $uploader->processUpload(
+				$prepared[$securedFile['upload_id']] = $uploader->prepareFile(
 					$securedFile['temp_path'],
 					[
 						'file_type'     => $fileType,
@@ -91,123 +98,245 @@
 						'content'       => $data['content'] ?? '',
 					]
 				);
-
-				if (!is_wp_error($result)) {
-					$standardized = [
-						'attachment_id' => $result['attachment_id'],
-						'url'           => $result['url'],
-						'file'          => $result['file'],
-						'upload_id'     => $securedFile['upload_id'] ?? null,
-					];
-
-					if ($standardized['upload_id']) {
-						$processedResults[$standardized['upload_id']] = $standardized;
-					} else {
-						$processedResults[] = $standardized;
-					}
-
-					// Apply frontend metadata if provided
-					if (!empty($securedFile['metadata'])) {
-						$this->applyMeta($standardized['attachment_id'], $securedFile['metadata']);
-					}
-
-					$progress->advance(1);
-				} else {
-					$progress->failItem($securedFile, $result->get_error_message());
-				}
-
 			} catch (Exception $e) {
-				$progress->failItem($securedFile, $e->getMessage());
+				$progress->failItem($securedFile['upload_id'], $e->getMessage());
 				$errors[] = $e->getMessage();
 			}
 		}
 
-		// Handle destination (meta, post, post_group)
-		$this->handleUploadDestination($data, $processedResults);
+		// Phase 2: DB registration — quick per-file writes
+		foreach ($prepared as $uploadId => $fileInfo) {
+			try {
+				$result = $uploader->registerFile($fileInfo);
 
-		// Cleanup temp files
+				if (!$result['success']) {
+					$progress->failItem($uploadId, 'Registration failed');
+					$errors[] = "Failed to register {$uploadId}";
+					continue;
+				}
+
+				$processedResults[$uploadId] = [
+					'upload_id'     => $uploadId,
+					'attachment_id' => $result['attachment_id'] ?? 0,
+					'url'           => $result['url'] ?? '',
+					'sizes'         => $result['sizes'] ?? [],
+				];
+
+				// Apply frontend metadata if provided
+				$securedFile = $this->findSecuredFile($securedFiles, $uploadId);
+				if ($securedFile && !empty($securedFile['metadata'])) {
+					$this->applyMeta($result['attachment_id'], $securedFile['metadata']);
+				}
+
+				$progress->advance();
+
+			} catch (Exception $e) {
+				$progress->failItem($uploadId, $e->getMessage());
+				$errors[] = $e->getMessage();
+			}
+		}
+
+		// Destination + cleanup unchanged
+		$this->handleUploadDestination($data, $processedResults);
 		$this->cleanupTempFiles($securedFiles, $operation->userId);
 
-		$outcome = 'success';
-		if (!empty($operation->failedItems)) {
-			$outcome = count($operation->failedItems) === count($securedFiles) ? 'failed' : 'partial';
+		$outcome = count($processedResults) > 0 ? 'success' : 'failed';
+		if (count($processedResults) > 0 && !empty($errors)) {
+			$outcome = 'partial';
 		}
 
 		return new Result(
 			outcome: $outcome,
-			result: $processedResults
+			result: [
+				'upload_ids'      => $uploadIds,
+				'uploads'         => $processedResults,
+				'processed_count' => count($processedResults),
+				'total_count'     => count($uploadIds),
+				'errors'          => $errors,
+			]
 		);
 	}
 
 	/**
+	 * Find a secured file entry by upload_id
+	 */
+	private function findSecuredFile(array $securedFiles, string $uploadId): ?array
+	{
+		foreach ($securedFiles as $file) {
+			if (($file['upload_id'] ?? null) === $uploadId) {
+				return $file;
+			}
+		}
+		return null;
+	}
+
+	/**
 	 * Attach upload results to content
 	 */
 	private function processAttachToContent(Operation $operation, array $data, Progress $progress): Result
 	{
 		$uploadOpId = $data['upload'] ?? null;
+		error_log('processing attached to content: '.print_r($data, true));
 		if (!$uploadOpId) {
 			throw new Exception('No upload operation ID provided');
 		}
 
-		// Get results from the dependency
 		$uploadOp = JVB()->queue()->get($uploadOpId);
-		if (!$uploadOp || $uploadOp->outcome !== 'success') {
+		if (!$uploadOp || !in_array($uploadOp->outcome, ['success', 'partial'])) {
 			throw new Exception("Upload operation {$uploadOpId} not completed successfully");
 		}
 
-		$uploadResults = $uploadOp->result ?? [];
+		$uploadResults = $uploadOp->result['uploads'] ?? [];
 		if (empty($uploadResults)) {
 			throw new Exception('No upload results found');
 		}
 
-		// Attach to content via field
+		// Resolve post_id from content_update dependency for new posts
+		if (empty($data['post_id']) || str_starts_with((string)($data['item_id'] ?? ''), 'new')) {
+			foreach ($operation->dependencies as $depId) {
+				$dep = JVB()->queue()->get($depId);
+				if ($dep && $dep->type === 'content_update' && !empty($dep->result['new_posts'])) {
+					$itemId = $data['item_id'] ?? null;
+					if ($itemId && isset($dep->result['new_posts'][$itemId])) {
+						$data['post_id'] = $dep->result['new_posts'][$itemId];
+						break;
+					}
+				}
+			}
+
+			if (empty($data['post_id'])) {
+				throw new Exception('Could not resolve post_id from dependencies');
+			}
+		}
+
 		if (!empty($data['field_name'])) {
-			$this->updateFieldValue($data, $uploadResults);
+			if ($data['field_name'] === 'timeline_gallery') {
+				$registrar = Registrar::getInstance($data['content']);
+				if ($registrar) {
+					switch ($registrar->getType()) {
+						case 'post':
+							$meta = Meta::forPost($data['item_id']);
+							break;
+						case 'term':
+							$meta = Meta::forTerm($data['item_id']);
+							break;
+						case 'user':
+							$meta = Meta::forUser($data['item_id']);
+							break;
+						default:
+							$meta = false;
+
+					}
+					if ($meta) {
+						$title = $meta->get('post_title');
+						$current = $meta->get('number');
+						$i = empty($current) ? 1 : $current + 1;
+						foreach ($data['upload_ids'] as $uploadID) {
+							if (!array_key_exists($uploadID, $uploadResults)) {
+								continue;
+							}
+							$imgID = $uploadResults[$uploadID]['attachment_id'] ?? false;
+							if (!$imgID) {
+								continue;
+							}
+							$this->createTimelinePoint($imgID, $data['item_id'], $data['user'], $data['content'], $title, $i);
+							$i++;
+						}
+					}
+				}
+			} else {
+				$this->saveToMeta($data, $uploadResults);
+			}
 		}
 
 		$progress->advance(1);
 
 		return new Result(
 			outcome: 'success',
-			result: ['attached' => count($uploadResults)]
+			result: ['attached' => count($uploadResults), 'post_id' => $data['post_id']]
 		);
 	}
 
+		protected function createTimelinePoint(int $imgID, int $parentID, int $user, string $postType, string $baseTitle, int $index):int|WP_Error
+		{
+			$title = $baseTitle.' - Treatment #'.$index;
+			$args = [
+				'post_type'		=> jvbCheckBase($postType),
+				'post_author'	=> $user,
+				'post_status'	=> 'draft',
+				'post_parent'	=> $parentID,
+				'post_title'	=> $title,
+				'post_name'		=> sanitize_title($title)
+			];
+
+			$child = wp_insert_post($args);
+			if ($child && !is_wp_error($child)) {
+				set_post_thumbnail($child, $imgID);
+			}
+			return $child;
+		}
+
 	/**
 	 * Process metadata updates for attachments
 	 */
-	private function processMetadataUpdate(Operation $operation, array $data, Progress $progress): Result
+	private function processMetaUpdate(Operation $operation, array $data, Progress $progress): Result
 	{
 		$updatedCount = 0;
 		$errors = [];
 
+		$postsAttachedTo =[];
+		error_log('Processing Meta Update with data: '.print_r($data, true));
 		foreach ($data as $uploadId => $info) {
-			if (!is_array($info) || empty($info['depends_on'])) {
+			if (!is_array($info)) {
 				continue;
 			}
-
+			$success = true;
 			try {
-				// Get the dependency operation to find attachment ID
-				$depOp = JVB()->queue()->get($info['depends_on']);
-				if (!$depOp || !$depOp->result) {
-					$errors[] = "Dependency {$info['depends_on']} not found or has no result";
+				if (array_key_exists('depends_on', $info)) {
+					// Get the dependency operation to find attachment ID
+					$depOp = JVB()->queue()->get($info['depends_on']);
+					if (!$depOp || !$depOp->result) {
+						$errors[] = "Dependency {$info['depends_on']} not found or has no result";
+						continue;
+					}
+					$attachmentId = $this->findAttachmentByUploadId($uploadId, $depOp->result);
+					if (!$attachmentId) {
+						$errors[] = "No attachment found for upload ID: {$uploadId}";
+						continue;
+					}
+				} else {
+					$attachmentId = $info['attachmentId']??false;
+				}
+
+				if (!$attachmentId) {
+					$errors[] = "No attachment found for: ".print_r($info, true);
 					continue;
 				}
 
-				$attachmentId = $this->findAttachmentByUploadId($uploadId, $depOp->result);
-				if (!$attachmentId) {
-					$errors[] = "No attachment found for upload ID: {$uploadId}";
-					continue;
-				}
+
 
 				$this->applyMeta($attachmentId, $info);
 				$updatedCount++;
 				$progress->advance(1);
 
 			} catch (Exception $e) {
+				$success = false;
 				$progress->failItem($uploadId, $e->getMessage());
 				$errors[] = $e->getMessage();
 			}
+
+			if ($success) {
+				$postID = wp_get_post_parent_id($attachmentId);
+				if ($postID && !in_array($postID, $postsAttachedTo)){
+					$postsAttachedTo[] = $postID;
+				}
+			}
+
+		}
+		if (!empty ($postsAttachedTo)) {
+			foreach ($postsAttachedTo as $postId) {
+				Cache::invalidateItem('post', $postId);
+			}
 		}
 
 		$outcome = $updatedCount > 0 ? 'success' : 'failed';
@@ -271,17 +400,27 @@
 		}
 
 		$uploads = [];
+		$uploadIds = [];
 		foreach ($dependencies as $dependency) {
 			$res = JVB()->queue()->getOperationValue($dependency, 'result');
 			if (empty($res)) {
 				continue;
 			}
 
-			// Results are stored at root level, keyed by upload_id
+			// Check if dependency result has upload_ids
+			if (isset($res['upload_ids'])) {
+				$uploadIds = array_merge($uploadIds, $res['upload_ids']);
+			}
+			// Results are stored in 'uploads', keyed by upload_id
 			// Filter to only include actual upload results (arrays with attachment_id)
-			foreach ($res as $key => $value) {
+			foreach ($res['uploads'] as $key => $value) {
 				if (is_array($value) && isset($value['attachment_id'])) {
 					$uploads[$key] = $value;
+
+					// If we didn't get upload_ids from result, track them from keys
+					if (!isset($res['upload_ids']) && !in_array($key, $uploadIds)) {
+						$uploadIds[] = $key;
+					}
 				}
 			}
 		}
@@ -302,76 +441,130 @@
 		}
 
 		$content = jvbCheckBase($data['content']);
-		if (Features::forContent($data['content'])->has('is_timeline')) {
+		$registrar = Registrar::getInstance($data['content']);
+		if ($registrar && $registrar->hasFeature('is_timeline')) {
 			return $this->processTimelineUploads($operation, $data, $progress, $all_uploads);
 		}
 
-		$user = (int)$operation->userId;
+		$user = $operation->userId;
 		$createdPosts = [];
+		$errors = [];
+		$groupMappings = [];
 		$usedUploads = [];
 
 		foreach($data['posts'] as $index => $post) {
-			$progress->advance();
-			$post_title = array_key_exists('post_title', $post['fields'])
-				? sanitize_text_field($post['fields']['post_title'])
-				: 'New '. JVB_CONTENT[$data['content']]['singular'].' '.($index + 1);
+			try {
+				$groupId = $post['groupId'] ?? null;
+				// Create post for this group
+				$created = $this->createPostFromGroup($post, $index+1, $content, $uploads, $operation);
 
-			$post_excerpt = array_key_exists('post_excerpt', $post['fields'])
-				? sanitize_textarea_field($post['fields']['post_excerpt'])
-				: '';
+				if ($created) {
+					$postId = $created['ID'];
+					$createdPosts[] = [
+						'post_id' => $postId,
+						'group_id' => $groupId,
+					];
 
-			$args = [
-				'post_type'		=> $content,
-				'post_author'	=> $user,
-				'post_status'	=> 'draft',
-				'post_title'	=> $post_title,
-				'post_excerpt'	=> $post_excerpt
-			];
-			$newPostID = wp_insert_post($args);
-			if ($newPostID && !is_wp_error($newPostID)) {
-				$createdPosts[] = $newPostID;
-
-				$featured_upload_id = $post['fields']['featured']??null;
-				$featured_attachment_id = null;
-				$gallery_attachment_ids = [];
-
-				foreach ($post['images'] as $img) {
-					$uploadId = $img['upload_id'];
-					$usedUploads[] = $uploadId;
-					if (array_key_exists($uploadId, $all_uploads)) {
-						$attachmentId = $all_uploads[$uploadId]['attachment_id'];
-
-						if ($uploadId === $featured_upload_id) {
-							$featured_attachment_id = $attachmentId;
-						} else {
-							$gallery_attachment_ids[] = $attachmentId;
-						}
+					if ($groupId) {
+						$groupMappings[$groupId] = $postId;
 					}
-				}
-				if ($featured_attachment_id) {
-					set_post_thumbnail($newPostID, $featured_attachment_id);
-				} elseif (!empty($gallery_attachment_ids)) {
-					set_post_thumbnail($newPostID, $gallery_attachment_ids[0]);
-					array_shift($gallery_attachment_ids);
-				}
 
-				if (!empty($gallery_attachment_ids)) {
-					$meta = new MetaManager($newPostID, 'post');
-					$fields = jvbGetFields($content, 'post');
-					foreach($fields as $name => $config) {
-						if ($config['type'] === 'gallery') {
-							$meta->updateValue($name, implode(',', $gallery_attachment_ids));
-							break;
-						}
-					}
+					$usedUploads = array_merge($usedUploads, $created['usedUploads']);
+					$progress->advance(1);
 				}
+			} catch (Exception $e) {
+				$errors[] = $e->getMessage();
+				$progress->failItem($index ?? 'unknown', $e->getMessage());
+			}
+		}
+		$outcome = !empty($createdPosts) ? 'success' : 'failed';
+		if (!empty($createdPosts) && !empty($errors)) {
+			$outcome = 'partial';
+		}
+		return new Result(
+			outcome: $outcome,
+			result: [
+				'upload_ids' => $usedUploads,
+				'created_posts' => $createdPosts,
+				'group_mappings' => $groupMappings,
+				'post_count' => count($createdPosts),
+				'processed_uploads' => count($uploads),
+				'errors' => $errors,
+			]
+		);
+	}
+
+	protected function createPostFromGroup(array $post, int $index, string $content, array $uploads, Operation $op):array|false
+	{
+		$registrar = Registrar::getInstance($content);
+
+		$post_title = array_key_exists('post_title', $post['fields'])
+			? sanitize_text_field($post['fields']['post_title'])
+			: ($registrar ? 'New '. $registrar->getSingular().' '.($index + 1) : 'New Item '.($index + 1));
+
+		$post_excerpt = array_key_exists('post_excerpt', $post['fields'])
+			? sanitize_textarea_field($post['fields']['post_excerpt'])
+			: '';
+
+		$ID = wp_insert_post([
+			'post_type'		=> $content,
+			'post_author'	=> $op->userId,
+			'post_status'	=> 'draft',
+			'post_title'	=> $post_title,
+			'post_excerpt'	=> $post_excerpt,
+		]);
+		if (!$ID || is_wp_error($ID)) {
+			throw new Exception('Could not create post: '.$ID?->get_error_message());
+		}
+
+		$uploadIds = [];
+		$featured_upload_id = $post['fields']['featured']??null;
+		$featured_attachment_id = null;
+		$gallery = [];
+
+		foreach ($post['images'] as $img) {
+			$uploadId = $img['upload_id'];
+			if (array_key_exists($uploadId, $uploads)){
+				$imgID = $uploads[$uploadId]['attachment_id'];
+				if ($uploadId === $featured_upload_id) {
+					$featured_attachment_id = $imgID;
+				} else {
+					$gallery[] = $imgID;
+				}
+				$uploadIds[] = $uploadId;
 			}
 		}
 
-		return new Result(
-			outcome: !empty($createdPosts) ? 'success' : 'failed',
-			result: ['posts' => $createdPosts]
-		);
+		if ($featured_attachment_id) {
+			set_post_thumbnail($ID, $featured_attachment_id);
+		} elseif (!empty($gallery)) {
+			set_post_thumbnail($ID, $gallery[0]);
+			array_shift($gallery);
+		}
+
+		if (!empty($gallery)) {
+			$meta = Meta::forPost($ID);
+			$fields = Registrar::getFieldsFor($content);
+			//add images to first found gallery field
+			$found = false;
+			foreach ($fields as $name =>$config) {
+				if ($config['type'] === 'gallery' || ($config['type'] === 'upload' && (array_key_exists('multiple', $config) && $config['multiple'] === true))) {
+					$found = true;
+					$meta->set($name, implode(',', $gallery));
+					break;
+				}
+			}
+			if (!$found) {
+				error_log('Could not find a gallery upload field for post '.$ID);
+			}
+
+		}
+
+
+		return [
+			'ID'	=> $ID,
+			'usedUploads' => $uploadIds
+		];
 	}
 
 	private function processTimelineUploads(Operation $operation, array $data, Progress $progress, array $uploads):Result
@@ -379,38 +572,45 @@
 		$user = $operation->userId;
 		$createdPosts = [];
 		$usedUploads = [];
+		$errors = [];
 
 		$content = jvbCheckBase($data['content']);
-		$config = Features::getConfig($content);
+		$registrar = Registrar::getInstance($data['content']);
 
-		$defaultTitle = 'New '.$config['singular']. ' ';
+
+		$defaultTitle = ($registrar) ? 'New '.$registrar->getSingular(). ' ' : 'New Item ';
 		foreach($data['posts'] as $index => $post) {
-			$title = array_key_exists('post_title', $post['fields'])
-				? sanitize_text_field($post['fields']['post_title'])
-				: $defaultTitle . ($index + 1);
+			try {
+				$title = array_key_exists('post_title', $post['fields'])
+					? sanitize_text_field($post['fields']['post_title'])
+					: $defaultTitle . ($index + 1);
 
-			$excerpt = array_key_exists('post_excerpt', $post['fields'])
-				? sanitize_textarea_field($post['fields']['post_excerpt'])
-				: '';
+				$excerpt = array_key_exists('post_excerpt', $post['fields'])
+					? sanitize_textarea_field($post['fields']['post_excerpt'])
+					: '';
 
-			$args = [
-				'post_type'	=> $content,
-				'post_author'	=> $user,
-				'post_status'	=> 'draft',
-				'post_title'	=> $title,
-				'post_excerpt'	=> $excerpt
-			];
+				$args = [
+					'post_type'	=> $content,
+					'post_author'	=> $user,
+					'post_status'	=> 'draft',
+					'post_title'	=> $title,
+					'post_name'		=> sanitize_title($title),
+					'post_excerpt'	=> $excerpt
+				];
 
-			$parent = wp_insert_post($args);
-			$progress->advance();
-			if ($parent && !is_wp_error($parent)) {
+				$parent = wp_insert_post($args);
+
+				if (!$parent || is_wp_error($parent)) {
+					throw new Exception('Could not create post: '.$parent->get_error_message());
+				}
+
+
 				$childPosts = [];
 				$featured = $post['fields']['featured']??null;
 				$featuredID = null;
 
 				foreach ($post['images'] as $img) {
 					$uploadId = $img['upload_id'];
-					$usedUploads[] = $uploadId;
 
 					if (array_key_exists($uploadId, $uploads)) {
 						$attachmentId = (int)$uploads[$uploadId]['attachment_id'];
@@ -421,56 +621,108 @@
 						}
 					}
 				}
+
 				if ($featuredID) {
+					$usedUploads[] = $featuredID;
 					set_post_thumbnail($parent, $featuredID);
 				} elseif (!empty($childPosts)) {
 					set_post_thumbnail($parent, (int)$childPosts[0]);
+					$usedUploads[] = (int)$childPosts[0];
 					array_shift($childPosts);
 				}
+
+				$createdChildren = [];
 				if (!empty($childPosts)) {
 					$args['post_parent'] = $parent;
 					$args['post_excerpt'] = '';
-					$createdPosts[$parent] = [];
+
 					foreach($childPosts as $i => $imgID) {
 						$treatment = $i + 1;
-						$args ['post_title'] = $title.' - Treatment #'.$treatment;
-						$child = wp_insert_post($args);
-						if ($child && !is_wp_error($child)) {
-							$createdPosts[$parent][] = $child;
-							set_post_thumbnail($child, $imgID);
+						$child = $this->createTimelinePoint($imgID, $parent, $args['post_author'], $args['post_type'], $title, $treatment);
+						if ($child && !is_wp_error($child) && $child> 0 ) {
+							$createdChildren[] = $child;
+							$usedUploads[] = $imgID;
 						}
 					}
 				}
+				$createdPosts[] = [
+					'parent'	=> $parent,
+					'children'	=> $createdChildren
+				];
+
+				$this->updateTimelineMetadata($parent);
+				$progress->advance();
+			} catch (Exception $e) {
+				$errors[] = $e->getMessage();
+				$progress->failItem($index ?? 'unknown', $e->getMessage());
 			}
 		}
+
+		$outcome = !empty($createdPosts) ? 'success' : 'failed';
+		if (!empty($createdPosts) && !empty($errors)) {
+			$outcome = 'partial';
+		}
 		return new Result(
-			outcome: !empty($createdPosts) ? 'success' : 'failed',
-			result: ['posts' => $createdPosts]
+			outcome: $outcome,
+			result: [
+				'upload_ids'	=> $usedUploads,
+				'created_posts'	=> $createdPosts,
+				'post_count'	=> count($createdPosts),
+				'processed_uploads'	=> count($uploads),
+				'errors'		=> $errors
+			]
 		);
 	}
 
+	/**
+	 * Update timeline parent post with count and latest date
+	 * @param int $parentId Parent timeline post ID
+	 */
+	private function updateTimelineMetadata(int $parentId): void
+	{
+		// Get all child posts
+		$children = get_children([
+			'post_parent' => $parentId,
+			'post_type' => get_post_type($parentId),
+			'post_status' => ['publish', 'draft'],
+			'orderby' => 'date',
+			'order' => 'DESC',
+			'fields' => 'ids'
+		]);
+
+		// Count includes parent + children
+		$number = count($children) + 1;
+
+		// Update both meta fields
+		update_post_meta($parentId, BASE . 'number', $number);
+		update_post_meta($parentId, BASE . 'latest_date', time());
+	}
+
 	// ─────────────────────────────────────────────────────────────
 	// Helper methods
 	// ─────────────────────────────────────────────────────────────
 
 	private function applyMeta(int $attachmentId, array $metadata): void
 	{
-		if (!empty($metadata['title'])) {
-			wp_update_post([
-				'ID'         => $attachmentId,
-				'post_title' => sanitize_text_field($metadata['title']),
-			]);
+		if (array_key_exists('image-alt-text', $metadata)) {
+			update_post_meta($attachmentId, '_wp_attachment_image_alt', sanitize_text_field($metadata['image-alt-text']));
 		}
 
-		if (!empty($metadata['alt'])) {
-			update_post_meta($attachmentId, '_wp_attachment_image_alt', sanitize_text_field($metadata['alt']));
+		$postUpdates = [];
+
+		if (array_key_exists('image-title', $metadata)) {
+			$postUpdates['post_title'] = sanitize_text_field($metadata['image-title']);
 		}
 
-		if (!empty($metadata['caption'])) {
-			wp_update_post([
-				'ID'           => $attachmentId,
-				'post_excerpt' => sanitize_textarea_field($metadata['caption']),
-			]);
+		if (array_key_exists('image-caption', $metadata)) {
+			$postUpdates['post_excerpt'] = sanitize_textarea_field($metadata['image-caption']);
+		}
+		if (array_key_exists('image-description', $metadata)) {
+			$postUpdates['post_excerpt']= sanitize_textarea_field($metadata['image-caption']);
+		}
+		if (!empty($postUpdates)){
+			$postUpdates['ID'] = $attachmentId;
+			wp_update_post($postUpdates);
 		}
 	}
 
@@ -503,11 +755,19 @@
 			return;
 		}
 
-		$existing = $meta->getValue($data['field_name']);
-		$existingIds = !empty($existing) ? explode(',', $existing) : [];
-		$allIds = array_unique(array_merge($existingIds, $attachmentIds));
+		$fieldType = $data['field_type'] ?? 'single';
 
-		$meta->updateValue($data['field_name'], implode(',', $allIds));
+		if ($fieldType === 'single') {
+			// Single field: replace with latest upload
+			$meta->set($data['field_name'], end($attachmentIds));
+		} else {
+			// Multi field: merge with existing
+			$existing = $meta->get($data['field_name']);
+			$existingIds = !empty($existing) ? explode(',', $existing) : [];
+			$allIds = array_unique(array_merge($existingIds, $attachmentIds));
+			$meta->set($data['field_name'], implode(',', $allIds));
+		}
+
 	}
 
 	private function updateFieldValue(array $data, array $results): void
@@ -522,25 +782,25 @@
 			return;
 		}
 
-		$existing = $meta->getValue($data['field_name']);
+		$existing = $meta->get($data['field_name']);
 		$existingIds = !empty($existing) ? explode(',', $existing) : [];
 		$allIds = array_unique(array_merge($existingIds, $attachmentIds));
 
-		$meta->updateValue($data['field_name'], implode(',', $allIds));
+		$meta->set($data['field_name'], implode(',', $allIds));
 	}
 
-	private function getMetaManager(array $data): ?MetaManager
+	private function getMetaManager(array $data): ?Meta
 	{
 		if (!empty($data['post_id'])) {
-			return new MetaManager($data['post_id'], 'post');
+			return Meta::forPost($data['post_id']);
 		}
 		if (!empty($data['term_id'])) {
-			return new MetaManager($data['term_id'], 'term');
+			return Meta::forTerm($data['term_id']);
 		}
 		if (!empty($data['user'])) {
 			$link = (int)get_user_meta($data['user'], BASE . 'link', true);
 			if ($link) {
-				return new MetaManager($link, 'post');
+				return Meta::forPost($link);
 			}
 		}
 		return null;
@@ -587,14 +847,14 @@
 		if (str_starts_with($mimeType, 'image/')) {
 			set_post_thumbnail($postId, $attachmentId);
 		} elseif (str_starts_with($mimeType, 'video/')) {
-			$meta = new MetaManager($postId, 'post');
-			$meta->updateValue('video', $attachmentId);
+			$meta = Meta::forPost($postId);
+			$meta->set('video', $attachmentId);
 		} else {
-			$meta = new MetaManager($postId, 'post');
-			$existing = $meta->getValue('documents');
+			$meta = Meta::forPost($postId);
+			$existing = $meta->get('documents');
 			$existingIds = !empty($existing) ? explode(',', $existing) : [];
 			$existingIds[] = $attachmentId;
-			$meta->updateValue('documents', implode(',', $existingIds));
+			$meta->set('documents', implode(',', $existingIds));
 		}
 	}
 

--
Gitblit v1.10.0