1window.appComponents.progressBar = (taskId, stoppable = false) => ({ 2 // --- STATE --- 3 percent: 0, 4 statusText: 'Initializing...', 5 hasFailed: false, 6 eventSource: null, 7 isComplete: false, 8 isStopping: false, 9 isStoppable: stoppable, 10 isTerminated: false, // Keep the anti-race-condition flag 11 12 init() { 13 const url = `/lib/static/progress_stream.php?taskId=${taskId}`; 14 this.eventSource = new EventSource(url); 15 16 this.eventSource.onmessage = (event) => { 17 const data = JSON.parse(event.data); 18 19 if (data.done) { 20 if (data.error) { 21 this.handleTaskError(data); 22 } else { 23 this.handleTaskComplete(data); 24 } 25 } else { 26 this.handleProgressUpdate(data); 27 } 28 }; 29 30 this.eventSource.onerror = (error) => { 31 // --- LOGGING --- 32 console.error('[PROGRESS JS - ONERROR]', { error, isTerminated: this.isTerminated }); 33 if (this.isTerminated) return; // Ignore intentional closures 34 35 if (!this.isComplete && !this.hasFailed) { 36 this.hasFailed = true; 37 this.statusText = 'Connection to the server was lost.'; 38 } 39 this.cleanup(); 40 }; 41 }, 42 43 handleProgressUpdate(data) { 44 this.percent = data.percent; 45 this.statusText = data.status; 46 }, 47 48 handleTaskComplete(data) { 49 this.isComplete = true; 50 this.percent = 100; 51 this.statusText = data.status || "Complete!"; 52 this.cleanup(); 53 54 setTimeout(() => { 55 Alpine.store('modals').close(); 56 this.routeOnCompleteAction(data.result || { action: 'close' }); 57 }, 750); 58 }, 59 60 handleTaskError(data) { 61 this.hasFailed = true; 62 this.percent = 100; 63 this.statusText = data.error || 'The task failed with an unknown error.'; 64 this.cleanup(); 65 }, 66 67 cleanup() { 68 if (this.eventSource) { 69 this.isTerminated = true; 70 this.eventSource.close(); 71 this.eventSource = null; 72 } 73 }, 74 75 closeAndCleanup() { 76 this.cleanup(); 77 Alpine.store('modals').close(); 78 }, 79 80 /** 81 * The rest of your code is correct. 82 */ 83 routeOnCompleteAction(onComplete) { 84 switch (onComplete.action) { 85 case 'successAndRedirect': 86 Alpine.store('modals').showSuccessAndRedirect(onComplete.message, onComplete.url); 87 break; 88 case 'flashAndRedirect': 89 case 'redirect': 90 if (onComplete.url) window.location.href = onComplete.url; 91 break; 92 case 'triggerEvents': 93 if (onComplete.events && typeof onComplete.events === 'object') { 94 Object.keys(onComplete.events).forEach(eventName => { 95 const eventDetail = onComplete.events[eventName]; 96 htmx.trigger(document.body, eventName, eventDetail); 97 }); 98 } 99 break; 100 case 'close': 101 default: 102 // Do nothing 103 break; 104 } 105 }, 106 107 stopTask() { 108 // Prevent multiple clicks or stopping a finished task 109 if (this.isStopping || this.isComplete) return; 110 111 this.isStopping = true; 112 this.statusText = "Requesting to stop..."; 113 114 // Send a simple, fire-and-forget request to the stop endpoint 115 fetch('/lib/static/task_stop.php', { 116 method: 'POST', 117 headers: { 'Content-Type': 'application/json' }, 118 body: JSON.stringify({ taskId: taskId }) 119 }) 120 .catch(error => { 121 console.error("Stop request failed:", error); 122 // Re-enable the button if the request fails 123 this.isStopping = false; 124 this.statusText = "Failed to send stop request."; 125 }); 126 }, 127});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.