From e6672fe38ce5d99f3b3f026154f777aded7361de Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 29 Jan 2026 17:36:53 +0000
Subject: [PATCH] =Starting refactor of Meta and Routes to fluent-style

---
 inc/managers/queue/executors/UploadExecutor.php |   50 
 inc/meta/Meta.php                               |  399 +++++++++
 inc/meta/Storage.php                            |  305 ++++++
 build/feed/view.asset.php                       |    2 
 inc/rest/Route.php                              |  480 ++++++++++
 inc/rest/PermissionHandler.php                  |  514 +++++++++++
 inc/rest/Response.php                           |  437 +++++++++
 inc/rest/RateLimits.php                         |    0 
 inc/rest/Rest.php                               |    0 
 build/feed/view.js                              |    2 
 inc/meta/_setup.php                             |   17 
 inc/rest/_setup.php                             |    8 
 inc/rest/routes/UploadRoutes.php                |   31 
 inc/meta/Field.php                              |   84 +
 src/feed/view.js                                |    4 
 inc/rest/routes/QueueRoutes.php                 |  178 ---
 inc/meta/Item.php                               |  115 ++
 17 files changed, 2,437 insertions(+), 189 deletions(-)

diff --git a/build/feed/view.asset.php b/build/feed/view.asset.php
index 90ed37f..c6293e9 100644
--- a/build/feed/view.asset.php
+++ b/build/feed/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '1589cfb61e8639162b4c');
+<?php return array('dependencies' => array(), 'version' => '1ef4e221261df2cef9a9');
diff --git a/build/feed/view.js b/build/feed/view.js
index 4245a34..f1acc13 100644
--- a/build/feed/view.js
+++ b/build/feed/view.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.cache=new window.jvbCache("feed"),this.templates=window.jvbTemplates,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.init())}init(){this.initElements(),this.defineTemplates(),this.initListeners(),this.initFilters(),"requestIdleCallback"in window?requestIdleCallback((()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()}),{timeout:2e3}):setTimeout((()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()}),100)}initElements(){this.selectors={filterTrigger:"[data-filter]",filters:{actions:".filter-actions .toggle-text",container:".filters",content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},grid:".item-grid",selected:".selected-items",buttons:{loadMore:"button.load-more",remove:".remove-term",clearFilters:"button.clear-filters",refresh:'button[data-action="refresh"]'}},this.ui=window.uiFromSelectors(this.selectors,this.container),this.ui.buttons.refresh=document.querySelector(this.selectors.buttons.refresh),this.ui.content=this.ui.filters.container.querySelectorAll('[name="content"]'),0===this.ui.content.length&&(this.ui.content=!1),this.ui.taxonomies=this.ui.filters.container.querySelectorAll("[data-taxonomy]"),0===this.ui.taxonomies.length&&(this.ui.taxonomies=!1),this.ui.orderbyWrap=this.ui.filters.container.querySelector("[data-for-order]"),0===this.ui.orderbyWrap.length&&(this.ui.orderbyWrap=!1),this.ui.order=this.ui.filters.container.querySelectorAll('[data-filter="order"]'),0===this.ui.order.length&&(this.ui.order=!1),this.ui.orderby=this.ui.filters.container.querySelectorAll('[data-filter="orderby"]'),0===this.ui.orderby.length&&(this.ui.orderby=!1),this.orderbyFilters=this.ui.orderby?Array.from(this.ui.orderby).map((e=>e.value)):[],this.contentTypes=this.ui.content?Array.from(this.ui.content).map((e=>e.value)):[this.container.dataset.content],this.taxonomies=this.ui.taxonomies?.length>0?Array.from(this.ui.taxonomies).map((e=>e.dataset.taxonomy)):[]}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}initFilters(){this.allowedFilters=["content","order","orderby","favourites","match"];let e={content:this.contentTypes[0],orderby:"date",order:"desc",page:1};this.config.context&&(e.context=this.config.context),this.config.source&&(e.source=this.config.source),this.filters=e,this.defaults={...e}}updateFilterUI(){if(this.ui.filters.container&&([this.ui.content,this.ui.orderby,this.ui.order].forEach((e=>{if(e)for(let t of e){let[e,i]=[t.dataset.filter,t.value];if(!Object.hasOwn(this.store.filters,e))break;let s=this.store.filters[e]===i;if(s){t.checked=s;break}}})),Object.hasOwn(this.store.filters,"taxonomy")))for(let[e,t]of Object.entries(this.store.filters.taxonomy))t.forEach((e=>{e=parseInt(e),this.selector.store.get(e)&&this.createTermElement(e)}))}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.selectors.buttons.loadMore)?this.nextPage():window.targetCheck(e,this.selectors.buttons.clearFilters)&&this.clearFilters();let t=window.targetCheck(e,this.selectors.buttons.remove);t&&this.removeSelectedTerm(t),window.targetCheck(e,this.selectors.buttons.refresh)&&(this.store.clearCache(),this.store.fetch());let i=window.targetCheck(e,'[data-filter="orderby"]');i&&"random"===i.value&&i.checked&&this.renderItems()}nextPage(){const e=(this.store.filters.page||1)+1,t=this.store.lastResponse?.pages||e;this.store.setFilters({page:Math.min(e,t)})}handleChange(e){const t=e.target;if(Object.hasOwn(t.dataset,"filter")){if(this.allowedFilters.includes(t.dataset.filter)){let e={};e[t.dataset.filter]=t.value,this.resetFilters(e)}switch(t.dataset.filter){case"content":this.updateContentFor(t.value);break;case"orderby":this.updateOrderOptions(t.value)}}}clearFilters(){this.taxFilters={},window.removeChildren(this.ui.selected),this.taxonomies.forEach((e=>{let t=this.getFieldId(e);this.selector.selectedTerms.get(t)?.clear()})),this.store.setFilters({...this.defaults,taxonomy:null}),this.updateURL(),this.saveToCacheFilters()}resetFilters(e){e={...this.store.filters,page:1,...e},this.store.setFilters(e),this.updateURL(),this.saveToCacheFilters()}getFieldId(e){var t;return this.selector.getFieldId(null!==(t=Array.from(this.ui.taxonomies).filter((t=>t.dataset.taxonomy===e))[0])&&void 0!==t?t:null)}removeSelectedTerm(e){const t=parseInt(e.dataset.id),i=e.dataset.taxonomy;Object.hasOwn(this.taxFilters,i)&&(this.taxFilters[i]=this.taxFilters[i].filter((e=>e!==t)),0===this.taxFilters[i].length&&delete this.taxFilters[i]),e.remove();const s=this.getFieldId(i);s&&(this.selector.activeField=s,this.selector.removeSelected(t,s)),this.resetFilters({taxonomy:Object.keys(this.taxFilters).length>0?this.taxFilters:null})}updateContentFor(e){[this.ui.taxonomies,this.ui.orderby].forEach((t=>{t&&t.forEach((t=>{var i;const s=null!==(i=t.dataset.for?.split(","))&&void 0!==i?i:[];t.hidden=s.length>0&&!s.includes(e),t.hidden&&t.checked&&(t.checked=!1)}))}))}updateOrderOptions(e){if(this.ui.orderbyWrap){var t;let i=null!==(t=this.ui.orderbyWrap.dataset.forOrder.split(","))&&void 0!==t?t:[];this.ui.orderbyWrap.hidden=!i.includes(e)}}updateFilterControls(){const e=0===Object.keys(this.taxFilters).length;this.ui.buttons.clearFilters&&(this.ui.buttons.clearFilters.hidden=e),this.ui.filters.actions&&(this.ui.filters.actions.hidden=e)}async initTaxonomies(){this.taxFilters={},this.selector=window.jvbSelector,this.selector.subscribe(((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)}))}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;0!==t.size&&(this.taxFilters[i]=Array.from(t),this.resetFilters({taxonomy:this.taxFilters}),t.forEach((e=>{this.createTermElement(e)})),this.updateFilterControls())}getTaxonomyIcon(e){let t=Array.from(this.ui.taxonomies).find((t=>t.dataset.taxonomy===e));return t?.dataset.icon.trim()||"tag"}createTermElement(e){const t=this.selector.store.get(e);t&&(this.ui.selected.querySelector(`[data-id="${e}"]`)||(t.icon=this.getTaxonomyIcon(t.taxonomy),this.ui.selected.append(this.templates.create("feedTerm",t))))}processCachedFilters(){Object.keys(this.filters).forEach((e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)}))}processURLFilters(){if(!this.isFirstPage())return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;let t=!1;this.allowedFilters.forEach((i=>{let s=e.get(`f_${i}`);s&&(t=!0,this.filters[i]=s)}));let i=!1;return e.forEach(((e,s)=>{if(s.startsWith("f_tax_")){i=!0,t=!0;const r=s.replace("f_tax_","");this.taxFilters[r]=e.split(",").map(Number)}})),t&&(i&&(this.filters.taxonomy=this.taxFilters),this.resetFilters(this.filters)),!0}updateURL(){const e=new URLSearchParams;this.allowedFilters.forEach((t=>{Object.hasOwn(this.store.filters,t)&&this.store.filters[t]!==this.defaults[t]&&e.set(`f_${t}`,this.store.filters[t])}));for(let[t,i]of Object.entries(this.taxFilters))i.length>0&&e.set(`f_tax_${t}`,i.join(","));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;t!==window.location.pathname+window.location.search&&window.history.pushState({filters:this.store.filters},"",t)}saveToCacheFilters(){Object.keys(this.store.filters).forEach((e=>{const t=`${this.config.source}_${this.config.context}_${e}`;this.store.filters[e]!==this.defaults[e]?this.cache.set(t,this.store.filters[e]):this.cache.remove(t)}));const e=`${this.config.source}_${this.config.context}_taxonomy`;Object.keys(this.taxFilters).length>0?this.cache.set(e,this.taxFilters):this.cache.remove(e)}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe(((e,t)=>{"load-more"===e&&this.store.lastResponse?.has_more&&this.nextPage()}))}initStore(){let e=this.orderbyFilters.filter((e=>!["date","modified","title","random"].includes(e))),t=[];e.forEach((e=>{t.push({name:e,keyPath:e})}));const i=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"},...t],filters:this.filters,TTL:216e5,showLoading:!0,required:"content"});this.store=i.feed,this.store.subscribe(((e,t)=>{var i;"data-loaded"===e&&(this.renderItems(t.items),this.ui.buttons.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse?.has_more&&(this.ui.buttons.loadMore.hidden=null===(i=!this.store.lastResponse?.has_more)||void 0===i||i))}))}isFirstPage(){return 1===this.store.filters.page}renderItems(e=null){e=null!=e?e:this.store.getFiltered(),this.isFirstPage()&&window.removeChildren(this.ui.grid),0===e.length?(this.showEmptyState(),this.a11y.announceItems(0,this.isFirstPage())):window.chunkIt(e,(e=>this.createItemElement(e)),(t=>{var i;this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.buildGalleryItems(".item img"),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,!this.isFirstPage(),null!==(i=this.store.lastResponse?.has_more)&&void 0!==i&&i)}),5).then((()=>{})),this.updateFilterControls()}showEmptyState(){window.removeChildren(this.ui.grid),this.ui.grid.append(this.templates.create("emptyState"))}createItemElement(e){if("object"==typeof e||(e=this.store.get(e)))return this.templates.create(`feedItem${window.uppercaseFirst(e.content)}`,e)}splitIDs(e){return String(e).split(",").map((e=>parseInt(e.trim()))).filter((e=>e))}isImageField(e,t){return!(!Object.hasOwn(e,"images")||0===Object.keys(e.images).length)&&this.splitIDs(t).some((t=>Object.keys(e.images).map((e=>parseInt(e))).includes(parseInt(t))))}formatImageFields(e,t,i){let s=this.splitIDs(t);if(0!==s.length)if(s.length>1){let t=e.querySelector("img");if(!t)return;s.forEach((s=>{let r=t.cloneNode(!0);this.formatImageField(r,s,i),e.append(r)})),t.remove()}else{if("IMG"!==e.tagName&&!(e=e.querySelector("img")))return;this.formatImageField(e,s[0],i)}}formatImageField(e,t,i){var s;let r=null!==(s=i.images[t])&&void 0!==s&&s;r&&([e.src,e.srcset,e.alt]=[r.tiny,`${r.tiny} 50w, ${r.small} 300w, ${r.medium} 1024w`,r["image-alt-text"]])}isTaxonomyField(e,t){return!(!Object.hasOwn(e,"taxonomies")||0===Object.keys(e.taxonomies).length)&&Object.keys(e.taxonomies).includes(t)}formatTaxonomyField(e,t,i,s){if("UL"!==e.tagName||!e.querySelector("li"))return;let r=this.splitIDs(s);0===r.length&&e.remove();let o=e.querySelector("li");for(let s of r){var a;let r=null!==(a=t.taxonomies[i][s])&&void 0!==a&&a;if(!r)continue;let n=o.cloneNode(!0),l=n.querySelector("a");l&&([l.href,l.title,l.textContent]=[r.url,`See more ${r.title}`,r.title],e.append(n))}o.remove()}isTimeField(e){return"TIME"===e.tagName||null!==e.querySelector("time")}formatTimeField(e,t){("TIME"===e.tagName||(e=e.querySelector("time")))&&(e.setAttribute("datetime",t),e.textContent=window.formatTimeAgo(t,"F Y"))}formatField(e,t){e.textContent=t}addTimelineElements(e,t){let[i,s,r,o]=[t.querySelector("span.after-text"),t.querySelector('[data-field="number"] b'),t.querySelector('[data-field="started"] time'),t.querySelector('[data-field="updated"] time')];i&&(i.textContent=`After ${e.number} Tx`),s&&(s.textContent=e.fields.number),r&&this.formatTimeField(r,e.fields.timeline[0].post_date),o&&this.formatTimeField(o,e.fields.timeline[e.fields.timeline.length-1].post_date)}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach((e=>e.remove()))}defineTemplates(){const e=this.templates,t=this;e.define("feedTerm",{refs:{icon:".icon",span:"span"},setup({el:e,refs:t,manyRefs:i,data:s}){e.dataset.id=s.id,e.dataset.taxonomy=s.taxonomy,t.icon&&(t.icon.className=`icon icon=${s.icon}`),t.span&&(t.span.textContent=s.name)}}),e.define("emptyState"),this.contentTypes.forEach((i=>{e.define(`feedItem${window.uppercaseFirst(i)}`,{refs:{link:"a"},manyRefs:{fields:"[data-field]"},setup({el:e,refs:i,manyRefs:s,data:r}){const o=Object.hasOwn(e.dataset,"timeline");if(s.fields){for(let e of s.fields){if(o&&["timeline","number"].includes(e.dataset.field))continue;const i=!!Object.hasOwn(r.fields,e.dataset.field)&&r.fields[e.dataset.field];i?t.isImageField(r,i)?t.formatImageField(e,i,r):t.isTaxonomyField(r,e.dataset.field)?t.formatTaxonomyField(e,r,e.dataset.field,i):t.isTimeField(e)?t.formatTimeField(e,i):t.formatField(e,i):e.remove()}var a;i.link&&""!==r.url&&(i.link.href=r.url,i.link.title=`View ${null!==(a=r.fields.post_title)&&void 0!==a?a:"Item"}`),o&&t.addTimelineElements(r,e)}}})}))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.feedBlock=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.cache=new window.jvbCache("feed"),this.templates=window.jvbTemplates,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.init())}init(){this.initElements(),this.defineTemplates(),this.initListeners(),this.initFilters(),"requestIdleCallback"in window?requestIdleCallback((()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()}),{timeout:2e3}):setTimeout((()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()}),100)}initElements(){this.selectors={filterTrigger:"[data-filter]",filters:{actions:".filter-actions .toggle-text",container:".filters",content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},grid:".item-grid",selected:".selected-items",buttons:{loadMore:"button.load-more",remove:".remove-term",clearFilters:"button.clear-filters",refresh:'button[data-action="refresh"]'}},this.ui=window.uiFromSelectors(this.selectors,this.container),this.ui.buttons.refresh=document.querySelector(this.selectors.buttons.refresh),this.ui.content=this.ui.filters.container.querySelectorAll('[name="content"]'),0===this.ui.content.length&&(this.ui.content=!1),this.ui.taxonomies=this.ui.filters.container.querySelectorAll("[data-taxonomy]"),0===this.ui.taxonomies.length&&(this.ui.taxonomies=!1),this.ui.orderbyWrap=this.ui.filters.container.querySelector("[data-for-order]"),0===this.ui.orderbyWrap.length&&(this.ui.orderbyWrap=!1),this.ui.order=this.ui.filters.container.querySelectorAll('[data-filter="order"]'),0===this.ui.order.length&&(this.ui.order=!1),this.ui.orderby=this.ui.filters.container.querySelectorAll('[data-filter="orderby"]'),0===this.ui.orderby.length&&(this.ui.orderby=!1),this.orderbyFilters=this.ui.orderby?Array.from(this.ui.orderby).map((e=>e.value)):[],this.contentTypes=this.ui.content?Array.from(this.ui.content).map((e=>e.value)):[this.container.dataset.content],this.taxonomies=this.ui.taxonomies?.length>0?Array.from(this.ui.taxonomies).map((e=>e.dataset.taxonomy)):[]}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}initFilters(){this.allowedFilters=["content","order","orderby","favourites","match"];let e={content:this.contentTypes[0],orderby:"date",order:"desc",page:1};this.config.context&&(e.context=this.config.context),this.config.source&&(e.source=this.config.source),this.filters=e,this.defaults={...e}}updateFilterUI(){if(this.ui.filters.container&&([this.ui.content,this.ui.orderby,this.ui.order].forEach((e=>{if(e)for(let t of e){let[e,i]=[t.dataset.filter,t.value];if(!Object.hasOwn(this.store.filters,e))break;let s=this.store.filters[e]===i;if(s){t.checked=s;break}}})),Object.hasOwn(this.store.filters,"taxonomy")))for(let[e,t]of Object.entries(this.store.filters.taxonomy))t.forEach((e=>{e=parseInt(e),this.selector.store.get(e)&&this.createTermElement(e)}))}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.selectors.buttons.loadMore)?this.nextPage():window.targetCheck(e,this.selectors.buttons.clearFilters)&&this.clearFilters();let t=window.targetCheck(e,this.selectors.buttons.remove);t&&this.removeSelectedTerm(t),window.targetCheck(e,this.selectors.buttons.refresh)&&(this.store.clearCache(),this.store.fetch());let i=window.targetCheck(e,'[data-filter="orderby"]');i&&"random"===i.value&&i.checked&&this.renderItems()}nextPage(){const e=(this.store.filters.page||1)+1,t=this.store.lastResponse?.pages||e;this.store.setFilters({page:Math.min(e,t)})}handleChange(e){const t=e.target;if(Object.hasOwn(t.dataset,"filter")){if(this.allowedFilters.includes(t.dataset.filter)){let e={};e[t.dataset.filter]=t.value,this.resetFilters(e)}switch(t.dataset.filter){case"content":this.updateContentFor(t.value);break;case"orderby":this.updateOrderOptions(t.value)}}}clearFilters(){this.taxFilters={},window.removeChildren(this.ui.selected),this.taxonomies.forEach((e=>{let t=this.getFieldId(e);this.selector.selectedTerms.get(t)?.clear()})),this.store.setFilters({...this.defaults,taxonomy:null}),this.updateURL(),this.saveToCacheFilters()}resetFilters(e){e={...this.store.filters,page:1,...e},this.store.setFilters(e),this.updateURL(),this.saveToCacheFilters()}getFieldId(e){var t;return this.selector.getFieldId(null!==(t=Array.from(this.ui.taxonomies).filter((t=>t.dataset.taxonomy===e))[0])&&void 0!==t?t:null)}removeSelectedTerm(e){const t=parseInt(e.dataset.id),i=e.dataset.taxonomy;Object.hasOwn(this.taxFilters,i)&&(this.taxFilters[i]=this.taxFilters[i].filter((e=>e!==t)),0===this.taxFilters[i].length&&delete this.taxFilters[i]),e.remove();const s=this.getFieldId(i);s&&(this.selector.activeField=s,this.selector.removeSelected(t,s)),this.resetFilters({taxonomy:Object.keys(this.taxFilters).length>0?this.taxFilters:null})}updateContentFor(e){[this.ui.taxonomies,this.ui.orderby].forEach((t=>{t&&t.forEach((t=>{var i;const s=null!==(i=t.dataset.for?.split(","))&&void 0!==i?i:[];t.hidden=s.length>0&&!s.includes(e),t.hidden&&t.checked&&(t.checked=!1)}))}))}updateOrderOptions(e){if(this.ui.orderbyWrap){var t;let i=null!==(t=this.ui.orderbyWrap.dataset.forOrder.split(","))&&void 0!==t?t:[];this.ui.orderbyWrap.hidden=!i.includes(e)}}updateFilterControls(){const e=0===Object.keys(this.taxFilters).length;this.ui.buttons.clearFilters&&(this.ui.buttons.clearFilters.hidden=e),this.ui.filters.actions&&(this.ui.filters.actions.hidden=e)}async initTaxonomies(){this.taxFilters={},this.selector=window.jvbSelector,this.selector.subscribe(((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)}))}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;0!==t.size&&(this.taxFilters[i]=Array.from(t),this.resetFilters({taxonomy:this.taxFilters}),t.forEach((e=>{this.createTermElement(e)})),this.updateFilterControls())}getTaxonomyIcon(e){let t=Array.from(this.ui.taxonomies).find((t=>t.dataset.taxonomy===e));return t?.dataset.icon.trim()||"tag"}createTermElement(e){const t=this.selector.store.get(e);t&&(this.ui.selected.querySelector(`[data-id="${e}"]`)||(t.icon=this.getTaxonomyIcon(t.taxonomy),this.ui.selected.append(this.templates.create("feedTerm",t))))}processCachedFilters(){Object.keys(this.filters).forEach((e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)}))}processURLFilters(){if(!this.isFirstPage())return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;let t=!1;this.allowedFilters.forEach((i=>{let s=e.get(`f_${i}`);s&&(t=!0,this.filters[i]=s)}));let i=!1;return e.forEach(((e,s)=>{if(s.startsWith("f_tax_")){i=!0,t=!0;const r=s.replace("f_tax_","");this.taxFilters[r]=e.split(",").map(Number)}})),t&&(i&&(this.filters.taxonomy=this.taxFilters),this.resetFilters(this.filters)),!0}updateURL(){const e=new URLSearchParams;this.allowedFilters.forEach((t=>{Object.hasOwn(this.store.filters,t)&&this.store.filters[t]!==this.defaults[t]&&e.set(`f_${t}`,this.store.filters[t])}));for(let[t,i]of Object.entries(this.taxFilters))i.length>0&&e.set(`f_tax_${t}`,i.join(","));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;t!==window.location.pathname+window.location.search&&window.history.pushState({filters:this.store.filters},"",t)}saveToCacheFilters(){Object.keys(this.store.filters).forEach((e=>{const t=`${this.config.source}_${this.config.context}_${e}`;this.store.filters[e]!==this.defaults[e]?this.cache.set(t,this.store.filters[e]):this.cache.remove(t)}));const e=`${this.config.source}_${this.config.context}_taxonomy`;Object.keys(this.taxFilters).length>0?this.cache.set(e,this.taxFilters):this.cache.remove(e)}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe(((e,t)=>{"load-more"===e&&this.store.lastResponse?.has_more&&this.nextPage()}))}initStore(){let e=this.orderbyFilters.filter((e=>!["date","modified","title","random"].includes(e))),t=[];e.forEach((e=>{t.push({name:e,keyPath:e})}));const i=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"},...t],filters:this.filters,TTL:216e5,showLoading:!0,required:"content"});this.store=i.feed,this.store.subscribe(((e,t)=>{var i;"data-loaded"===e&&(this.renderItems(t.items),this.ui.buttons.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse?.has_more&&(this.ui.buttons.loadMore.hidden=null===(i=!this.store.lastResponse?.has_more)||void 0===i||i))}))}isFirstPage(){return 1===this.store.filters.page}renderItems(e=null){e=null!=e?e:this.store.getFiltered(),this.isFirstPage()&&window.removeChildren(this.ui.grid),0===e.length?(this.showEmptyState(),this.a11y.announceItems(0,this.isFirstPage())):window.chunkIt(e,(e=>this.createItemElement(e)),(t=>{var i;this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.buildGalleryItems(".item img"),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,!this.isFirstPage(),null!==(i=this.store.lastResponse?.has_more)&&void 0!==i&&i)}),5).then((()=>{})),this.updateFilterControls()}showEmptyState(){window.removeChildren(this.ui.grid),this.ui.grid.append(this.templates.create("emptyState"))}createItemElement(e){if("object"==typeof e||(e=this.store.get(e)))return this.templates.create(`feedItem${window.uppercaseFirst(e.content)}`,e)}splitIDs(e){return String(e).split(",").map((e=>parseInt(e.trim()))).filter((e=>e))}isImageField(e,t){return!(!Object.hasOwn(e,"images")||0===Object.keys(e.images).length)&&this.splitIDs(t).some((t=>Object.keys(e.images).map((e=>parseInt(e))).includes(parseInt(t))))}formatImageFields(e,t,i){let s=this.splitIDs(t);if(0!==s.length)if(s.length>1){let t=e.querySelector("img");if(!t)return;s.forEach((s=>{let r=t.cloneNode(!0);this.formatImageField(r,s,i),e.append(r)})),t.remove()}else{if("IMG"!==e.tagName&&!(e=e.querySelector("img")))return;this.formatImageField(e,s[0],i)}}formatImageField(e,t,i){var s;let r=null!==(s=i.images[t])&&void 0!==s&&s;r&&([e.src,e.srcset,e.alt]=[r.tiny,`${r.tiny} 50w, ${r.small} 300w, ${r.medium} 1024w`,r["image-alt-text"]])}isTaxonomyField(e,t){return!(!Object.hasOwn(e,"taxonomies")||0===Object.keys(e.taxonomies).length)&&Object.keys(e.taxonomies).includes(t)}formatTaxonomyField(e,t,i,s){if("UL"!==e.tagName||!e.querySelector("li"))return;let r=this.splitIDs(s);0===r.length&&e.remove();let o=e.querySelector("li");for(let s of r){var a;let r=null!==(a=t.taxonomies[i][s])&&void 0!==a&&a;if(!r)continue;let n=o.cloneNode(!0),l=n.querySelector("a");l&&([l.href,l.title,l.textContent]=[r.url,`See more ${r.title}`,r.title],e.append(n))}o.remove()}isTimeField(e){return"TIME"===e.tagName||null!==e.querySelector("time")}formatTimeField(e,t){("TIME"===e.tagName||(e=e.querySelector("time")))&&(e.setAttribute("datetime",t),e.textContent=window.formatTimeAgo(t,"F Y"))}formatField(e,t){e.textContent=t}addTimelineElements(e,t){let[i,s,r,o]=[t.querySelector("span.after-text"),t.querySelector('[data-field="number"] b'),t.querySelector('[data-field="started"] time'),t.querySelector('[data-field="updated"] time')];i&&(i.textContent=`After ${e.number-1} Tx`),s&&(s.textContent=e.number-1),r&&this.formatTimeField(r,e.fields.timeline[0].post_date),o&&this.formatTimeField(o,e.fields.timeline[e.fields.timeline.length-1].post_date)}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach((e=>e.remove()))}defineTemplates(){const e=this.templates,t=this;e.define("feedTerm",{refs:{icon:".icon",span:"span"},setup({el:e,refs:t,manyRefs:i,data:s}){e.dataset.id=s.id,e.dataset.taxonomy=s.taxonomy,t.icon&&(t.icon.className=`icon icon=${s.icon}`),t.span&&(t.span.textContent=s.name)}}),e.define("emptyState"),this.contentTypes.forEach((i=>{e.define(`feedItem${window.uppercaseFirst(i)}`,{refs:{link:"a"},manyRefs:{fields:"[data-field]"},setup({el:e,refs:i,manyRefs:s,data:r}){const o=Object.hasOwn(e.dataset,"timeline");if(s.fields){for(let e of s.fields){if(o&&["timeline","number"].includes(e.dataset.field))continue;const i=!!Object.hasOwn(r.fields,e.dataset.field)&&r.fields[e.dataset.field];i?t.isImageField(r,i)?t.formatImageField(e,i,r):t.isTaxonomyField(r,e.dataset.field)?t.formatTaxonomyField(e,r,e.dataset.field,i):t.isTimeField(e)?t.formatTimeField(e,i):t.formatField(e,i):e.remove()}var a;i.link&&""!==r.url&&(i.link.href=r.url,i.link.title=`View ${null!==(a=r.fields.post_title)&&void 0!==a?a:"Item"}`),o&&t.addTimelineElements(r,e)}}})}))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.feedBlock=new e)}))}))})();
\ No newline at end of file
diff --git a/inc/managers/queue/executors/UploadExecutor.php b/inc/managers/queue/executors/UploadExecutor.php
index 7461cfd..ab2f992 100644
--- a/inc/managers/queue/executors/UploadExecutor.php
+++ b/inc/managers/queue/executors/UploadExecutor.php
@@ -14,7 +14,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 +22,7 @@
 		'image_upload',
 		'video_upload',
 		'document_upload',
-		'update_metadata',
+		'update_image_meta',
 		'temporary_cleanup',
 		'attach_upload_to_content',
 		'process_upload_groups'
@@ -41,7 +41,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),
@@ -176,29 +176,39 @@
 	/**
 	 * 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 = [];
 
 		foreach ($data as $uploadId => $info) {
-			if (!is_array($info) || empty($info['depends_on'])) {
+			if (!is_array($info)) {
 				continue;
 			}
 
 			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++;
@@ -484,21 +494,21 @@
 
 	private function applyMeta(int $attachmentId, array $metadata): void
 	{
-		if (!empty($metadata['title'])) {
+		if (!empty($metadata['image-title'])) {
 			wp_update_post([
 				'ID'         => $attachmentId,
-				'post_title' => sanitize_text_field($metadata['title']),
+				'post_title' => sanitize_text_field($metadata['image-title']),
 			]);
 		}
 
-		if (!empty($metadata['alt'])) {
-			update_post_meta($attachmentId, '_wp_attachment_image_alt', sanitize_text_field($metadata['alt']));
+		if (!empty($metadata['image-alt-text'])) {
+			update_post_meta($attachmentId, '_wp_attachment_image_alt', sanitize_text_field($metadata['image-alt-text']));
 		}
 
-		if (!empty($metadata['caption'])) {
+		if (!empty($metadata['image-caption'])) {
 			wp_update_post([
 				'ID'           => $attachmentId,
-				'post_excerpt' => sanitize_textarea_field($metadata['caption']),
+				'post_excerpt' => sanitize_textarea_field($metadata['image-caption']),
 			]);
 		}
 	}
diff --git a/inc/meta/Field.php b/inc/meta/Field.php
new file mode 100644
index 0000000..0efb8e3
--- /dev/null
+++ b/inc/meta/Field.php
@@ -0,0 +1,84 @@
+<?php
+namespace JVBase\meta;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Single field data container
+ * Holds value, config, and tracks dirty state
+ */
+final class Field
+{
+	public string $name;
+	public mixed $value;
+	public mixed $originalValue;
+	public array $config;
+	public bool $isDirty = false;
+	public bool $isValid = true;
+	public array $errors = [];
+
+	public function __construct(string $name, mixed $value, array $config = [])
+	{
+		$this->name = $name;
+		$this->value = $value;
+		$this->originalValue = $value;
+		$this->config = $config;
+	}
+
+	public function set(mixed $value): self
+	{
+		$this->value = $value;
+		$this->isDirty = ($value !== $this->originalValue);
+		return $this;
+	}
+
+	public function get(): mixed
+	{
+		return $this->value;
+	}
+
+	public function markClean(): self
+	{
+		$this->originalValue = $this->value;
+		$this->isDirty = false;
+		return $this;
+	}
+
+	public function reset(): self
+	{
+		$this->value = $this->originalValue;
+		$this->isDirty = false;
+		return $this;
+	}
+
+	public function addError(string $message): self
+	{
+		$this->errors[] = $message;
+		$this->isValid = false;
+		return $this;
+	}
+
+	public function clearErrors(): self
+	{
+		$this->errors = [];
+		$this->isValid = true;
+		return $this;
+	}
+
+	public function type(): string
+	{
+		return $this->config['type'] ?? 'text';
+	}
+
+	public function isWpDefault(): bool
+	{
+		return $this->config['_wp_default'] ?? false;
+	}
+
+	public function isTaxonomy(): bool
+	{
+		return $this->type() === 'taxonomy' && !isset($this->config['taxonomy_type']);
+	}
+}
diff --git a/inc/meta/Item.php b/inc/meta/Item.php
new file mode 100644
index 0000000..33ae50c
--- /dev/null
+++ b/inc/meta/Item.php
@@ -0,0 +1,115 @@
+<?php
+namespace JVBase\meta;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Data container for a single WordPress object (post, term, user, or options)
+ * Holds the WP object reference and a collection of Field instances
+ */
+final class Item
+{
+	public int|string|null $id;
+	public string $objectType;      // post, term, user, options
+	public ?string $contentType;    // tattoo, artist, style (without BASE prefix)
+	public ?object $wpObject;       // WP_Post, WP_Term, WP_User
+
+	/** @var array<string, Field> */
+	public array $fields = [];
+
+	/** @var array<string, array> Raw field configs from registry */
+	public array $fieldConfigs = [];
+
+	public ?string $baseKey = null; // For options
+
+	public const WP_DEFAULTS = [
+		'post' => [
+			'post_title',
+			'post_excerpt',
+			'post_content',
+			'post_date',
+			'post_status',
+			'post_modified',
+			'post_thumbnail',
+			'menu_order'
+		],
+		'user' => [
+			'first_name',
+			'last_name',
+			'display_name',
+			'description',
+			'user_email',
+		],
+		'term' => [
+			'term_name',
+			'description'
+		]
+	];
+
+	public function __construct(
+		int|string|null $id,
+		string $objectType,
+		?string $contentType = null
+	) {
+		$this->id = $id;
+		$this->objectType = $objectType;
+		$this->contentType = $contentType;
+	}
+
+	public function hasField(string $name): bool
+	{
+		return isset($this->fields[$name]) || isset($this->fieldConfigs[$name]);
+	}
+
+	public function getField(string $name): ?Field
+	{
+		return $this->fields[$name] ?? null;
+	}
+
+	public function setField(Field $field): self
+	{
+		$this->fields[$field->name] = $field;
+		return $this;
+	}
+
+	public function getFieldConfig(string $name): ?array
+	{
+		return $this->fieldConfigs[$name] ?? null;
+	}
+
+	public function isWpDefault(string $name): bool
+	{
+		$defaults = self::WP_DEFAULTS[$this->objectType] ?? [];
+		return in_array($name, $defaults, true);
+	}
+
+	public function getDirtyFields(): array
+	{
+		return array_filter($this->fields, fn(Field $f) => $f->isDirty);
+	}
+
+	public function getInvalidFields(): array
+	{
+		return array_filter($this->fields, fn(Field $f) => !$f->isValid);
+	}
+
+	public function markAllClean(): self
+	{
+		foreach ($this->fields as $field) {
+			$field->markClean();
+		}
+		return $this;
+	}
+
+	public function hasDirtyFields(): bool
+	{
+		return !empty($this->getDirtyFields());
+	}
+
+	public function isValid(): bool
+	{
+		return empty($this->getInvalidFields());
+	}
+}
diff --git a/inc/meta/Meta.php b/inc/meta/Meta.php
new file mode 100644
index 0000000..05810e6
--- /dev/null
+++ b/inc/meta/Meta.php
@@ -0,0 +1,399 @@
+<?php
+namespace JVBase\meta;
+
+use Exception;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Main facade for meta operations
+ * Fluent API for getting/setting meta values with validation & sanitization
+ */
+class Meta
+{
+	protected Item $item;
+	protected Storage $storage;
+	protected MetaValidator $validator;
+	protected MetaSanitizer $sanitizer;
+	protected MetaTypeManager $typeManager;
+
+	protected bool $autoValidate = true;
+	protected bool $autoSanitize = true;
+
+	// ─────────────────────────────────────────────────────────────
+	// Factory Methods
+	// ─────────────────────────────────────────────────────────────
+
+	public static function forPost(int $id): self
+	{
+		return new self($id, 'post');
+	}
+
+	public static function forTerm(int $id): self
+	{
+		return new self($id, 'term');
+	}
+
+	public static function forUser(int $id): self
+	{
+		return new self($id, 'user');
+	}
+
+	public static function forOptions(?string $baseKey = null): self
+	{
+		$instance = new self($baseKey, 'options');
+		$instance->item->baseKey = $baseKey;
+		return $instance;
+	}
+
+	// ─────────────────────────────────────────────────────────────
+	// Constructor
+	// ─────────────────────────────────────────────────────────────
+
+	public function __construct(int|string|null $id, string $type)
+	{
+		$this->storage = new Storage();
+		$this->validator = new MetaValidator();
+		$this->sanitizer = new MetaSanitizer();
+		$this->typeManager = new MetaTypeManager();
+
+		$this->item = $this->buildItem($id, $type);
+	}
+
+	protected function buildItem(int|string|null $id, string $type): Item
+	{
+		$contentType = null;
+		$wpObject = null;
+
+		if ($id && $type !== 'options') {
+			[$wpObject, $contentType] = match ($type) {
+				'post' => [get_post($id), jvbNoBase(get_post_type($id))],
+				'term' => [get_term($id), jvbNoBase(get_term($id)->taxonomy)],
+				'user', 'integrations' => [get_user_by('id', $id), jvbUserRole($id)],
+				default => [null, null]
+			};
+		}
+
+		$item = new Item($id, $type, $contentType);
+		$item->wpObject = $wpObject;
+		$item->fieldConfigs = $this->loadFieldConfigs($contentType, $type);
+
+		// Mark WP defaults in configs
+		$defaults = Item::WP_DEFAULTS[$type] ?? [];
+		foreach ($defaults as $name) {
+			if (!isset($item->fieldConfigs[$name])) {
+				$item->fieldConfigs[$name] = ['type' => 'text', '_wp_default' => true];
+			} else {
+				$item->fieldConfigs[$name]['_wp_default'] = true;
+			}
+		}
+
+		return $item;
+	}
+
+	protected function loadFieldConfigs(?string $contentType, string $objectType): array
+	{
+		if (!$contentType && $objectType !== 'options') {
+			return [];
+		}
+
+		return jvbGetFields($contentType ?? 'options', $objectType);
+	}
+
+	// ─────────────────────────────────────────────────────────────
+	// Magic Methods for Fluent Access
+	// ─────────────────────────────────────────────────────────────
+
+	public function __get(string $name): mixed
+	{
+		return $this->get($name);
+	}
+
+	public function __set(string $name, mixed $value): void
+	{
+		$this->set($name, $value);
+	}
+
+	public function __isset(string $name): bool
+	{
+		return $this->item->hasField($name);
+	}
+
+	// ─────────────────────────────────────────────────────────────
+	// Core API
+	// ─────────────────────────────────────────────────────────────
+
+	/**
+	 * Get a field value
+	 */
+	public function get(string $name): mixed
+	{
+		// Return from loaded field if exists
+		if ($field = $this->item->getField($name)) {
+			return $field->get();
+		}
+
+		// Load from storage
+		$value = $this->storage->get($this->item, $name);
+		$config = $this->item->getFieldConfig($name) ?? ['type' => 'text'];
+
+		$field = new Field($name, $value, $config);
+		$this->item->setField($field);
+
+		return $value;
+	}
+
+	/**
+	 * Set a field value (validates & sanitizes)
+	 */
+	public function set(string $name, mixed $value): self
+	{
+		$config = $this->item->getFieldConfig($name);
+
+		if (!$config) {
+			// Allow setting unknown fields with minimal config
+			$config = ['type' => 'text', 'name' => $name];
+		}
+
+		$config['name'] = $name;
+
+		// Validate
+		if ($this->autoValidate && !$this->validator->validate($value, $config)) {
+			$field = $this->item->getField($name) ?? new Field($name, $value, $config);
+			$field->addError("Validation failed for {$name}");
+			$this->item->setField($field);
+			return $this;
+		}
+
+		// Sanitize
+		if ($this->autoSanitize) {
+			$value = $this->sanitizer->sanitize($value, $config);
+		}
+
+		// Get or create field
+		$field = $this->item->getField($name);
+
+		if ($field) {
+			$field->set($value);
+		} else {
+			// Need to load original to track dirty state
+			$original = $this->storage->get($this->item, $name);
+			$field = new Field($name, $original, $config);
+			$field->set($value);
+			$this->item->setField($field);
+		}
+
+		return $this;
+	}
+
+	/**
+	 * Get multiple fields
+	 */
+	public function getAll(array $fields = []): array
+	{
+		if (empty($fields) || $fields === ['all']) {
+			$fields = array_keys($this->item->fieldConfigs);
+		}
+
+		// Load all from storage
+		$values = $this->storage->getAll($this->item, $fields);
+
+		// Create Field instances
+		foreach ($values as $name => $value) {
+			if (!$this->item->getField($name)) {
+				$config = $this->item->getFieldConfig($name) ?? ['type' => 'text'];
+				$this->item->setField(new Field($name, $value, $config));
+			}
+		}
+
+		return $values;
+	}
+
+	/**
+	 * Set multiple fields
+	 */
+	public function setAll(array $data): self
+	{
+		foreach ($data as $name => $value) {
+			$this->set($name, $value);
+		}
+		return $this;
+	}
+
+	/**
+	 * Save all dirty fields to database
+	 */
+	public function save(bool $updateTimestamp = true): bool
+	{
+		if (!$this->item->isValid()) {
+			JVB()->error()->log('meta', 'Cannot save: validation errors exist', [
+				'fields' => array_keys($this->item->getInvalidFields())
+			], 'warning');
+			return false;
+		}
+
+		// Check for field overrides before saving
+		foreach ($this->item->getDirtyFields() as $field) {
+			if ($this->checkOverrides($field)) {
+				$field->markClean();
+			}
+		}
+
+		return $this->storage->save($this->item, $updateTimestamp);
+	}
+
+	/**
+	 * Delete a field value
+	 */
+	public function delete(string $name): bool
+	{
+		$result = $this->storage->delete($this->item, $name);
+
+		if ($result && $field = $this->item->getField($name)) {
+			$field->set($this->getDefaultValue($name));
+			$field->markClean();
+		}
+
+		return $result;
+	}
+
+	// ─────────────────────────────────────────────────────────────
+	// Utility Methods
+	// ─────────────────────────────────────────────────────────────
+
+	/**
+	 * Get all dirty (changed) fields
+	 */
+	public function getDirty(): array
+	{
+		return array_map(
+			fn(Field $f) => $f->value,
+			$this->item->getDirtyFields()
+		);
+	}
+
+	/**
+	 * Check if any fields have changed
+	 */
+	public function isDirty(): bool
+	{
+		return $this->item->hasDirtyFields();
+	}
+
+	/**
+	 * Discard all unsaved changes
+	 */
+	public function reset(): self
+	{
+		foreach ($this->item->fields as $field) {
+			$field->reset();
+		}
+		return $this;
+	}
+
+	/**
+	 * Get validation errors
+	 */
+	public function getErrors(): array
+	{
+		$errors = [];
+		foreach ($this->item->getInvalidFields() as $name => $field) {
+			$errors[$name] = $field->errors;
+		}
+		return $errors;
+	}
+
+	/**
+	 * Check if valid (no validation errors)
+	 */
+	public function isValid(): bool
+	{
+		return $this->item->isValid();
+	}
+
+	/**
+	 * Disable auto-validation for bulk operations
+	 */
+	public function withoutValidation(): self
+	{
+		$this->autoValidate = false;
+		return $this;
+	}
+
+	/**
+	 * Disable auto-sanitization
+	 */
+	public function withoutSanitization(): self
+	{
+		$this->autoSanitize = false;
+		return $this;
+	}
+
+	/**
+	 * Get the underlying item (for rendering, etc)
+	 */
+	public function item(): Item
+	{
+		return $this->item;
+	}
+
+	/**
+	 * Get field configuration
+	 */
+	public function config(string $name): ?array
+	{
+		return $this->item->getFieldConfig($name);
+	}
+
+	/**
+	 * Get all field configurations
+	 */
+	public function configs(): array
+	{
+		return $this->item->fieldConfigs;
+	}
+
+	// ─────────────────────────────────────────────────────────────
+	// Protected Helpers
+	// ─────────────────────────────────────────────────────────────
+
+	protected function checkOverrides(Field $field): bool
+	{
+		$name = $field->name;
+		$type = $field->type();
+		$value = $field->value;
+
+		do_action('jvb_meta_update', $name, $value, $this->item->objectType);
+
+		$overrides = [
+			BASE . 'update_' . $name,
+			BASE . 'update_' . $type,
+			'jvb_update_' . $name,
+			'jvb_update_' . $type,
+		];
+
+		foreach ($overrides as $override) {
+			if (function_exists($override)) {
+				$override($this->item->id, $value);
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	protected function getDefaultValue(string $name): mixed
+	{
+		$config = $this->item->getFieldConfig($name);
+		$type = $config['type'] ?? 'text';
+
+		return match ($this->typeManager->getMetaType($type)) {
+			'object', 'array' => [],
+			'boolean' => false,
+			'integer' => 0,
+			default => '',
+		};
+	}
+}
diff --git a/inc/meta/Storage.php b/inc/meta/Storage.php
new file mode 100644
index 0000000..b2ea8ac
--- /dev/null
+++ b/inc/meta/Storage.php
@@ -0,0 +1,305 @@
+<?php
+namespace JVBase\meta;
+
+use Exception;
+use wpdb;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Handles persistence of meta values to WordPress
+ * Pure storage layer - no validation or sanitization
+ */
+class Storage
+{
+	protected wpdb $wpdb;
+
+	public function __construct()
+	{
+		global $wpdb;
+		$this->wpdb = $wpdb;
+	}
+
+	/**
+	 * Load a single field value from database
+	 */
+	public function get(Item $item, string $name): mixed
+	{
+		if ($item->isWpDefault($name)) {
+			return $this->getWpDefault($item, $name);
+		}
+
+		$metaKey = BASE . $name;
+
+		return match ($item->objectType) {
+			'post' => get_post_meta($item->id, $metaKey, true),
+			'term' => get_term_meta($item->id, $metaKey, true),
+			'user', 'integrations' => get_user_meta($item->id, $metaKey, true),
+			'options' => $this->getOption($item, $name),
+			default => ''
+		};
+	}
+
+	/**
+	 * Load multiple field values in a single query
+	 */
+	public function getAll(Item $item, array $fieldNames): array
+	{
+		if (empty($fieldNames) || !$item->id) {
+			return [];
+		}
+
+		$defaults = Item::WP_DEFAULTS[$item->objectType] ?? [];
+		$wpFields = array_intersect($defaults, $fieldNames);
+		$metaFields = array_diff($fieldNames, $wpFields);
+
+		$values = [];
+
+		// Get meta fields in bulk
+		if (!empty($metaFields)) {
+			$values = $this->bulkGetMeta($item, $metaFields);
+		}
+
+		// Get WP default fields
+		foreach ($wpFields as $name) {
+			$values[$name] = $this->getWpDefault($item, $name);
+		}
+
+		return $values;
+	}
+
+	/**
+	 * Save a single field
+	 */
+	public function saveField(Item $item, Field $field): bool
+	{
+		if ($field->isWpDefault()) {
+			return $this->saveWpDefault($item, $field);
+		}
+
+		if ($field->isTaxonomy()) {
+			return $this->saveTaxonomyField($item, $field);
+		}
+
+		$metaKey = BASE . $field->name;
+
+		return match ($item->objectType) {
+			'post' => update_post_meta($item->id, $metaKey, $field->value) !== false,
+			'term' => update_term_meta($item->id, $metaKey, $field->value) !== false,
+			'user', 'integrations' => update_user_meta($item->id, $metaKey, $field->value) !== false,
+			'options' => $this->saveOption($item, $field),
+			default => false
+		};
+	}
+
+	/**
+	 * Save all dirty fields on an item
+	 */
+	public function save(Item $item, bool $updateTimestamp = true): bool
+	{
+		$dirty = $item->getDirtyFields();
+
+		if (empty($dirty)) {
+			return true;
+		}
+
+		$this->wpdb->query('START TRANSACTION');
+
+		try {
+			foreach ($dirty as $field) {
+				if (!$this->saveField($item, $field)) {
+					throw new Exception("Failed to save field: {$field->name}");
+				}
+				$field->markClean();
+			}
+
+			$this->wpdb->query('COMMIT');
+
+			// Update post modified timestamp
+			if ($updateTimestamp && $item->objectType === 'post' && $item->id) {
+				wp_update_post(['ID' => $item->id]);
+			}
+
+			$this->clearCache($item);
+
+			return true;
+
+		} catch (Exception $e) {
+			$this->wpdb->query('ROLLBACK');
+			JVB()->error()->log('meta_storage', $e->getMessage(), [
+				'item_id' => $item->id,
+				'object_type' => $item->objectType,
+				'fields' => array_keys($dirty)
+			], 'error');
+			return false;
+		}
+	}
+
+	/**
+	 * Delete a field value
+	 */
+	public function delete(Item $item, string $name): bool
+	{
+		$metaKey = BASE . $name;
+
+		return match ($item->objectType) {
+			'post' => delete_post_meta($item->id, $metaKey),
+			'term' => delete_term_meta($item->id, $metaKey),
+			'user', 'integrations' => delete_user_meta($item->id, $metaKey),
+			'options' => delete_option($this->optionKey($item, $name)),
+			default => false
+		};
+	}
+
+	// ─────────────────────────────────────────────────────────────
+	// Protected helpers
+	// ─────────────────────────────────────────────────────────────
+
+	protected function getWpDefault(Item $item, string $name): mixed
+	{
+		if (in_array($name, ['featured_image', 'post_thumbnail'])) {
+			return get_post_thumbnail_id($item->id);
+		}
+
+		return match ($item->objectType) {
+			'post' => $this->getPostField($item, $name),
+			'term' => $this->getTermField($item, $name),
+			'user' => $this->getUserField($item, $name),
+			default => ''
+		};
+	}
+
+	protected function getPostField(Item $item, string $name): mixed
+	{
+		return match ($name) {
+			'post_title' => get_the_title($item->id),
+			'post_excerpt' => get_the_excerpt($item->id),
+			'post_content' => get_post_field('post_content', $item->id),
+			default => $item->wpObject->$name ?? ''
+		};
+	}
+
+	protected function getTermField(Item $item, string $name): mixed
+	{
+		return match ($name) {
+			'term_name' => get_term_field('name', $item->id),
+			'description' => get_term_field('description', $item->id),
+			default => ''
+		};
+	}
+
+	protected function getUserField(Item $item, string $name): mixed
+	{
+		return match ($name) {
+			'display_name' => get_the_author_meta('display_name', $item->id),
+			'user_email' => get_the_author_meta('user_email', $item->id),
+			'first_name' => get_the_author_meta('first_name', $item->id),
+			'last_name' => get_the_author_meta('last_name', $item->id),
+			default => $item->wpObject->$name ?? ''
+		};
+	}
+
+	protected function saveWpDefault(Item $item, Field $field): bool
+	{
+		$name = $field->name;
+		$value = $field->value;
+
+		if (in_array($name, ['featured_image', 'post_thumbnail'])) {
+			return set_post_thumbnail($item->id, $value) !== false;
+		}
+
+		return match ($item->objectType) {
+			'post' => wp_update_post(['ID' => $item->id, $name => $value]) !== 0,
+			'term' => !is_wp_error(wp_update_term($item->id, $item->wpObject->taxonomy, [
+				$name => $value,
+				'slug' => $name === 'term_name' ? sanitize_title($value) : null
+			])),
+			'user' => wp_update_user(['ID' => $item->id, $name => $value]) !== 0,
+			default => false
+		};
+	}
+
+	protected function saveTaxonomyField(Item $item, Field $field): bool
+	{
+		$taxonomy = jvbCheckBase($field->config['taxonomy']);
+		$value = $field->value;
+
+		if (empty(trim($value))) {
+			wp_set_object_terms($item->id, [], $taxonomy, false);
+			return true;
+		}
+
+		$termIds = array_map('intval', array_filter(explode(',', $value)));
+		$result = wp_set_object_terms($item->id, $termIds, $taxonomy, false);
+
+		return !is_wp_error($result);
+	}
+
+	protected function bulkGetMeta(Item $item, array $fields): array
+	{
+		[$table, $idColumn] = $this->getTableInfo($item->objectType);
+
+		if (!$table) {
+			return [];
+		}
+
+		$metaKeys = array_map(fn($f) => BASE . $f, $fields);
+		$placeholders = implode(',', array_fill(0, count($metaKeys), '%s'));
+
+		$query = $this->wpdb->prepare(
+			"SELECT meta_key, meta_value FROM {$table}
+             WHERE {$idColumn} = %d AND meta_key IN ({$placeholders})",
+			array_merge([$item->id], $metaKeys)
+		);
+
+		$results = $this->wpdb->get_results($query, ARRAY_A);
+
+		$values = array_fill_keys($fields, '');
+
+		foreach ($results as $row) {
+			$key = str_replace(BASE, '', $row['meta_key']);
+			$values[$key] = maybe_unserialize($row['meta_value']);
+		}
+
+		return $values;
+	}
+
+	protected function getTableInfo(string $objectType): array
+	{
+		return match ($objectType) {
+			'post' => [$this->wpdb->postmeta, 'post_id'],
+			'term' => [$this->wpdb->termmeta, 'term_id'],
+			'user', 'integrations' => [$this->wpdb->usermeta, 'user_id'],
+			default => [null, null]
+		};
+	}
+
+	protected function getOption(Item $item, string $name): mixed
+	{
+		return get_option($this->optionKey($item, $name));
+	}
+
+	protected function saveOption(Item $item, Field $field): bool
+	{
+		return update_option($this->optionKey($item, $field->name), $field->value);
+	}
+
+	protected function optionKey(Item $item, string $name): string
+	{
+		return $item->baseKey
+			? BASE . $item->baseKey . '_' . $name
+			: BASE . $name;
+	}
+
+	protected function clearCache(Item $item): void
+	{
+		match ($item->objectType) {
+			'post' => clean_post_cache($item->id),
+			'term' => clean_term_cache($item->id),
+			'user', 'integrations' => clean_user_cache($item->id),
+			default => null
+		};
+	}
+}
diff --git a/inc/meta/_setup.php b/inc/meta/_setup.php
index 5af0e09..8e84bde 100644
--- a/inc/meta/_setup.php
+++ b/inc/meta/_setup.php
@@ -1,8 +1,17 @@
 <?php
-	require(JVB_DIR . '/inc/meta/MetaTypeManager.php');
+//REFACTOR
+require(JVB_DIR . '/inc/meta/Meta.php'); 		//Main facade
+require(JVB_DIR . '/inc/meta/Item.php'); 		//Data container for one object
+require(JVB_DIR . '/inc/meta/Field.php'); 		//Single field with value, dirty state
+require(JVB_DIR . '/inc/meta/Storage.php'); 	//Persistence layer
+require(JVB_DIR . '/inc/meta/MetaTypeManager.php'); //Keep as is
+require(JVB_DIR . '/inc/meta/MetaValidator.php');	//Keep as is
+
+
+require(JVB_DIR . '/inc/meta/MetaRenderer.php');	//decouple from manager
+require(JVB_DIR . '/inc/meta/MetaForm.php');		//decouple from manager
+
+//OLD SYSTEM
 	require(JVB_DIR . '/inc/meta/MetaManager.php');
 	require(JVB_DIR . '/inc/meta/MetaRegistry.php');
 	require(JVB_DIR . '/inc/meta/MetaSanitizer.php');
-	require(JVB_DIR . '/inc/meta/MetaValidator.php');
-	require(JVB_DIR . '/inc/meta/MetaRenderer.php');
-	require(JVB_DIR . '/inc/meta/MetaForm.php');
diff --git a/inc/rest/PermissionHandler.php b/inc/rest/PermissionHandler.php
new file mode 100644
index 0000000..409d479
--- /dev/null
+++ b/inc/rest/PermissionHandler.php
@@ -0,0 +1,514 @@
+<?php
+namespace JVBase\rest;
+
+use WP_REST_Request;
+use WP_Error;
+use WP_User;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Centralized Permission Handler for REST Routes
+ *
+ * Provides reusable permission callbacks and utilities for route authentication.
+ */
+class PermissionHandler
+{
+	/**
+	 * Check if the 'user' parameter in request matches current logged-in user
+	 * Common pattern for user-specific endpoints
+	 */
+	public static function userMatch(WP_REST_Request $request): bool|WP_Error
+	{
+		if (!is_user_logged_in()) {
+			return new WP_Error(
+				'not_logged_in',
+				'You must be logged in to access this resource',
+				['status' => 401]
+			);
+		}
+
+		$requestedUserId = $request->get_param('user');
+
+		// No user param specified - allow (controller will handle)
+		if (empty($requestedUserId)) {
+			return true;
+		}
+
+		$currentUserId = get_current_user_id();
+
+		if ((int) $requestedUserId !== $currentUserId) {
+			return new WP_Error(
+				'forbidden',
+				'You can only access your own resources',
+				['status' => 403]
+			);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Check if current user is an administrator
+	 */
+	public static function isAdmin(WP_REST_Request $request): bool|WP_Error
+	{
+		if (!is_user_logged_in()) {
+			return new WP_Error(
+				'not_logged_in',
+				'You must be logged in',
+				['status' => 401]
+			);
+		}
+
+		if (!current_user_can('manage_options')) {
+			return new WP_Error(
+				'forbidden',
+				'Administrator access required',
+				['status' => 403]
+			);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Check if current user is verified (has skip_moderation capability)
+	 */
+	public static function isVerified(WP_REST_Request $request): bool|WP_Error
+	{
+		if (!is_user_logged_in()) {
+			return new WP_Error(
+				'not_logged_in',
+				'You must be logged in',
+				['status' => 401]
+			);
+		}
+
+		if (!current_user_can('skip_moderation')) {
+			return new WP_Error(
+				'not_verified',
+				'Account verification required',
+				['status' => 403]
+			);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Check if current user has a specific role
+	 */
+	public static function hasRole(WP_REST_Request $request, string $role): bool|WP_Error
+	{
+		if (!is_user_logged_in()) {
+			return new WP_Error(
+				'not_logged_in',
+				'You must be logged in',
+				['status' => 401]
+			);
+		}
+
+		$user = wp_get_current_user();
+		$role = self::normalizeRole($role);
+
+		if (!in_array($role, $user->roles, true)) {
+			return new WP_Error(
+				'forbidden',
+				'You do not have the required role',
+				['status' => 403]
+			);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Check if current user has any of the specified roles
+	 */
+	public static function hasAnyRole(WP_REST_Request $request, array $roles): bool|WP_Error
+	{
+		if (!is_user_logged_in()) {
+			return new WP_Error(
+				'not_logged_in',
+				'You must be logged in',
+				['status' => 401]
+			);
+		}
+
+		$user = wp_get_current_user();
+		$normalizedRoles = array_map([self::class, 'normalizeRole'], $roles);
+
+		foreach ($user->roles as $userRole) {
+			if (in_array($userRole, $normalizedRoles, true)) {
+				return true;
+			}
+		}
+
+		return new WP_Error(
+			'forbidden',
+			'You do not have any of the required roles',
+			['status' => 403]
+		);
+	}
+
+	/**
+	 * Check if current user has all specified roles
+	 */
+	public static function hasAllRoles(WP_REST_Request $request, array $roles): bool|WP_Error
+	{
+		if (!is_user_logged_in()) {
+			return new WP_Error(
+				'not_logged_in',
+				'You must be logged in',
+				['status' => 401]
+			);
+		}
+
+		$user = wp_get_current_user();
+		$normalizedRoles = array_map([self::class, 'normalizeRole'], $roles);
+
+		foreach ($normalizedRoles as $role) {
+			if (!in_array($role, $user->roles, true)) {
+				return new WP_Error(
+					'forbidden',
+					'You do not have all required roles',
+					['status' => 403]
+				);
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * Check if current user has a specific capability
+	 */
+	public static function hasCapability(WP_REST_Request $request, string $capability): bool|WP_Error
+	{
+		if (!is_user_logged_in()) {
+			return new WP_Error(
+				'not_logged_in',
+				'You must be logged in',
+				['status' => 401]
+			);
+		}
+
+		if (!current_user_can($capability)) {
+			return new WP_Error(
+				'forbidden',
+				'You do not have the required permission',
+				['status' => 403]
+			);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Check if current user has any of the specified capabilities
+	 */
+	public static function hasAnyCapability(WP_REST_Request $request, array $capabilities): bool|WP_Error
+	{
+		if (!is_user_logged_in()) {
+			return new WP_Error(
+				'not_logged_in',
+				'You must be logged in',
+				['status' => 401]
+			);
+		}
+
+		foreach ($capabilities as $cap) {
+			if (current_user_can($cap)) {
+				return true;
+			}
+		}
+
+		return new WP_Error(
+			'forbidden',
+			'You do not have any of the required permissions',
+			['status' => 403]
+		);
+	}
+
+	/**
+	 * Check if user owns a specific post
+	 */
+	public static function ownsPost(WP_REST_Request $request, string $paramName = 'post_id'): bool|WP_Error
+	{
+		if (!is_user_logged_in()) {
+			return new WP_Error(
+				'not_logged_in',
+				'You must be logged in',
+				['status' => 401]
+			);
+		}
+
+		$postId = $request->get_param($paramName);
+
+		if (empty($postId)) {
+			return new WP_Error(
+				'missing_param',
+				'Post ID is required',
+				['status' => 400]
+			);
+		}
+
+		$post = get_post($postId);
+
+		if (!$post) {
+			return new WP_Error(
+				'not_found',
+				'Post not found',
+				['status' => 404]
+			);
+		}
+
+		if ((int) $post->post_author !== get_current_user_id()) {
+			// Allow admins to bypass ownership check
+			if (!current_user_can('manage_options')) {
+				return new WP_Error(
+					'forbidden',
+					'You can only modify your own content',
+					['status' => 403]
+				);
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * Check if user can edit a specific post type
+	 */
+	public static function canEditPostType(WP_REST_Request $request, string $postType): bool|WP_Error
+	{
+		if (!is_user_logged_in()) {
+			return new WP_Error(
+				'not_logged_in',
+				'You must be logged in',
+				['status' => 401]
+			);
+		}
+
+		$postTypeObj = get_post_type_object($postType);
+
+		if (!$postTypeObj) {
+			return new WP_Error(
+				'invalid_post_type',
+				'Invalid post type',
+				['status' => 400]
+			);
+		}
+
+		if (!current_user_can($postTypeObj->cap->edit_posts)) {
+			return new WP_Error(
+				'forbidden',
+				'You cannot edit this content type',
+				['status' => 403]
+			);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Verify nonce from request header
+	 */
+	public static function verifyNonce(WP_REST_Request $request, string $action = 'wp_rest', string $header = 'X-WP-Nonce'): bool|WP_Error
+	{
+		$nonce = $request->get_header($header);
+
+		if (empty($nonce)) {
+			return new WP_Error(
+				'missing_nonce',
+				'Security token is missing',
+				['status' => 403]
+			);
+		}
+
+		if (!wp_verify_nonce($nonce, $action)) {
+			return new WP_Error(
+				'invalid_nonce',
+				'Invalid or expired security token',
+				['status' => 403]
+			);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Verify action-specific nonce (e.g., 'dash-{user_id}')
+	 */
+	public static function verifyActionNonce(WP_REST_Request $request, string $actionPrefix, string $header = 'X-Action-Nonce'): bool|WP_Error
+	{
+		$userId = $request->get_param('user') ?: get_current_user_id();
+		$action = $actionPrefix . $userId;
+
+		return self::verifyNonce($request, $action, $header);
+	}
+
+	/**
+	 * Combined permission check: user match + rate limit
+	 */
+	public static function userMatchWithRateLimit(WP_REST_Request $request): bool|WP_Error
+	{
+		static $rateLimiter = null;
+
+		if ($rateLimiter === null) {
+			$rateLimiter = new RateLimiter();
+		}
+
+		// Check rate limit first
+		if (!$rateLimiter->checkLimit($request)) {
+			return new WP_Error(
+				'rate_limit',
+				'Too many requests. Please wait before trying again.',
+				['status' => 429]
+			);
+		}
+
+		return self::userMatch($request);
+	}
+
+	/**
+	 * Create a custom permission callback combining multiple checks
+	 *
+	 * Usage:
+	 *   PermissionHandler::combine(['logged_in', 'verified'])
+	 *   PermissionHandler::combine([['role' => 'artist'], ['capability' => 'edit_posts']])
+	 */
+	public static function combine(array $checks): callable
+	{
+		return function(WP_REST_Request $request) use ($checks) {
+			foreach ($checks as $check) {
+				$result = match (true) {
+					$check === 'logged_in' => is_user_logged_in() ?: new WP_Error('not_logged_in', 'Login required', ['status' => 401]),
+					$check === 'admin' => self::isAdmin($request),
+					$check === 'verified' => self::isVerified($request),
+					$check === 'user' => self::userMatch($request),
+					is_array($check) && isset($check['role']) => self::hasRole($request, $check['role']),
+					is_array($check) && isset($check['roles']) => self::hasAnyRole($request, $check['roles']),
+					is_array($check) && isset($check['capability']) => self::hasCapability($request, $check['capability']),
+					is_callable($check) => $check($request),
+					default => true,
+				};
+
+				if (is_wp_error($result)) {
+					return $result;
+				}
+
+				if ($result === false) {
+					return new WP_Error('forbidden', 'Access denied', ['status' => 403]);
+				}
+			}
+
+			return true;
+		};
+	}
+
+	/**
+	 * Create an OR permission callback (passes if ANY check passes)
+	 */
+	public static function any(array $checks): callable
+	{
+		return function(WP_REST_Request $request) use ($checks) {
+			$lastError = null;
+
+			foreach ($checks as $check) {
+				$result = match (true) {
+					$check === 'logged_in' => is_user_logged_in(),
+					$check === 'admin' => self::isAdmin($request),
+					$check === 'verified' => self::isVerified($request),
+					$check === 'user' => self::userMatch($request),
+					is_array($check) && isset($check['role']) => self::hasRole($request, $check['role']),
+					is_array($check) && isset($check['capability']) => self::hasCapability($request, $check['capability']),
+					is_callable($check) => $check($request),
+					default => false,
+				};
+
+				// If it's a successful check (true), pass
+				if ($result === true) {
+					return true;
+				}
+
+				// Track last error for reporting
+				if (is_wp_error($result)) {
+					$lastError = $result;
+				}
+			}
+
+			return $lastError ?: new WP_Error('forbidden', 'Access denied', ['status' => 403]);
+		};
+	}
+
+	/**
+	 * Normalize role name (add BASE prefix if needed)
+	 */
+	private static function normalizeRole(string $role): string
+	{
+		if (defined('BASE') && !str_starts_with($role, BASE)) {
+			// Check if it's a WordPress core role
+			$coreRoles = ['administrator', 'editor', 'author', 'contributor', 'subscriber'];
+			if (!in_array($role, $coreRoles, true)) {
+				return BASE . $role;
+			}
+		}
+		return $role;
+	}
+
+	/**
+	 * Get current user for request (cached)
+	 */
+	public static function getCurrentUser(): ?WP_User
+	{
+		static $user = null;
+
+		if ($user === null && is_user_logged_in()) {
+			$user = wp_get_current_user();
+		}
+
+		return $user instanceof WP_User ? $user : null;
+	}
+
+	/**
+	 * Check if request is from same origin (basic CSRF protection)
+	 */
+	public static function verifySameOrigin(WP_REST_Request $request): bool|WP_Error
+	{
+		$origin = $request->get_header('Origin');
+		$referer = $request->get_header('Referer');
+
+		$siteUrl = get_site_url();
+		$siteDomain = parse_url($siteUrl, PHP_URL_HOST);
+
+		// Check origin header
+		if ($origin) {
+			$originDomain = parse_url($origin, PHP_URL_HOST);
+			if ($originDomain !== $siteDomain) {
+				return new WP_Error(
+					'cross_origin',
+					'Cross-origin requests not allowed',
+					['status' => 403]
+				);
+			}
+		}
+
+		// Check referer as fallback
+		if (!$origin && $referer) {
+			$refererDomain = parse_url($referer, PHP_URL_HOST);
+			if ($refererDomain !== $siteDomain) {
+				return new WP_Error(
+					'cross_origin',
+					'Cross-origin requests not allowed',
+					['status' => 403]
+				);
+			}
+		}
+
+		return true;
+	}
+}
diff --git a/inc/rest/RateLimits.php b/inc/rest/RateLimits.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/inc/rest/RateLimits.php
diff --git a/inc/rest/Response.php b/inc/rest/Response.php
new file mode 100644
index 0000000..d77980d
--- /dev/null
+++ b/inc/rest/Response.php
@@ -0,0 +1,437 @@
+<?php
+namespace JVBase\rest;
+
+use WP_REST_Response;
+use WP_Error;
+use DateTime;
+use DateTimeZone;
+use Exception;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Fluent Response Builder for REST API
+ *
+ * Provides consistent response formatting, caching headers, and error handling.
+ *
+ * Usage:
+ *   return Response::success(['user' => $userData]);
+ *   return Response::error('Not found', 'not_found', 404);
+ *   return Response::paginated($items, $total, $page, $perPage);
+ */
+class Response
+{
+	private array $data = [];
+	private int $status = 200;
+	private array $headers = [];
+	private ?WP_REST_Response $response = null;
+
+	/**
+	 * Create a success response
+	 */
+	public static function success(array $data = [], int $status = 200): WP_REST_Response
+	{
+		$response = new self();
+		$response->data = array_merge(['success' => true], $data);
+		$response->status = $status;
+		return $response->build();
+	}
+
+	/**
+	 * Create an error response
+	 */
+	public static function error(
+		string $message,
+		string $code = 'error',
+		int $status = 400,
+		?string $field = null,
+		array $extra = []
+	): WP_REST_Response {
+		$data = [
+			'success' => false,
+			'code' => $code,
+			'message' => $message,
+		];
+
+		if ($field !== null) {
+			$data['field'] = $field;
+		}
+
+		if (!empty($extra)) {
+			$data = array_merge($data, $extra);
+		}
+
+		return new WP_REST_Response($data, $status);
+	}
+
+	/**
+	 * Create a validation error response (422)
+	 */
+	public static function validationError(array $errors, string $message = 'Validation failed'): WP_REST_Response
+	{
+		return new WP_REST_Response([
+			'success' => false,
+			'code' => 'validation_error',
+			'message' => $message,
+			'errors' => $errors,
+		], 422);
+	}
+
+	/**
+	 * Create an unauthorized response (401)
+	 */
+	public static function unauthorized(string $message = 'Authentication required'): WP_REST_Response
+	{
+		return self::error($message, 'unauthorized', 401);
+	}
+
+	/**
+	 * Create a forbidden response (403)
+	 */
+	public static function forbidden(string $message = 'Access denied'): WP_REST_Response
+	{
+		return self::error($message, 'forbidden', 403);
+	}
+
+	/**
+	 * Create a not found response (404)
+	 */
+	public static function notFound(string $message = 'Resource not found'): WP_REST_Response
+	{
+		return self::error($message, 'not_found', 404);
+	}
+
+	/**
+	 * Create a rate limit response (429)
+	 */
+	public static function rateLimited(int $retryAfter = 60, string $message = 'Too many requests'): WP_REST_Response
+	{
+		$response = self::error($message, 'rate_limit', 429);
+		$response->header('Retry-After', (string) $retryAfter);
+		return $response;
+	}
+
+	/**
+	 * Create a server error response (500)
+	 */
+	public static function serverError(string $message = 'An unexpected error occurred'): WP_REST_Response
+	{
+		return self::error($message, 'server_error', 500);
+	}
+
+	/**
+	 * Create a paginated response
+	 */
+	public static function paginated(
+		array $items,
+		int $total,
+		int $page = 1,
+		int $perPage = 20,
+		array $extra = []
+	): WP_REST_Response {
+		$totalPages = (int) ceil($total / $perPage);
+
+		$data = array_merge([
+			'success' => true,
+			'items' => $items,
+			'pagination' => [
+				'total' => $total,
+				'per_page' => $perPage,
+				'current_page' => $page,
+				'total_pages' => $totalPages,
+				'has_more' => $page < $totalPages,
+			],
+		], $extra);
+
+		$response = new WP_REST_Response($data, 200);
+
+		// Add pagination headers
+		$response->header('X-Total-Count', (string) $total);
+		$response->header('X-Total-Pages', (string) $totalPages);
+		$response->header('X-Current-Page', (string) $page);
+		$response->header('X-Per-Page', (string) $perPage);
+
+		return $response;
+	}
+
+	/**
+	 * Create a collection response (list without pagination)
+	 */
+	public static function collection(array $items, array $extra = []): WP_REST_Response
+	{
+		$data = array_merge([
+			'success' => true,
+			'items' => $items,
+			'total' => count($items),
+		], $extra);
+
+		return new WP_REST_Response($data, 200);
+	}
+
+	/**
+	 * Create a single item response
+	 */
+	public static function item(array $item, string $key = 'item'): WP_REST_Response
+	{
+		return new WP_REST_Response([
+			'success' => true,
+			$key => $item,
+		], 200);
+	}
+
+	/**
+	 * Create a created response (201)
+	 */
+	public static function created(array $data = [], ?string $location = null): WP_REST_Response
+	{
+		$response = new WP_REST_Response(
+			array_merge(['success' => true], $data),
+			201
+		);
+
+		if ($location) {
+			$response->header('Location', $location);
+		}
+
+		return $response;
+	}
+
+	/**
+	 * Create a no content response (204)
+	 */
+	public static function noContent(): WP_REST_Response
+	{
+		return new WP_REST_Response(null, 204);
+	}
+
+	/**
+	 * Create a response from WP_Error
+	 */
+	public static function fromError(WP_Error $error): WP_REST_Response
+	{
+		$data = $error->get_error_data();
+		$status = is_array($data) && isset($data['status']) ? $data['status'] : 400;
+
+		return self::error(
+			$error->get_error_message(),
+			$error->get_error_code(),
+			$status
+		);
+	}
+
+	/**
+	 * Build a response with fluent interface
+	 */
+	public static function make(array $data = []): self
+	{
+		$instance = new self();
+		$instance->data = $data;
+		return $instance;
+	}
+
+	/**
+	 * Set response data
+	 */
+	public function data(array $data): self
+	{
+		$this->data = array_merge($this->data, $data);
+		return $this;
+	}
+
+	/**
+	 * Set HTTP status code
+	 */
+	public function status(int $status): self
+	{
+		$this->status = $status;
+		return $this;
+	}
+
+	/**
+	 * Add a response header
+	 */
+	public function header(string $name, string $value): self
+	{
+		$this->headers[$name] = $value;
+		return $this;
+	}
+
+	/**
+	 * Add multiple headers
+	 */
+	public function headers(array $headers): self
+	{
+		$this->headers = array_merge($this->headers, $headers);
+		return $this;
+	}
+
+	/**
+	 * Add cache control headers
+	 */
+	public function cache(int $maxAge = 300, bool $private = true): self
+	{
+		$directive = $private ? 'private' : 'public';
+		$this->headers['Cache-Control'] = "{$directive}, max-age={$maxAge}";
+		$this->headers['Vary'] = 'Cookie';
+		return $this;
+	}
+
+	/**
+	 * Add no-cache headers
+	 */
+	public function noCache(): self
+	{
+		$this->headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0';
+		$this->headers['Pragma'] = 'no-cache';
+		return $this;
+	}
+
+	/**
+	 * Add ETag header for conditional requests
+	 */
+	public function etag(string $data): self
+	{
+		$this->headers['ETag'] = '"' . md5($data) . '"';
+		return $this;
+	}
+
+	/**
+	 * Add Last-Modified header
+	 */
+	public function lastModified(string|int $timestamp): self
+	{
+		if (is_string($timestamp)) {
+			$timestamp = strtotime($timestamp);
+		}
+		$this->headers['Last-Modified'] = gmdate('D, d M Y H:i:s', $timestamp) . ' GMT';
+		return $this;
+	}
+
+	/**
+	 * Build and return the response
+	 */
+	public function build(): WP_REST_Response
+	{
+		$response = new WP_REST_Response($this->data, $this->status);
+
+		foreach ($this->headers as $name => $value) {
+			$response->header($name, $value);
+		}
+
+		return $response;
+	}
+
+	/**
+	 * Convert to WP_REST_Response (alias for build)
+	 */
+	public function toResponse(): WP_REST_Response
+	{
+		return $this->build();
+	}
+
+	// =========================================================================
+	// UTILITY METHODS
+	// =========================================================================
+
+	/**
+	 * Format MySQL datetime to ISO 8601 timestamp
+	 */
+	public static function formatTimestamp(?string $mysqlDatetime): ?string
+	{
+		if (empty($mysqlDatetime)) {
+			return null;
+		}
+
+		try {
+			$wpTimezone = wp_timezone();
+			$date = new DateTime($mysqlDatetime, $wpTimezone);
+			$date->setTimezone(new DateTimeZone('UTC'));
+			return $date->format('c');
+		} catch (Exception $e) {
+			return null;
+		}
+	}
+
+	/**
+	 * Format an array of items with a callback
+	 */
+	public static function formatItems(array $items, callable $formatter): array
+	{
+		return array_map($formatter, $items);
+	}
+
+	/**
+	 * Pluck specific fields from items
+	 */
+	public static function pluck(array $items, array $fields): array
+	{
+		return array_map(function($item) use ($fields) {
+			$result = [];
+			foreach ($fields as $field) {
+				if (is_array($item) && array_key_exists($field, $item)) {
+					$result[$field] = $item[$field];
+				} elseif (is_object($item) && property_exists($item, $field)) {
+					$result[$field] = $item->$field;
+				}
+			}
+			return $result;
+		}, $items);
+	}
+
+	/**
+	 * Add server timestamp to response data
+	 */
+	public static function withTimestamp(array $data): array
+	{
+		$data['timestamp'] = date('c');
+		$data['server_time'] = date('c');
+		return $data;
+	}
+
+	/**
+	 * Create response for queued operation
+	 */
+	public static function queued(string $operationId, string $message = 'Queued for processing', array $extra = []): WP_REST_Response
+	{
+		return self::success(array_merge([
+			'message' => $message,
+			'operation_id' => $operationId,
+			'status' => 'queued',
+		], $extra), 202);
+	}
+
+	/**
+	 * Create response for operation status
+	 */
+	public static function operationStatus(
+		string $id,
+		string $status,
+		int $progress = 0,
+		int $total = 0,
+		?string $message = null
+	): WP_REST_Response {
+		$data = [
+			'operation_id' => $id,
+			'status' => $status,
+			'progress' => $progress,
+			'total' => $total,
+		];
+
+		if ($total > 0) {
+			$data['progress_percentage'] = round(($progress / $total) * 100);
+		}
+
+		if ($message) {
+			$data['message'] = $message;
+		}
+
+		return self::success($data);
+	}
+}
+
+/**
+ * Alias for backward compatibility
+ */
+class ResponseBuilder extends Response {}
diff --git a/inc/rest/Rest.php b/inc/rest/Rest.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/inc/rest/Rest.php
diff --git a/inc/rest/Route.php b/inc/rest/Route.php
new file mode 100644
index 0000000..2350eea
--- /dev/null
+++ b/inc/rest/Route.php
@@ -0,0 +1,480 @@
+<?php
+namespace JVBase\rest;
+
+use WP_REST_Request;
+use WP_Error;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Fluent REST Route Builder
+ *
+ * Usage:
+ *   Route::get('queue', [$this, 'getQueue'])->auth('user')->args(['status' => 'string']);
+ *   Route::resource('content')->get(...)->post(...)->delete(false);
+ */
+class Route
+{
+	private string $path;
+	private array $methods = [];
+	private array $currentMethod = [];
+	private bool $registered = false;
+
+	private static string $namespace = 'jvb/v1';
+	private static ?RateLimiter $rateLimiter = null;
+
+	/**
+	 * Create a resource route (supports multiple methods)
+	 */
+	public static function resource(string $path): self
+	{
+		return new self($path);
+	}
+
+	/**
+	 * Create a GET route
+	 */
+	public static function get(string $path, callable|array $callback): self
+	{
+		return (new self($path))->addMethod('GET', $callback);
+	}
+
+	/**
+	 * Create a POST route
+	 */
+	public static function post(string $path, callable|array $callback): self
+	{
+		return (new self($path))->addMethod('POST', $callback);
+	}
+
+	/**
+	 * Create a PUT route
+	 */
+	public static function put(string $path, callable|array $callback): self
+	{
+		return (new self($path))->addMethod('PUT', $callback);
+	}
+
+	/**
+	 * Create a PATCH route
+	 */
+	public static function patch(string $path, callable|array $callback): self
+	{
+		return (new self($path))->addMethod('PATCH', $callback);
+	}
+
+	/**
+	 * Create a DELETE route
+	 */
+	public static function delete(string $path, callable|array $callback): self
+	{
+		return (new self($path))->addMethod('DELETE', $callback);
+	}
+
+	/**
+	 * Set custom namespace (defaults to 'jvb/v1')
+	 */
+	public static function setNamespace(string $namespace): void
+	{
+		self::$namespace = $namespace;
+	}
+
+	/**
+	 * Get current namespace
+	 */
+	public static function getNamespace(): string
+	{
+		return self::$namespace;
+	}
+
+	private function __construct(string $path)
+	{
+		$this->path = '/' . ltrim($path, '/');
+	}
+
+	/**
+	 * Add GET method to resource
+	 */
+	public function get(callable|array $callback): self
+	{
+		return $this->addMethod('GET', $callback);
+	}
+
+	/**
+	 * Add POST method to resource
+	 */
+	public function post(callable|array $callback): self
+	{
+		return $this->addMethod('POST', $callback);
+	}
+
+	/**
+	 * Add PUT method to resource
+	 */
+	public function put(callable|array $callback): self
+	{
+		return $this->addMethod('PUT', $callback);
+	}
+
+	/**
+	 * Add PATCH method to resource
+	 */
+	public function patch(callable|array $callback): self
+	{
+		return $this->addMethod('PATCH', $callback);
+	}
+
+	/**
+	 * Add DELETE method to resource (pass false to explicitly disable)
+	 */
+	public function delete(callable|array|false $callback): self
+	{
+		if ($callback === false) {
+			return $this; // Explicitly disabled
+		}
+		return $this->addMethod('DELETE', $callback);
+	}
+
+	/**
+	 * Internal method to add HTTP method
+	 */
+	private function addMethod(string $method, callable|array $callback): self
+	{
+		// Finalize previous method if exists
+		if (!empty($this->currentMethod)) {
+			$this->methods[] = $this->currentMethod;
+		}
+
+		$this->currentMethod = [
+			'methods' => $method,
+			'callback' => $callback,
+			'permission_callback' => '__return_true',
+			'args' => [],
+		];
+
+		return $this;
+	}
+
+	/**
+	 * Set authentication/permission requirement
+	 *
+	 * @param string|array|false $auth
+	 *   - 'public' or false: Anyone can access
+	 *   - 'user': Logged-in user must match 'user' param in request
+	 *   - 'logged_in': Any logged-in user
+	 *   - 'admin': Users with manage_options capability
+	 *   - 'verified': Users with skip_moderation capability
+	 *   - ['capability' => 'edit_posts']: Specific capability check
+	 *   - ['role' => 'artist']: Specific role check
+	 *   - ['roles' => ['artist', 'admin']]: Multiple roles (OR)
+	 *   - callable: Custom permission callback
+	 */
+	public function auth(string|array|callable|false $auth): self
+	{
+		if (empty($this->currentMethod)) {
+			return $this;
+		}
+
+		$this->currentMethod['permission_callback'] = match (true) {
+			$auth === false || $auth === 'public'
+			=> '__return_true',
+
+			$auth === 'logged_in'
+			=> 'is_user_logged_in',
+
+			$auth === 'user'
+			=> [PermissionHandler::class, 'userMatch'],
+
+			$auth === 'admin'
+			=> [PermissionHandler::class, 'isAdmin'],
+
+			$auth === 'verified'
+			=> [PermissionHandler::class, 'isVerified'],
+
+			is_callable($auth)
+			=> $auth,
+
+			is_array($auth) && isset($auth['capability'])
+			=> fn(WP_REST_Request $req) => current_user_can($auth['capability']),
+
+			is_array($auth) && isset($auth['role'])
+			=> fn(WP_REST_Request $req) => PermissionHandler::hasRole($req, $auth['role']),
+
+			is_array($auth) && isset($auth['roles'])
+			=> fn(WP_REST_Request $req) => PermissionHandler::hasAnyRole($req, $auth['roles']),
+
+			default
+			=> '__return_true',
+		};
+
+		return $this;
+	}
+
+	/**
+	 * Add rate limiting to the route
+	 *
+	 * @param int $limit Maximum requests
+	 * @param int $window Time window in seconds
+	 */
+	public function rateLimit(int $limit = 60, int $window = 60): self
+	{
+		if (empty($this->currentMethod)) {
+			return $this;
+		}
+
+		$originalCallback = $this->currentMethod['permission_callback'];
+
+		$this->currentMethod['permission_callback'] = function(WP_REST_Request $request) use ($originalCallback, $limit, $window) {
+			// Initialize rate limiter if needed
+			if (self::$rateLimiter === null) {
+				self::$rateLimiter = new RateLimiter();
+			}
+
+			// Check rate limit first
+			if (!self::$rateLimiter->checkLimit($request, $limit, $window)) {
+				return new WP_Error(
+					'rate_limit',
+					'Too many requests. Please wait before trying again.',
+					['status' => 429]
+				);
+			}
+
+			// Then check original permission
+			if ($originalCallback === '__return_true') {
+				return true;
+			}
+
+			if (is_callable($originalCallback)) {
+				return call_user_func($originalCallback, $request);
+			}
+
+			return true;
+		};
+
+		return $this;
+	}
+
+	/**
+	 * Require nonce verification
+	 *
+	 * @param string $action Nonce action name (default: 'wp_rest')
+	 * @param string $header Header name containing nonce (default: 'X-WP-Nonce')
+	 */
+	public function nonce(string $action = 'wp_rest', string $header = 'X-WP-Nonce'): self
+	{
+		if (empty($this->currentMethod)) {
+			return $this;
+		}
+
+		$originalCallback = $this->currentMethod['permission_callback'];
+
+		$this->currentMethod['permission_callback'] = function(WP_REST_Request $request) use ($originalCallback, $action, $header) {
+			$nonce = $request->get_header($header);
+
+			if (!wp_verify_nonce($nonce, $action)) {
+				return new WP_Error(
+					'invalid_nonce',
+					'Invalid or expired security token',
+					['status' => 403]
+				);
+			}
+
+			// Then check original permission
+			if ($originalCallback === '__return_true') {
+				return true;
+			}
+
+			if (is_callable($originalCallback)) {
+				return call_user_func($originalCallback, $request);
+			}
+
+			return true;
+		};
+
+		return $this;
+	}
+
+	/**
+	 * Define route arguments with shorthand syntax
+	 *
+	 * @param array $args Argument definitions
+	 *   Shorthand: ['name' => 'type|required|default:value|enum:a,b,c']
+	 *   Full: ['name' => ['type' => 'string', 'required' => true, ...]]
+	 *
+	 * Examples:
+	 *   'status' => 'string'
+	 *   'status' => 'string|required'
+	 *   'status' => 'string|default:all'
+	 *   'status' => 'string|enum:pending,completed,failed'
+	 *   'limit' => 'integer|default:50|min:1|max:100'
+	 *   'ids' => 'array|required'
+	 */
+	public function args(array $args): self
+	{
+		if (empty($this->currentMethod)) {
+			return $this;
+		}
+
+		foreach ($args as $name => $definition) {
+			$this->currentMethod['args'][$name] = $this->parseArgDefinition($definition);
+		}
+
+		return $this;
+	}
+
+	/**
+	 * Add a single argument
+	 */
+	public function arg(string $name, string|array $definition): self
+	{
+		if (empty($this->currentMethod)) {
+			return $this;
+		}
+
+		$this->currentMethod['args'][$name] = $this->parseArgDefinition($definition);
+		return $this;
+	}
+
+	/**
+	 * Parse shorthand argument definition into WP REST format
+	 */
+	private function parseArgDefinition(string|array $definition): array
+	{
+		// Already full format
+		if (is_array($definition)) {
+			return $definition;
+		}
+
+		$parts = explode('|', $definition);
+		$type = trim($parts[0]);
+
+		$arg = [
+			'type' => $type,
+			'required' => false,
+		];
+
+		// Add sanitize callback based on type
+		$arg['sanitize_callback'] = match ($type) {
+			'integer', 'int' => 'absint',
+			'string' => 'sanitize_text_field',
+			'email' => 'sanitize_email',
+			'url' => 'esc_url_raw',
+			'boolean', 'bool' => 'rest_sanitize_boolean',
+			default => null,
+		};
+
+		// Normalize type for WP
+		if ($type === 'int') {
+			$arg['type'] = 'integer';
+		} elseif ($type === 'bool') {
+			$arg['type'] = 'boolean';
+		}
+
+		// Parse modifiers
+		foreach (array_slice($parts, 1) as $part) {
+			$part = trim($part);
+
+			if ($part === 'required') {
+				$arg['required'] = true;
+			} elseif (str_starts_with($part, 'default:')) {
+				$value = substr($part, 8);
+				$arg['default'] = $this->castValue($value, $type);
+			} elseif (str_starts_with($part, 'enum:')) {
+				$arg['enum'] = array_map('trim', explode(',', substr($part, 5)));
+			} elseif (str_starts_with($part, 'min:')) {
+				$arg['minimum'] = (int) substr($part, 4);
+			} elseif (str_starts_with($part, 'max:')) {
+				$arg['maximum'] = (int) substr($part, 4);
+			} elseif (str_starts_with($part, 'desc:')) {
+				$arg['description'] = substr($part, 5);
+			} elseif (str_starts_with($part, 'pattern:')) {
+				$arg['pattern'] = substr($part, 8);
+			}
+		}
+
+		// Remove null sanitize callback
+		if ($arg['sanitize_callback'] === null) {
+			unset($arg['sanitize_callback']);
+		}
+
+		return $arg;
+	}
+
+	/**
+	 * Cast value to appropriate type
+	 */
+	private function castValue(string $value, string $type): mixed
+	{
+		return match ($type) {
+			'integer', 'int' => (int) $value,
+			'boolean', 'bool' => filter_var($value, FILTER_VALIDATE_BOOLEAN),
+			'number' => (float) $value,
+			'array' => explode(',', $value),
+			default => $value,
+		};
+	}
+
+	/**
+	 * Register the route with WordPress
+	 */
+	public function register(): self
+	{
+		if ($this->registered) {
+			return $this;
+		}
+
+		// Add current method if not empty
+		if (!empty($this->currentMethod)) {
+			$this->methods[] = $this->currentMethod;
+			$this->currentMethod = [];
+		}
+
+		if (empty($this->methods)) {
+			return $this;
+		}
+
+		// Register single method or array of methods
+		$config = count($this->methods) === 1 ? $this->methods[0] : $this->methods;
+
+		register_rest_route(self::$namespace, $this->path, $config);
+
+		$this->registered = true;
+
+		return $this;
+	}
+
+	/**
+	 * Auto-register on destruction
+	 */
+	public function __destruct()
+	{
+		if (!$this->registered && !empty($this->methods) || !empty($this->currentMethod)) {
+			$this->register();
+		}
+	}
+
+	/**
+	 * Convert WordPress route pattern to more readable format
+	 * Converts: queue/{id} to queue/(?P<id>[a-zA-Z0-9_-]+)
+	 */
+	public static function pattern(string $path, array $patterns = []): string
+	{
+		$defaults = [
+			'id' => '[a-zA-Z0-9_-]+',
+			'slug' => '[a-zA-Z0-9_-]+',
+			'type' => '[a-zA-Z_]+',
+			'int' => '[0-9]+',
+		];
+
+		$patterns = array_merge($defaults, $patterns);
+
+		return preg_replace_callback('/\{(\w+)(?::(\w+))?\}/', function($matches) use ($patterns) {
+			$name = $matches[1];
+			$type = $matches[2] ?? $name;
+			$pattern = $patterns[$type] ?? $patterns['id'];
+			return "(?P<{$name}>{$pattern})";
+		}, $path);
+	}
+}
diff --git a/inc/rest/_setup.php b/inc/rest/_setup.php
index e228477..b94d38f 100644
--- a/inc/rest/_setup.php
+++ b/inc/rest/_setup.php
@@ -1,6 +1,14 @@
 <?php
 use JVBase\utility\Features;
 
+//NEW METHOD
+//require(JVB_DIR . '/inc/rest/Route.php');
+//require(JVB_DIR . '/inc/rest/PermissionHandler.php');
+//require(JVB_DIR . '/inc/rest/Response.php');
+//require(JVB_DIR . '/inc/rest/Rest.php'); //Refactored RestRouteManager.php
+//require(JVB_DIR . '/inc/rest/RateLimits.php');
+
+//OLD METHOD
 require(JVB_DIR . '/inc/rest/RateLimiter.php');
 require(JVB_DIR . '/inc/rest/RestRouteManager.php');
 require(JVB_DIR . '/inc/rest/RegisterRoutes.php');
diff --git a/inc/rest/routes/QueueRoutes.php b/inc/rest/routes/QueueRoutes.php
index 400717b..3654ae0 100644
--- a/inc/rest/routes/QueueRoutes.php
+++ b/inc/rest/routes/QueueRoutes.php
@@ -11,26 +11,26 @@
 use DateTimeZone;
 
 if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
+	exit; // Exit if accessed directly
 }
 
 class QueueRoutes extends RestRouteManager
 {
-    public function __construct()
-    {
-        $this->cache_name = 'queue';
-        $this->cache_ttl = 300;
-        parent::__construct();
+	public function __construct()
+	{
+		$this->cache_name = 'queue';
+		$this->cache_ttl = 300;
+		parent::__construct();
 
 		if (JVB_TESTING) {
 			$this->cache->flush();
 		}
-    }
+	}
 
-    /**
-     * Registers queue routes
-     * @return void
-     */
+	/**
+	 * Registers queue routes
+	 * @return void
+	 */
 	public function registerRoutes():void
 	{
 		register_rest_route($this->namespace, '/queue', [
@@ -79,48 +79,14 @@
 				]
 			]
 		]);
-
-		register_rest_route($this->namespace, '/queue/errors', [
-			'methods'   => 'GET',
-			'callback'  => [$this, 'getOperationErrors'],
-			'permission_callback' => [$this, 'checkPermission'],
-		]);
-
-		register_rest_route($this->namespace, '/queue/poll', [
-			'methods'   => 'GET',
-			'callback'  => [$this, 'pollQueue'],
-			'permission_callback' => [$this, 'checkPermission'],
-			'args' => [
-				'since' => [
-					'type' => 'string',
-					'description' => 'ISO timestamp - only return operations updated after this'
-				],
-				'ids' => [
-					'type' => 'string',
-					'description' => 'Comma-separated IDs to check'
-				]
-			]
-		]);
-
-		register_rest_route($this->namespace, '/queue/(?P<id>[a-zA-Z0-9_-]+)', [
-			'methods'   => 'GET',
-			'callback'  => [$this, 'getOperation'],
-			'permission_callback' => [$this, 'checkPermission'],
-			'args' => [
-				'id' => [
-					'required' => true,
-					'type' => 'string',
-				]
-			]
-		]);
 	}
 
-    /**
-     * Get queue operations with optional filtering
-     *
-     * @param WP_REST_Request $request
-     * @return WP_REST_Response
-     */
+	/**
+	 * Get queue operations with optional filtering
+	 *
+	 * @param WP_REST_Request $request
+	 * @return WP_REST_Response
+	 */
 	public function getQueue(WP_REST_Request $request): WP_REST_Response
 	{
 		$user_id = $request->get_param('user');
@@ -209,7 +175,7 @@
 	/**
 	 * Format Operation object for API response
 	 */
-	protected function formatOperationFromObject(\JVBase\managers\queue\Operation $op, bool $full = false): array
+	protected function formatOperationFromObject(\JVBase\managers\queue\Operation $op): array
 	{
 		$formatted = [
 			'id' => $op->id,
@@ -217,31 +183,31 @@
 			'status' => $this->mapStateToStatus($op->state, $op->outcome),
 			'progress_count' => $op->processedItems,
 			'count' => $op->totalItems,
-			'title' => $this->getOperationTitle($op->type, $op->requestData),
-			'created_at' => $this->formatTimestamp($op->scheduledAt),
-			'updated_at' => $this->formatTimestamp($op->completedAt ?? $op->startedAt ?? $op->scheduledAt),
+			'retries' => $op->retries,
+			'data' => $op->requestData,
+			'result' => $op->result ?? [],
 		];
 
-		if ($op->processedItems > 0 && $op->totalItems > 0) {
-			$formatted['progress_percentage'] = round(($op->processedItems / $op->totalItems) * 100);
+		$formatted['created_at'] = $this->formatTimestamp($op->scheduledAt);
+		$formatted['updated_at'] = $this->formatTimestamp($op->completedAt ?? $op->startedAt ?? $op->scheduledAt);
+
+		if ($op->state === 'completed' && $op->completedAt) {
+			$formatted['completed_at'] = $this->formatTimestamp($op->completedAt);
 		}
 
 		if ($op->errorMessage) {
 			$formatted['error_message'] = $op->errorMessage;
 		}
 
-		// Only include heavy data when requested
-		if ($full) {
-			$formatted['data'] = $op->requestData;
-			$formatted['result'] = $op->result ?? [];
-			$formatted['retries'] = $op->retries;
-			$formatted['user_dismissed'] = $op->userDismissed;
-
-			if ($op->state === 'completed' && $op->completedAt) {
-				$formatted['completed_at'] = $this->formatTimestamp($op->completedAt);
-			}
+		if ($formatted['count'] > 0) {
+			$formatted['progress_percentage'] = round(
+				($formatted['progress_count'] / $formatted['count']) * 100
+			);
 		}
 
+		$formatted['title'] = $this->getOperationTitle($op->type, $op->requestData);
+		$formatted['user_dismissed'] = $op->userDismissed;
+
 		return $formatted;
 	}
 
@@ -276,12 +242,12 @@
 		return $base_title;
 	}
 
-    /**
-     * Update operation status (dismiss or retry)
-     *
-     * @param WP_REST_Request $request
-     * @return WP_REST_Response
-     */
+	/**
+	 * Update operation status (dismiss or retry)
+	 *
+	 * @param WP_REST_Request $request
+	 * @return WP_REST_Response
+	 */
 	public function handleAction(WP_REST_Request $request): WP_REST_Response
 	{
 		$data = $request->get_json_params();
@@ -436,70 +402,4 @@
 			'cleanup_reason' => null
 		];
 	}
-
-
-	public function pollQueue(WP_REST_Request $request): WP_REST_Response
-	{
-		$user_id = $request->get_param('user');
-		$since = $request->get_param('since');
-		$ids = $request->get_param('ids');
-
-		$filters = [
-			'not_dismissed' => true,
-			'limit' => 50,
-		];
-
-		if (!empty($ids)) {
-			$filters['ids'] = array_map('trim', explode(',', $ids));
-		}
-
-		$operations = JVB()->queue()->getUserOperations($user_id, $filters);
-
-		// Filter by timestamp if provided
-		if ($since) {
-			$sinceTime = strtotime($since);
-			$operations = array_filter($operations, function($op) use ($sinceTime) {
-				$updatedAt = strtotime($op->completedAt ?? $op->startedAt ?? $op->scheduledAt);
-				return $updatedAt > $sinceTime;
-			});
-		}
-
-		// Return minimal data
-		$items = array_map(function($op) {
-			return [
-				'id' => $op->id,
-				'status' => $this->mapStateToStatus($op->state, $op->outcome),
-				'progress_count' => $op->processedItems,
-				'count' => $op->totalItems,
-				'updated_at' => $this->formatTimestamp($op->completedAt ?? $op->startedAt ?? $op->scheduledAt),
-				'error_message' => $op->errorMessage,
-			];
-		}, $operations);
-
-		return new WP_REST_Response([
-			'items' => array_values($items),
-			'server_time' => date('c'),
-			'has_active' => count(array_filter($items, fn($i) => in_array($i['status'], ['pending', 'processing']))) > 0,
-		]);
-	}
-
-	public function getOperation(WP_REST_Request $request): WP_REST_Response
-	{
-		$id = $request->get_param('id');
-		$user_id = $request->get_param('user');
-
-		$op = JVB()->queue()->get($id);
-
-		if (!$op || $op->userId !== $user_id) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Operation not found'
-			], 404);
-		}
-
-		return new WP_REST_Response([
-			'success' => true,
-			'operation' => $this->formatOperationFromObject($op, true) // Full data
-		]);
-	}
 }
diff --git a/inc/rest/routes/UploadRoutes.php b/inc/rest/routes/UploadRoutes.php
index 1da0412..c0016bf 100644
--- a/inc/rest/routes/UploadRoutes.php
+++ b/inc/rest/routes/UploadRoutes.php
@@ -57,7 +57,7 @@
 		));
 
 		// Metadata updates
-		$registry->register('update_metadata', new TypeConfig(
+		$registry->register('update_image_meta', new TypeConfig(
 			executor: $executor
 		));
 
@@ -797,23 +797,15 @@
 			}
 			$pending = [];
 			$attachments = array_filter($items, function ($item) {
-				return array_key_exists('attachmentId', $item);
+				return array_key_exists('attachmentId', $item) || array_key_exists('uploadId', $item);
 			});
-			if (count($attachments) !== count($items)) {
-				$pending = array_filter($items, function ($item) {
-					return array_key_exists('uploadId',$item);
-				});
-			}
 
 
 			if (!empty($attachments)) {
-				// Phase 2B: Direct attachment update (images already processed)
-				return $this->updateMeta($attachments, $data['user']);
+				error_log('Attachments: '.print_r($attachments, true));
+				return $this->queueMetaUpdate($attachments, $data['user']);
 			}
-			elseif (!empty($pending)) {
-				// Phase 2A: Queue metadata update with dependency on upload operation
-				return $this->queueMetaUpdate($pending, $data['user']);
-			}
+
 
 			return $this->sendResponse(
 				false,
@@ -849,6 +841,7 @@
 		foreach ($data as $info) {
 			try {
 				$attachment_id = $info['attachmentId'];
+				error_log('Updating attachment ID:'.print_r($attachment_id,true));
 				$ids[] = $attachment_id;
 				unset($info['attachmentId']);
 				// Verify attachment exists and user has permission
@@ -887,22 +880,16 @@
 		$errors = [];
 		$original = count($data);
 		foreach ($data as $uploadID => $info) {
-			if (!array_key_exists('depends_on', $info)) {
-				unset($data[$uploadID]);
-				$errors[$uploadID] = $info;
-				continue;
-			}
-			if (!in_array($info['depends_on'], $depends_on)) {
+			if (array_key_exists('depends_on', $info) && !in_array($info['depends_on'], $depends_on)) {
 				$depends_on[] = $info['depends_on'];
 			}
 		}
 		$operationID = $queue->queueOperation(
-			'update_metadata',
+			'update_image_meta',
 			$user,
 			$data,
 			[
 				'depends_on' => $depends_on,
-				'priority' => 'medium',
 			]
 		);
 
@@ -929,7 +916,7 @@
 			$errors = [];
 			foreach ($operation->depends_on as $dependency) {
 				$operationData = JVB()->queue()->getOperation($dependency);
-				if (!$operationData || $operationData->status !== 'completed') {
+				if (!$operationData || $operationData->state !== 'completed') {
 					throw new Exception('Original upload operation not found or not completed');
 				}
 
diff --git a/src/feed/view.js b/src/feed/view.js
index 1248322..c9b7d59 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -638,10 +638,10 @@
 		];
 
 		if (afterEl) {
-			afterEl.textContent = `After ${item.number} Tx`;
+			afterEl.textContent = `After ${item.number - 1} Tx`;
 		}
 		if (number) {
-			number.textContent = item.fields.number;
+			number.textContent = item.number - 1;
 		}
 		if (started) {
 			this.formatTimeField(started, item.fields.timeline[0]['post_date']);

--
Gitblit v1.10.0