From e6287fd606e6e3220261fd68c394989e6ade0f90 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 02 Jan 2026 23:41:57 +0000
Subject: [PATCH] Merge branch 'main' of https://github.com/jakevdwerf/jvb

---
 assets/js/concise/Queue.js |  655 ++++++++++++++++++++++++-----------------------------------
 1 files changed, 270 insertions(+), 385 deletions(-)

diff --git a/assets/js/concise/Queue.js b/assets/js/concise/Queue.js
index 6b58475..7e3033c 100644
--- a/assets/js/concise/Queue.js
+++ b/assets/js/concise/Queue.js
@@ -5,7 +5,6 @@
 class QueueManager {
 	constructor(config = {}) {
 		this.canUpdateUI = true;
-		console.log('jvbSettings', jvbSettings);
 		this.config = {
 			apiBase: jvbSettings.api,
 			maxRetries: 3,
@@ -15,33 +14,6 @@
 			endpoint: 'queue',
 			...config
 		};
-		this.user = jvbSettings.currentUser;
-
-
-		this.headers = {
-			'X-WP-Nonce': jvbSettings.nonce,
-			...config.headers
-		};
-
-		this.a11y = window.jvbA11y;
-		this.errors = window.jvbError;
-
-		// Initialize DataStore for queue persistence
-		this.store = new window.jvbStore({
-			name: 'queue',
-			endpoint: this.config.endpoint,
-			useIndexedDB: true,
-			TTL: Infinity, //Queue data doesn't expire,
-			showLoading: false
-		});
-
-		this.queue = new Map();
-
-		this.classes = [
-			'offline',
-			'synced',
-			'pending'
-		];
 
 		// Queue state
 		this.isProcessing = false;
@@ -60,44 +32,117 @@
 			'failed_permanent'
 		];
 
+		this.user = window.auth.getUser();
+
+		if (!this.user) {
+			console.log('Queue: User not logged in, queue disabled');
+			this.store = null;
+			this.canUpdateUI = false;
+			return;
+		}
+
+		this.headers = {
+			'X-WP-Nonce': window.auth.getNonce(),
+			...config.headers
+		};
+
+		this.a11y = window.jvbA11y;
+		this.errors = window.jvbError;
+
+		// Initialize DataStore for queue persistence
+		const store = window.jvbStore.register('queue', {
+			storeName: 'queue',
+			keyPath: 'id',
+			endpoint: this.config.endpoint,
+			TTL: Infinity,
+			indexes: [
+				{name: 'status', keyPath: 'status'},
+				{name: 'type', keyPath: 'type'},
+			],
+			showLoading: false,
+			delayFetch: false, // Queue should fetch immediately
+		});
+		this.store = store.queue;
+
+		this.classes = [
+			'offline',
+			'synced',
+			'pending'
+		];
+
+
+
 		// Initialize
 		this.initUI();
 		this.initListeners();
-		this.initQueue();
-
-		if (this.user) {
-			this.ui.toggle.hidden = false;
-			this.ui.panel.hidden = false;
+		if (this.ui.panel) {
+			this.popup = new window.jvbPopup({
+				popup: this.ui.panel,
+				toggle: this.ui.toggle,
+				name: 'Queue Panel',
+			});
 		}
+		this.updateUI = () => window.debouncer.schedule('queue-ui-update', this._updateUI.bind(this), 100);
+		this.initQueue();
 	}
 
 	async initQueue() {
-		const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false)
-
-		if (incomplete.length > 0) {
-			this.startPolling();
-		} else {
+		let polling = this.maybeStartPolling();
+		if (!polling) {
 			this.updateStatusPanel('synced');
 		}
 
+
 		this.store.subscribe((event, data) => {
 			switch (event) {
-				case 'data-fetched':
-				case 'data-cached':
-					this.updateOperationsFromServer(data.data.items);
+				case 'data-loaded':
+				case 'items-saved':
+					this.maybeStartPolling();
+					this.updateUI();
 					break;
-				case 'items-updated':
-					this.updateOperationsFromServer(data.items);
+				case 'item-saved':
+					console.log(data,'Item saved data');
+					if (data.previousItem && data.previousItem.status !== data.item.status) {
+						this.handleOperationStatusChange(data.item, data.previousItem.status);
+					}
+					this.maybeStartPolling();
 					break;
-				case 'item-stored':
-					this.updateOperationsFromServer([data])
+				default:
+					this.updateUI();
 					break;
 			}
 
 		});
+	}
 
-		this.store.fetch();
-		this.notify('queue-initialized', {operations: incomplete});
+	maybeStartPolling()
+	{
+		const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false);
+		if (incomplete.length > 0) {
+			this.startPolling();
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Handle operation status changes and notify subscribers
+	 */
+	handleOperationStatusChange(operation) {
+
+		// Notify based on new status
+		switch(operation.status) {
+			case 'completed':
+				console.log(operation);
+				this.notify('operation-completed', operation);
+				break;
+			case 'failed':
+				this.notify('operation-failed', operation);
+				break;
+			case 'failed_permanent':
+				this.notify('operation-failed-permanent', operation);
+				break;
+		}
 	}
 	/**
 	 *
@@ -118,6 +163,7 @@
 			method: 'POST',
 			headers: {},
 			data: {},
+			sendNow: false,				// true = process immediately
 			canMerge: true,
 			popup: 'Saving changes...',
 			title: 'Operation',
@@ -138,7 +184,15 @@
 			return null;
 		}
 
-		const existingOps = Array.from(this.queue.values()).filter(op=>
+		if (item.sendNow) {
+			this.processOperation(item).then(()=> {});
+			this.store.clearCache();
+			window.debouncer.schedule('fastQueue', this.startPolling.bind(this), 200);
+			this.showQueue();
+			return item.id;
+		}
+
+		const existingOps = Array.from(this.store.data.values()).filter(op=>
 			op.status === 'queued' &&
 			op.endpoint === item.endpoint &&
 			op.canMerge
@@ -156,7 +210,7 @@
 			return existing.id;
 		}
 
-		console.log('Added to Queue: ', item);
+		this.store.clearCache();
 
 		//Add new operation to DataStore
 		this.setQueue(item);
@@ -170,33 +224,28 @@
 
 	}
 
+
 	setQueue(item) {
-		this.queue.set(item.id, item);
-		this.store.setItem(item.id, item);
+		this.store.save(item);
 	}
 
 	updateOperationStatus(itemID, status) {
-		let item = this.queue.get(itemID);
-		if (!item){
-			return;
-		}
+		let item = this.store.get(itemID);
+		if (!item) return;
+
+		// Update status
 		item.status = status;
+
 		this.notify('operation-status', item);
 		this.updateOperationUI(item);
 	}
 
 	getQueue(itemID) {
-		if (this.queue.has(itemID)) {
-			return this.queue.get(itemID);
-		}
-		return this.store.getItem(itemID);
+		return this.store.get(itemID);
 	}
 
 	clearQueue(itemID) {
-		if (this.queue.has(itemID)) {
-			this.queue.delete(itemID);
-		}
-		this.store.clearItem(itemID);
+		this.store.delete(itemID);
 	}
 
 	startActivityTracking() {
@@ -212,8 +261,6 @@
 	}
 
 	resetActivityTimer() {
-		this.lastActivity = Date.now();
-
 		if (this.activityTimer) {
 			clearTimeout(this.activityTimer);
 		}
@@ -236,6 +283,15 @@
 		}
 	}
 
+	hideQueue(){
+		this.ui.panel.hidden = true;
+		this.ui.toggle.hidden = true;
+	}
+	showQueue() {
+		this.ui.panel.hidden = false;
+		this.ui.toggle.hidden = false;
+	}
+
 	setProcessing(on) {
 		this.isProcessing = on;
 		this.ui.toggle.classList.toggle('saving', on);
@@ -262,18 +318,19 @@
 		this.setProcessing(false);
 		this.stopActivityTracking();
 
-		const pending = this.getOperationsByStatus(['queued', 'completed', 'failed_permanent'], false);
-		if (pending.length > 0) {
-			this.startPolling();
-		}
+		this.maybeStartPolling() ? this.showQueue() : this.hideQueue();
 	}
 
-	async processOperation(operation) {
+	async processOperation(operation, skip = false) {
 		try {
-			//update to uploading
-			this.updateOperationStatus(operation.id, 'uploading');
+			if (!skip) {
+				this.updateOperationStatus(operation.id, 'uploading');
 
-			//build request
+				if (operation.data?._isFormData) {
+					operation.data = await this.store.objectToFormData(operation.data);
+				}
+			}
+
 			const url = `${this.config.apiBase}${operation.endpoint}`;
 			let requestBody;
 
@@ -289,7 +346,6 @@
 				});
 				operation.headers['Content-Type'] = 'application/json';
 			}
-
 			const response = await fetch(url, {
 				method: operation.method,
 				headers: operation.headers,
@@ -297,43 +353,17 @@
 			});
 
 			const result = await response.json();
-
+			if (skip) {
+				operation.data = {};
+			}
 			if (response.ok && result.success !== false) {
 				// Handle server-side merge
 				if (result.id && operation.id !== result.id) {
-
-					// Check if the returned ID exists locally
-					const existingOp = this.getQueue(result.id);
-
-					if (existingOp) {
-						// Merge data from both operations
-						existingOp.data = window.deepMerge(existingOp.data, operation.data);
-						existingOp.status = 'pending';
-						existingOp.serverData = result;
-						this.updateOperationStatus(existingOp.id, existingOp.status);
-						// Update the existing operation
-						this.setQueue(existingOp);
-
-						this.removeOperationFromUI(operation.id);
-
-						// Switch reference to the merged operation
-						operation = existingOp;
-					} else {
-						// Server merged with an operation we don't have locally
-						// Update the ID and continue
-						this.clearQueue(operation.id);
-						operation.id = result.id;
-						operation.status = 'pending';
-						operation.serverData = result;
-						this.updateOperationStatus(operation.id, operation.status);
-						this.setQueue(operation);
-					}
+					operation = await this.handleServerMerge(operation, result);
 				} else {
-					// Normal processing - no merge
-					operation.status = 'pending';
+					operation.status = result.status || 'pending';
 					operation.serverData = result;
-					this.updateOperationStatus(operation.id, 'pending');
-					this.setQueue(operation);
+					this.updateOperationStatus(operation.id, operation.status);
 				}
 
 				this.a11y.announce(`${operation.title} sent to server for processing.`);
@@ -359,76 +389,48 @@
 		}
 	}
 
+	async handleServerMerge(operation, result) {
+		const existingOp = this.getQueue(result.id);
+
+		if (existingOp) {
+			// Merge with existing local operation
+			existingOp.data = window.deepMerge(existingOp.data, operation.data);
+			existingOp.status = result.status || 'pending';
+			existingOp.serverData = result;
+			this.updateOperationStatus(existingOp.id, existingOp.status);
+			this.removeOperationFromUI(operation.id);
+			this.clearQueue(operation.id);
+			return existingOp;
+		} else {
+			// Server merged with unknown operation
+			this.clearQueue(operation.id);
+			operation.id = result.id;
+			operation.status = result.status || 'pending';
+			operation.serverData = result;
+			this.updateOperationStatus(operation.id, operation.status);
+			return operation;
+		}
+	}
+
 	startPolling() {
 		if (this.isPolling) return;
+
 		this.isPolling = true;
-		this.pollServer();
-		this.pollTimer = setInterval(() => {
-			this.pollServer();
-		}, this.config.pollInterval);
-
-		this.updateCountdown();
-	}
-
-	pollServer(force = false) {
-		const operations = this.getOperationsByStatus(['pending', 'processing', 'uploading']);
-
-		if (operations.length === 0 && !force) {
-			this.stopPolling();
-			return;
-		}
 		this.updateStatusPanel('pending');
 
-		try {
-			// const operationIds = operations.map(op => op.id);
-			// this.store.setFilter('operation_ids', operationIds.join(','));
-			this.store.fetch();
-		} catch (error) {
-			console.error('Polling error:', error);
-		} finally {
-			this.updateStatusPanel();
-		}
-	}
+		this.pollTimer = setInterval(async () => {
+			try {
+				this.store.clearCache();
+				await this.store.fetch(); // Fetches from server, updates store.data
 
-	async updateOperationsFromServer(serverOperations) {
-		let hasChanges = false;
-		const processedIds = new Set();
-		for (const serverOp of serverOperations) {
-			let operation = (this.queue.has(serverOp.id)) ? this.queue.get(serverOp.id) : {};
-			processedIds.add(serverOp.id);
-			if (serverOp.status !== operation.status) {
-				operation = {
-					... operation,
-					... serverOp
-				};
-				// Update in DataStore
-				this.queue.set(operation.id, operation);
-
-				// Update UI for this operation
-				this.updateOperationStatus(operation.id, operation.status);
+				if (!this.maybeStartPolling()) {
+					this.stopPolling();
+					this.updateStatusPanel('synced');
+				}
+			} catch (error) {
+				console.error('Polling error:', error);
 			}
-		}
-
-		// Clean up operations that were completed/dismissed on server
-		const localOps = this.getOperationsByStatus(['pending', 'processing', 'uploading']);
-		for (const localOp of localOps) {
-			if (!processedIds.has(localOp.id)) {
-				localOp.status = 'completed';
-				localOp.completedAt = Date.now();
-				this.setQueue(localOp);
-				hasChanges = true;
-				this.updateOperationStatus(localOp.id, localOp.status);
-			}
-		}
-
-		// Check if all operations are completed
-		const pendingOps = this.getOperationsByStatus(['pending', 'processing', 'uploading']);
-
-		if (pendingOps.length === 0) {
-			this.stopPolling();
-		}
-
-		this.updateUI();
+		}, this.config.pollInterval);
 	}
 
 	stopPolling() {
@@ -443,7 +445,9 @@
 			this.countdownTimer = null;
 		}
 	}
-
+	getOperationIds(operations) {
+		return operations.map(op => op.id);
+	}
 	/***********************************************************
 	USER ACTIONS
 	 ***********************************************************/
@@ -455,41 +459,29 @@
 	 * @returns {Promise<void>}
 	 */
 	async updateServerOperations(ids, action) {
-		//ensure ids are in an array
-		ids = Array.isArray(ids) ? ids : ((ids.includes(',')) ? ids.split(',') : [ids]);
-		ids = ids.filter((id) => {
+		ids = Array.isArray(ids) ? ids : (ids.includes(',') ? ids.split(',') : [ids]);
+		ids = ids.filter(id => {
 			let item = this.getQueue(id);
 			return this.getAllowedActions(item.status).includes(action);
 		});
 
-		if (ids.length === 0) {
-			return;
-		}
+		if (ids.length === 0) return;
 
-		if (['cancel', 'dismiss'].includes(action)) {
-			ids.forEach(id => {
-				this.removeOperationFromUI(id);
-			});
+		// SINGLE place to handle UI removal
+		const shouldRemove = ['cancel', 'dismiss'].includes(action);
+		if (shouldRemove) {
+			ids.forEach(id => this.removeOperationFromUI(id));
 		}
 
 		try {
-			const url = `${this.config.apiBase}${this.config.endpoint}`;
-
-			const response = await fetch(
-				url,
-				{
-					method: 'POST',
-					headers: {
-						'Content-Type': 'application/json',
-						...this.headers
-					},
-					body: JSON.stringify({ids,action})
-				}
-			);
+			const response = await fetch(`${this.config.apiBase}${this.config.endpoint}`, {
+				method: 'POST',
+				headers: { 'Content-Type': 'application/json', ...this.headers },
+				body: JSON.stringify({ ids, action, user: window.auth.getUser() })
+			});
 
 			if (!response.ok) {
-				const errorData = await response.json().catch(()=>{});
-				throw new Error(errorData.message || `${action} failed: ${response.status}`);
+				throw new Error(`${action} failed: ${response.status}`);
 			}
 
 			const result = await response.json();
@@ -497,41 +489,40 @@
 				throw new Error(result.message || `${action} operation failed`);
 			}
 
-			if (['cancel', 'dismiss'].includes(action)) {
-				ids.forEach(id => {
-					let item = this.getQueue(id);
-					this.notify(`${action}-operation`, item);
-					this.clearQueue(id);
-				});
-			} else {
-				ids.forEach(id => {
-					let item = this.getQueue(id);
-					this.notify(`${action}-operation`, item);
+			// SINGLE place to handle store updates
+			ids.forEach(id => {
+				let item = this.getQueue(id);
+				this.notify(`${action}-operation`, item);
 
+				if (shouldRemove) {
+					this.clearQueue(id);
+				} else {
 					item.status = 'queued';
 					item.retries = 0;
 					this.setQueue(item);
 					this.updateOperationStatus(item.id, item.status);
-				});
+				}
+			});
+
+			if (action === 'retry') {
 				this.startActivityTracking();
 			}
-			this.updateUI();
 
+			this.updateUI();
 			return result;
+
 		} catch (error) {
-			const result = await window.jvbError.log(error, {
+			// Log and let jvbError handle retry
+			await window.jvbError.log(error, {
 				component: 'QueueManager',
 				operation: 'performQueueAction',
 				action: action,
 				operationIds: ids,
 				itemCount: ids.length
-			}, () => this.updateServerOperations(ids, action)); // Retry callback
+			}, () => this.updateServerOperations(ids, action));
 
-			if (result.retried) {
-				return result; // Return successful retry result
-			} else {
-				throw error; // Re-throw if not retried
-			}
+			// Don't re-throw - error is logged and handled
+			return { success: false, error: error.message };
 		}
 	}
 
@@ -554,11 +545,8 @@
 	*********************************************/
 	initListeners() {
 		this.clickHandler = this.handleClick.bind(this);
-		this.changeHandler = this.handleChange.bind(this);
-		this.keyHandler = this.handleEscape.bind(this);
 
 		document.addEventListener('click', this.clickHandler);
-		this.ui.panel?.addEventListener('change', this.changeHandler);
 
 		this.handleOnline = () => {
 			this.updateStatusPanel();
@@ -568,10 +556,9 @@
 		};
 		this.handleOffline = () => this.updateStatusPanel('offline');
 		this.handleBeforeUnload = (e) => {
-			const hasPending = this.getOperationsByStatus(['queued', 'uploading']);
-			if (hasPending.length > 0) {
+			if (this.isPolling || this.isProcessing) {
 				e.preventDefault();
-				return 'You have unsaved changes in the queue.';
+				return 'You have unsaved changes in the queue. Proceed?';
 			}
 		};
 
@@ -580,28 +567,22 @@
 		window.addEventListener('beforeunload', this.handleBeforeUnload);
 	}
 	handleClick(e) {
-		if(!e.target.closest(this.selectors.panel) && !e.target.closest(this.selectors.toggle)) {
-			if (this.panelIsOpen()) {
-				this.togglePanel(false);
-			}
+		if (!e.target.closest(this.selectors.panel, this.selectors.toggle)) {
 			return;
 		}
-
-		if (e.target.closest(this.selectors.toggle)) {
-			this.togglePanel(!this.panelIsOpen());
-		} else if (e.target.closest(this.selectors.refreshButton)) {
-			this.pollServer(true);
+		if (e.target.closest(this.selectors.refreshButton)) {
+			this.store.clearCache();
+			this.store.clearHttpHeaders(); // Clear cached headers first
+			this.store.fetch();
 		} else if (e.target.closest(this.selectors.clearButton)) {
-			const completedOps = this.getOperationsByStatus('completed');
+			const completedOps = this.getOperationIds(this.getOperationsByStatus('completed'));
 			if (completedOps.length > 0) {
-				const ids = completedOps.map(op => op.id);
-				this.updateServerOperations(ids, 'dismiss');
+				this.updateServerOperations(completedOps, 'dismiss');
 			}
 		} else if (e.target.closest(this.selectors.retryButton)) {
-			const failedOps = this.getOperationsByStatus('failed');
+			const failedOps = this.getOperationIds(this.getOperationsByStatus('failed'));
 			if (failedOps.length > 0) {
-				const ids = failedOps.map(op => op.id);
-				this.updateServerOperations(ids, 'retry');
+				this.updateServerOperations(failedOps, 'retry');
 			}
 		} else if (e.target.closest('[data-action]')) {
 			const button = e.target.closest('[data-action]');
@@ -616,39 +597,14 @@
 
 	}
 
-	handleChange(e) {
-	}
-
-	handleEscape(e) {
-		if (e.key === 'Escape') {
-			this.togglePanel(false);
-		}
-	}
-	panelIsOpen() {
-		return this.ui.panel?.classList.contains('expanded');
-	}
-	togglePanel(open) {
-		if (!this.ui.panel) return;
-
-		if (open) {
-			document.addEventListener('keydown', this.keyHandler);
-		} else {
-			document.removeEventListener('keydown', this.keyHandler);
-		}
-		this.ui.toggle.title = (open) ? 'Hide Queue' : 'Show Queue';
-		this.a11y.announce((open) ? 'Opened Queue Panel': 'Closed Queue Panel');
-		this.ui.panel.ariaExpanded = open;
-		this.ui.panel.classList.toggle('expanded', open);
-	}
-
 	/*********************************************
 	UI
 	 *********************************************/
 	initUI() {
 		this.icons = {
-			queued: 'refresh', localProcessing: 'refresh', uploading: 'syncing',
-			pending: 'cloud', processing: 'syncing', completed: 'synced',
-			failed: 'error', failed_permanent: 'error'
+			queued: 'arrows-clockwise', localProcessing: 'arrows-clockwise', uploading: 'syncing',
+			pending: 'cloud', processing: 'syncing', completed: 'cloud-check',
+			failed: 'cloud-warning', failed_permanent: 'cloud-warning'
 		};
 
 		this.selectors = {
@@ -674,43 +630,37 @@
 			}
 		};
 
-		this.ui = {
-			panel: document.querySelector(this.selectors.panel),
-			toggle: document.querySelector(this.selectors.toggle),
-			count: document.querySelector(this.selectors.count),
-			indicator: document.querySelector(this.selectors.indicator),
-		};
+		this.ui = window.uiFromSelectors(this.selectors);
 		if (!this.ui.panel) {
 			this.canUpdateUI = false;
-			return;
-		}
-
-		for (let [key, selector] of Object.entries(this.selectors)) {
-			if (['panel', 'toggle', 'count', 'indicator'].includes(key)) {
-				continue;
-			}
-			if (typeof selector === 'object') {
-				this.ui[key] = {};
-				for (let [k, s] of Object.entries(selector)) {
-					this.ui[key][k] = this.ui.panel.querySelector(s);
-				}
-			}else {
-				this.ui[key] = this.ui.panel.querySelector(selector);
-			}
 		}
 	}
 
-	updateUI() {
+	_updateUI() {
 		if (!this.canUpdateUI) {
 			return;
 		}
-		const stats = this.getQueueStats();
+
+		// Get current operations from store
+		const operations = Array.from(this.store.data.values());
+
+		// Get stats from last fetch response (server-provided)
+		const stats = this.store.lastResponse?.queue_stats || {
+			queued: 0,
+			localProcessing: 0,
+			uploading: 0,
+			pending: 0,
+			processing: 0,
+			completed: 0,
+			failed: 0,
+			failed_permanent: 0
+		};
 
 		// Update count badge
 		if (this.ui.count) {
-			const total = stats.total - stats.completed;
-			this.ui.count.textContent = total > 0 ? total : '';
-			this.ui.count.style.display = total > 0 ? '' : 'none';
+			const activeCount = operations.length - stats.completed;
+			this.ui.count.textContent = activeCount > 0 ? activeCount : '';
+			this.ui.count.style.display = activeCount > 0 ? '' : 'none';
 		}
 
 		// Update indicator
@@ -719,14 +669,16 @@
 				stats.pending > 0 || stats.processing > 0;
 			this.ui.indicator.classList.toggle('active', hasActive);
 		}
-		let failed = this.getOperationsByStatus('failed');
-		let completed = this.getOperationsByStatus('completed');
-		this.ui.clearButton.disabled = completed.length === 0;
-		this.ui.retryButton.disabled = failed.length === 0;
 
-		// Update filter counts
+		// Update button states
+		this.ui.clearButton.disabled = this.getOperationsByStatus('completed').length === 0;
+		this.ui.retryButton.disabled = this.getOperationsByStatus('failed').length === 0 && this.getOperationsByStatus('failed_permanent').length === 0;
+
+		// Update filter counts (from server stats)
 		Object.entries(this.ui.filters).forEach(([status, button]) => {
-			const count = status === 'all' ? stats.total : stats[status] || 0;
+			const count = status === 'all'
+				? operations.length
+				: stats[status] || 0;
 			const countEl = button.querySelector('.count');
 			if (countEl) {
 				countEl.textContent = count > 0 ? count : '';
@@ -734,7 +686,7 @@
 			button.setAttribute('data-count', count);
 		});
 
-		// Update operation list
+		// Render current operations
 		this.renderOperations();
 	}
 
@@ -793,46 +745,24 @@
 		return statusProgress[item.status] || 0;
 	}
 
-	getQueueStats() {
-		const stats = {};
-		this.statuses.forEach(status => {
-			stats[status] = 0;
-		});
-
-		Array.from(this.store.items.values())
-			.forEach(op => {
-				if (stats.hasOwnProperty(op.status)) {
-					stats[op.status]++;
-				}
-			});
-
-		stats.total = Array.from(this.store.items.values()).length;
-
-		return stats;
-	}
 
 	renderOperations() {
 		if (!this.ui.itemsContainer) return;
 
-		const activeFilter = this.getActiveFilter();
-		const operations = this.getFilteredOperations(activeFilter);
+		const operations = this.store.getFiltered();
 
 		// Clear container
 		window.removeChildren(this.ui.itemsContainer);
 
-		// Render each operation
+		// Render operations or empty state
 		if (operations.length === 0) {
 			let empty = window.getTemplate('emptyQueue');
 			this.ui.itemsContainer.append(empty);
 			this.a11y.announce('Nothing queued.');
 		} else {
-			let empty = this.ui.itemsContainer.querySelector('.emptyQueue');
-			if (empty) {
-				empty.remove();
-			}
 			operations.forEach(op => {
 				const element = this.createOperationUI(op);
-				this.ui.itemsContainer.appendChild(element);
+				this.ui.itemsContainer.append(element);
 			});
 		}
 	}
@@ -948,25 +878,6 @@
 		}
 	}
 
-	updateCountdown() {
-		if (!this.ui.countdown || !this.isPolling) return;
-
-		let seconds = this.config.pollInterval / 1000;
-
-		this.countdownTimer = setInterval(() => {
-			seconds--;
-
-			this.ui.countdown.textContent = seconds;
-
-			if (seconds <= 0) {
-				clearInterval(this.countdownTimer);
-				if (this.isPolling) {
-					setTimeout(() => this.updateCountdown(), 100);
-				}
-			}
-		}, 1000);
-	}
-
 	updateStatusPanel(status) {
 		this.ui.panel?.classList.remove(...this.classes);
 		if (!this.classes.includes(status)) {
@@ -979,69 +890,39 @@
 	 FILTERS
 	 **************************************************/
 	setFilter(filter) {
+		// Update active button
 		Object.values(this.ui.filters).forEach(button => {
 			if (button) {
 				button.classList.toggle('active', button.dataset.filter === filter);
 			}
 		});
 
-		this.activeFilter = filter;
-		this.renderOperations();
-	}
-
-	getActiveFilter() {
-		const activeButton = this.ui.panel?.querySelector('.filter.active');
-		return activeButton?.dataset.filter || 'all';
-	}
-
-	getFilteredOperations(filter) {
-		const operations = Array.from(this.store.items.values());
-
 		if (filter === 'all') {
-			return operations;
+			this.store.clearFilters();
+		} else {
+			this.store.setFilter('status', filter);
 		}
-
-		return operations.filter(op => op.status === filter);
 	}
 
 	/**************************************************************************
-	 NOTIFICATIONS
-	**************************************************************************/
-	showPopup(message, type = 'success') {
-		if (!this.ui.popup) return;
-
-		const span = this.ui.popup.querySelector('span');
-		if (span) {
-			span.textContent = message;
-		}
-
-		this.ui.popup.className = `popup ${type} show`;
-
-		setTimeout(() => {
-			this.ui.popup.classList.remove('show');
-		}, 3000);
-	}
-	/**************************************************************************
 	 HELPERS
 	**************************************************************************/
 	getOperationsByStatus(status, include = true) {
 
-		status = Array.isArray(status) ? status : ((status.includes(',')) ? status.split(',') : [status]);
-		if (include) {
-			return Array.from(this.queue.values()).filter(op =>
-				status.includes(op.status)
-			);
+		if (!Array.isArray(status) && typeof status === 'string') {
+			status = [status];
 		}
-		return Array.from(this.queue.values()).filter(op =>
-			!status.includes(op.status)
-		);
+		return (include)
+			? Array.from(this.store.data.values()).filter((item) => status.includes(item.status))
+			: Array.from(this.store.data.values()).filter((item) => !status.includes(item.status));
 	}
 	hasQueuedOperations() {
-		return this.queue.some(op =>
-			op.status === 'queued'
-		);
+		return this.getOperationsByStatus('queued').length > 0;
 	}
 	subscribe(callback) {
+		if (!this.subscribers) {
+			return;
+		}
 		this.subscribers.add(callback);
 		return () => this.subscribers.delete(callback);
 	}
@@ -1069,6 +950,10 @@
 	}
 }
 
-document.addEventListener('DOMContentLoaded', function() {
-	window.jvbQueue = new QueueManager();
+document.addEventListener('DOMContentLoaded', async function() {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbQueue = new QueueManager();
+		}
+	});
 });

--
Gitblit v1.10.0