From 7a9054bb3f033c98067b3196378311dae54c5fbf Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 20 Jan 2026 01:31:53 +0000
Subject: [PATCH] =OperationQueue refactor to the JVBase/managers/queue namespace

---
 assets/js/concise/Queue.js |  212 ++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 147 insertions(+), 65 deletions(-)

diff --git a/assets/js/concise/Queue.js b/assets/js/concise/Queue.js
index 478be92..4c9adbc 100644
--- a/assets/js/concise/Queue.js
+++ b/assets/js/concise/Queue.js
@@ -5,6 +5,7 @@
 
 		this.user = window.auth.getUser();
 
+
 		this.canUpdateUI = true;
 		this.isProcessing = false;
 		this.isPolling = false;
@@ -15,6 +16,8 @@
 		this.api = jvbSettings.api;
 		this.endpoint = 'queue';
 
+		this.queueItems = new Map();
+
 		this.init();
 	}
 	init() {
@@ -24,13 +27,14 @@
 		this.initElements();
 		this.initListeners();
 		this.initStore();
-		if (this.canUpdateUI) {
+		if (this.canUpdateUI && this.ui.panel) {
 			this.popup = new window.jvbPopup({
 				popup: this.ui.panel,
 				toggle: this.ui.toggle.button,
 				name: 'Queue Panel',
 			});
 		}
+		this.defineTemplates();
 	}
 
 	initElements() {
@@ -138,6 +142,17 @@
 		if (!this.ui.panel) this.canUpdateUI = false;
 	}
 
+	defineTemplates() {
+		const T = window.jvbTemplates;
+
+		T.define('emptyState');
+		T.define('queueItem', {
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
+			}
+		});
+	}
+
 
 	initListeners() {
 		this.activityListeners = null;
@@ -152,7 +167,7 @@
 		window.addEventListener('beforeunload', this.unloadHandler);
 	}
 		handleOnline() {
-			this.updatePanel();
+			this.updatePanel('synced');
 			if (this.getQueueByStatus(this.pendingStatuses).length > 0) {
 				this.processQueue();
 			}
@@ -161,6 +176,7 @@
 			this.updatePanel('offline');
 		}
 	handleBeforeUnload(e) {
+		if (!this.ui.panel) return;
 		const total = this.getQueueByStatus(this.pendingStatuses).length;
 		if (total > 0) {
 			// Modern browsers ignore custom messages, but this triggers the native dialog
@@ -173,8 +189,12 @@
 			if (!window.targetCheck(e, this.selectors.panel+', '+this.selectors.toggle.button)) return;
 			const refresh = window.targetCheck(e, this.selectors.refresh.button);
 			if (refresh) {
+				this.ui.refresh.button.classList.add('fetching');
 				this.store.clearCache();
-				this.store.fetch();
+				this.store.clearFilters();
+				this.store.fetch().finally(() => {
+					this.ui.refresh.button.classList.remove('fetching');
+				});
 				return;
 			}
 
@@ -265,6 +285,9 @@
 					{name: 'status', keyPath: 'status'},
 					{name: 'type', keyPath: 'type'},
 				],
+				filters: {
+					user: window.auth.getUser()
+				},
 				showLoading: false,
 			}
 		)
@@ -304,6 +327,7 @@
 			title: 'Operation',
 			status: 'queued',
 			timestamp: Date.now(),
+			created_at: new Date().toISOString(),
 			retries: 0,
 			user: this.user,
 			... operation
@@ -364,9 +388,12 @@
 		}
 		if (statusOrId.length ===0) return;
 		if (!['cancel', 'dismiss', 'retry'].includes(action)) return;
+
 		const shouldRemove = ['cancel', 'dismiss'].includes(action);
 		if (shouldRemove) {
-			statusOrId.forEach(id => this.removeOperationUI(id));
+			statusOrId.forEach(id => {
+				this.removeOperationUI(id)
+			});
 		}
 
 		try {
@@ -394,7 +421,10 @@
 			}
 			statusOrId.forEach(id => {
 				let item = this.getQueue(id);
-				this.notify(`${action}-operation`, item);
+				if (item) {
+					this.notify(`${action}-operation`, item);
+				}
+
 				if (shouldRemove) {
 					this.clearQueue(id);
 				} else {
@@ -441,8 +471,7 @@
 		this.setProcessing(false);
 		this.stopActivityTracking();
 
-		// this.toggleQueue(this.maybeStartPolling());
-
+		this.toggleQueue(this.maybeStartPolling());
 	}
 
 	async processOperation(operation) {
@@ -462,13 +491,13 @@
 			let requestBody;
 			if (operation.data instanceof FormData) {
 				operation.data.append('id', operation.id);
-				operation.data.append('user', this.user);
+				operation.data.append('user', window.auth.getUser());
 				requestBody = operation.data;
 			} else {
 				requestBody = JSON.stringify({
 					...operation.data,
 					id: operation.id,
-					user: this.user
+					user: window.auth.getUser()
 				});
 				operation.headers['Content-Type'] = 'application/json';
 			}
@@ -545,13 +574,37 @@
 		});
 	}
 
+	sortOperations(ops) {
+		const statusPriority = {
+			'processing': 0,
+			'uploading': 1,
+			'pending': 2,
+			'queued': 3,
+			'localProcessing': 4,
+			'failed': 5,
+			'completed': 6,
+			'failed_permanent': 7
+		};
+
+		return ops.sort((a, b) => {
+			// First by status priority
+			const priorityDiff = (statusPriority[a.status] ?? 99) - (statusPriority[b.status] ?? 99);
+			if (priorityDiff !== 0) return priorityDiff;
+
+			// Then by updated_at (most recent first)
+			const aTime = a.updated_at ?? a.timestamp ?? 0;
+			const bTime = b.updated_at ?? b.timestamp ?? 0;
+			return new Date(bTime) - new Date(aTime);
+		});
+	}
+
 	getAllQueue() {
 		let ops = [... new Set([
 			...Array.from(this.store.data.values()),
 			... Array.from(this.queue.values())
 		])];
 		//Sort operations by operation updated_at
-		return this.sortByDate(ops);
+		return this.sortOperations(ops);
 	}
 
 	getQueueByStatus(status) {
@@ -563,7 +616,7 @@
 			...Array.from(this.store.filterByIndex({status: status})),
 			...Array.from(this.queue.values()).filter(op => status.includes(op.status))
 		])];
-		return this.sortByDate(ops);
+		return this.sortOperations(ops);
 	}
 
 
@@ -589,32 +642,59 @@
 	 POLLING
 	****************************************************************************/
 	maybeStartPolling() {
-		const incomplete = this.getQueueByStatus(this.pendingStatuses);
+		const incomplete = this.getQueueByStatus([...this.pendingStatuses, ...this.workingStatuses]);
 		if (incomplete.length > 0) {
 			this.startPolling();
 			return true;
 		}
+		this.updatePanel('synced');
 		return false;
 	}
 	startPolling() {
 		if (this.isPolling) return;
 		this.isPolling = true;
 		this.updatePanel('pending');
+		this.runPollCycle();
+	}
 
-		this.pollTimer = setInterval(async () => {
-			try {
-				this.store.clearCache();
-				await this.store.fetch();
-				if (!this.maybeStartPolling()) {
-					this.stopPolling();
-					this.updatePanel('synced');
-				}
-			} catch (error) {
-				console.error('Polling error:', error);
+	async runPollCycle() {
+		if (!this.isPolling) return;
+
+		try {
+			this.ui.refresh.button.classList.add('fetching');
+			this.store.clearCache();
+			await this.store.fetch();
+			this.ui.refresh.button.classList.remove('fetching');
+			if (!this.maybeStartPolling()) {
+				this.stopPolling();
+				this.updatePanel('synced');
+				return;
 			}
-		},
-			5000);
-		this.startCountdown();
+		} catch (error) {
+			console.error('Polling error:', error);
+		}
+
+		// Schedule next poll with countdown
+		this.startCountdown(5, () => this.runPollCycle());
+	}
+
+	startCountdown(count, onComplete) {
+		if (!this.ui.refresh.countdown) {
+			console.warn('Countdown element not found');
+			return;
+		}
+		this.ui.refresh.countdown.classList.add('counting');
+		this.ui.refresh.countdown.textContent = count;
+
+		this.countdownTimer = setInterval(() => {
+			count--;
+			if (count > 0) {
+				this.ui.refresh.countdown.textContent = count;
+			} else {
+				this.stopCountdown();
+				if (onComplete) onComplete();
+			}
+		}, 1000);
 	}
 
 	stopPolling() {
@@ -626,24 +706,14 @@
 		}
 		this.stopCountdown();
 	}
-	startCountdown(count = 5) {
-		if (!this.isPolling) return;
-		this.ui.refresh.countdown.textContent = count;
-		this.countdownTimer = setInterval(async() => {
-			count--;
-			if (count >= 0) {
-				this.ui.refresh.countdown.textContent = count;
-			}else {
-				this.ui.refresh.countdown.textContent = '';
-				this.stopCountdown();
-			}
-		},1000);
-	}
+
 	stopCountdown() {
 		if (this.countdownTimer) {
 			clearInterval(this.countdownTimer);
 			this.countdownTimer = null;
 		}
+		this.ui.refresh.countdown.classList.remove('counting');
+		this.ui.refresh.countdown.textContent = '';
 	}
 	/****************************************************************************
 	 UI
@@ -661,6 +731,12 @@
 			this.ui.actions.retry.disabled = operations.filter(op => op.status === 'failed').length === 0;
 			this.ui.actions.clear.disabled = operations.filter(op => op.status === 'completed').length ===0;
 
+			const activeCount = operations.filter(op =>
+				[...this.pendingStatuses, ...this.workingStatuses].includes(op.status)
+			).length;
+			this.ui.toggle.count.hidden = activeCount === 0;
+			this.ui.toggle.count.textContent = activeCount;
+
 			for (let status of this.statuses) {
 				if (status === 'failed_permanent') continue;
 				let total = operations.filter(op => op.status === status).length;
@@ -681,35 +757,45 @@
 
 		const status = this.store.filters?.status ?? 'all';
 		const operations = (status === 'all') ? this.getAllQueue() : this.getQueueByStatus(status);
+		const sortedOps = this.sortOperations(operations);
 
-		window.removeChildren(this.ui.items.container);
-
-		if (operations.length === 0) {
-			const empty = window.getTemplate('emptyQueue');
+		if (sortedOps.length === 0) {
+			window.removeChildren(this.ui.items.container);
+			const empty = window.jvbTemplates.create('emptyQueue');
 			this.ui.items.container.append(empty);
 			this.a11y.announce('No items in queue');
 			return;
+		} else {
+			this.ui.items.container.querySelector('.empty-group')?.remove();
 		}
 
-		operations.forEach(op => {
-			let item = this.items.get(op.id);
+		// Track which items should exist
+		const expectedIds = new Set(sortedOps.map(op => op.id));
 
+		// Remove items that shouldn't exist
+		this.items.forEach((item, id) => {
+			if (!expectedIds.has(id)) {
+				item.element?.remove();
+				this.items.delete(id);
+			}
+		});
+
+		// Update/add items in order
+		sortedOps.forEach((op, index) => {
+			let item = this.items.get(op.id);
 			if (!item) {
-				// Create new element and reference
 				item = this.createOperationElement(op);
 			}
-
 			if (item?.element) {
 				this.updateOperationUI(op.id);
+				// Reorder by re-appending (moves to end in correct order)
 				this.ui.items.container.append(item.element);
 			}
 		});
 	}
 
 	createOperationElement(op) {
-		const el = window.getTemplate('queueItem');
-		el.dataset.id = op.id;
-
+		const el = window.jvbTemplates.create('queueItem', op);
 		const item = {
 			element: el,
 			ui: window.uiFromSelectors(this.selectors.item, el)
@@ -742,20 +828,15 @@
 				item.ui.startedAt.textContent = window.formatTimeAgo(op.created_at);
 			}
 			let text = op.status === 'completed' ? 'Completed: ' : 'Last updated: ';
-			let shouldShow =Object.hasOwn(op, 'updated_at') || Object.hasOwn(op, 'completed_at');
-			item.ui.completed.wrap.hidden = !shouldShow;
-			if (shouldShow && item.ui.completed.label && item.ui.completed.time) {
-				let time;
-				if (Object.hasOwn(op, 'completed_at')) {
-					time = op.completed_at;
-				} else {
-					time = op.updated_at;
-				}
-
-				item.ui.completed.label.textContent = text;
-				item.ui.completed.time.setAttribute('datetime', time);
-				item.ui.completed.time.textContent = window.formatTimeAgo(time);
+			const shouldShowCompleted = op.status === 'completed' && (op.completed_at || op.updated_at);
+			item.ui.completed.wrap.hidden = !shouldShowCompleted;
+			if (shouldShowCompleted) {
+				const completedTime = op.completed_at ?? op.updated_at;
+				item.ui.completed.label.textContent = 'Completed: ';
+				item.ui.completed.time.setAttribute('datetime', completedTime);
+				item.ui.completed.time.textContent = window.formatTimeAgo(completedTime);
 			}
+
 			window.showProgress(item.ui.progress, progress, 100, this.statusLabel(op.status));
 			if (item.ui.actions.cancel) item.ui.actions.cancel.hidden = this.completedStatuses.includes(op.status);
 			if (item.ui.actions['retry']) {
@@ -785,8 +866,8 @@
 	}
 
 	updatePanel(status = 'syncing') {
-		if (!this.panelStatuses.includes(status)) return;
-		this.ui.panel.classList.remove(this.panelStatuses);
+		if (!this.ui.panel || !this.panelStatuses.includes(status)) return;
+		this.ui.panel.classList.remove(...this.panelStatuses);
 		this.ui.panel.classList.add(status);
 	}
 	/****************************************************************************
@@ -822,7 +903,7 @@
 			case 'completed':
 				return 'Successfully completed';
 			case 'failed':
-				return `Failed: ${item.lastError || 'Unknown error'} (Retry ${item.retries}/${this.config.maxRetries})`;
+				return `Failed: ${item.lastError || 'Unknown error'} (Retry ${item.retries}/${2})`;
 			case 'failed_permanent':
 				return `Failed: ${item.lastError || 'Unknown error'}`;
 			default:
@@ -830,6 +911,7 @@
 		}
 	}
 	toggleQueue(on = true) {
+		if (!this.ui.panel) return;
 		this.ui.panel.hidden = !on;
 		this.ui.toggle.button.hidden = !on;
 	}

--
Gitblit v1.10.0