// Auto-extracted from tasks-manager.js (verbatim) — augments TasksManager.prototype. Loaded after the base. Object.assign(TasksManager.prototype, { toggleTaskDetails(taskId) { // A manual toggle means the user has taken control of the view — stop // auto-following the running task for the rest of this visit. this.followRunning = false; const details = document.getElementById(`details-${taskId}`); const toggleBtn = document.querySelector(`.task-btn.toggle-details[onclick*="toggleTaskDetails('${taskId}')"]`); if (details) { const isOpen = details.style.display === 'block'; // Close all other task details and reset their buttons if (isOpen) { document.querySelectorAll('.task-details').forEach(otherDetails => { if (otherDetails.id !== `details-${taskId}`) { otherDetails.style.display = 'none'; otherDetails.classList.remove('task-details-open'); } }); document.querySelectorAll('.task-btn.toggle-details').forEach(otherBtn => { if (!otherBtn.getAttribute('onclick').includes(taskId)) { otherBtn.classList.remove('expanded'); } }); // Close current details.style.display = 'none'; details.classList.remove('task-details-open'); if (toggleBtn) toggleBtn.classList.remove('expanded'); } else { // Open current details.style.display = 'block'; details.classList.add('task-details-open'); if (toggleBtn) toggleBtn.classList.add('expanded'); // Auto-load logs when opened. For active tasks, hand off to the live // streamer so SSE chunks keep updating the panel; for terminal tasks // a one-shot snapshot is enough. const t = this.tasks.find(x => x.id === taskId); if (t && (t.status === 'running' || t.status === 'queued' || t.status === 'pending')) { this.startLogStreaming(taskId, t); } else { this.loadTaskLogs(taskId); } // Scroll to task const taskElement = document.querySelector(`[data-task-id="${taskId}"]`); if (taskElement) { taskElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } // Update URL to include task parameter this.updateURL(this.currentCategory, isOpen ? null : taskId); this.highlightedTaskId = isOpen ? null : taskId; } else { // Remove task parameter from URL this.updateURL(this.currentCategory); this.highlightedTaskId = null; } }, // Programmatically open exactly one task row: collapse any other open // panel, expand this one, attach the right log view (live stream for // active tasks, snapshot for terminal ones) and bring it into view. // Used by deep links and the running-task auto-follow — unlike // toggleTaskDetails it never closes an already-open target and never // touches followRunning. Returns false when the row isn't in the DOM yet. selectTask(taskId) { const details = document.getElementById(`details-${taskId}`); if (!details) return false; document.querySelectorAll('.task-details.task-details-open').forEach(el => { if (el.id !== `details-${taskId}`) { el.style.display = 'none'; el.classList.remove('task-details-open'); } }); document.querySelectorAll('.task-btn.toggle-details.expanded').forEach(btn => { if (!(btn.getAttribute('onclick') || '').includes(`'${taskId}'`)) btn.classList.remove('expanded'); }); const wasOpen = details.classList.contains('task-details-open'); details.style.display = 'block'; details.classList.add('task-details-open'); const toggleBtn = document.querySelector(`.task-btn.toggle-details[onclick*="toggleTaskDetails('${taskId}')"]`); if (toggleBtn) toggleBtn.classList.add('expanded'); const task = this.tasks.find(t => t.id === taskId); if (task && (task.status === 'running' || task.status === 'queued' || task.status === 'pending')) { this.startLogStreaming(taskId, task); } else { this.loadTaskLogs(taskId); } if (!wasOpen) { const row = document.querySelector(`.task-item[data-task-id="${taskId}"]`); if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' }); } this.highlightedTaskId = taskId; return true; }, // Pick which row should open when the page (re)loads, and arm auto-follow. // // A `?task=` deep link normally wins. The setup-wizard handoff is the // exception: it deep-links the FIRST task of its install queue // (&from=setup), but by the time the page renders the processor may // already be tasks ahead — so for setup arrivals, and for plain visits // with no deep link, open whatever is actually running (else next in the // queue) and keep following the queue until the user takes over. applyInitialSelection() { if (!document.getElementById('tasks-list')) return; const params = new URL(window.location.href).searchParams; const fromSetup = params.get('from') === 'setup'; const isActive = (t) => t && (t.status === 'running' || t.status === 'queued' || t.status === 'pending'); const pinned = this.highlightedTaskId ? this.tasks.find(t => t.id === this.highlightedTaskId) : null; this.followRunning = fromSetup || !pinned; let target = pinned; if (this.followRunning) { const running = this.tasks.find(t => t.status === 'running'); // this.tasks is sorted newest-first, so the oldest queued is next to run const nextQueued = [...this.tasks].reverse().find(t => t.status === 'queued' || t.status === 'pending'); target = running || (isActive(pinned) ? pinned : null) || nextQueued || pinned; } if (!target) return; // renderTasks restores the pre-render scroll position one frame from now; // selecting after that frame keeps our scrollIntoView from being cancelled. const id = target.id; requestAnimationFrame(() => requestAnimationFrame(() => this.selectTask(id))); }, // The queue advanced to a new running task — if the user hasn't taken over // and we're on the tasks page, move the open panel along with it. maybeFollowRunningTask(task) { if (!this.followRunning || !task || task.status !== 'running') return; if (!document.getElementById('tasks-list')) return; if (this.highlightedTaskId === task.id) return; const current = this.tasks.find(t => t.id === this.highlightedTaskId); if (current && current.status === 'running') return; // never yank a live view if (!this.selectTask(task.id)) { // Row not rendered yet (task created since the last render) — rebuild // the list from the upserted cache, then select. this.renderTasks(); this.selectTask(task.id); } }, // Monitor a specific task. State changes now arrive via SSE through // TaskEventBus, so this is mostly a hook for the UI to: // - auto-expand the task if it's the one we just started // - start log streaming when the task transitions to running // - clean up local intervals when it terminates // No more polling; the only fetches happen on-demand via TaskManager. monitorTask(taskId, appName, action) { if (!this.highlightedTaskId || this.highlightedTaskId === taskId) { setTimeout(() => this.autoExpandTask(taskId), 1500); } let statusUpdateInterval = null; const onUpdate = (event) => { const t = event.detail && event.detail.task; if (!t || t.id !== taskId) return; if (t.status === 'running') { this.updateTaskStructure(taskId, t); this.startLogStreaming(taskId, t); if (this.highlightedTaskId === taskId && !statusUpdateInterval) { statusUpdateInterval = setInterval(() => this.updateHighlightedTaskStatus(taskId), 2000); } } else { this.updateTaskDisplay(t); } }; const onComplete = (event) => { if (!event.detail || event.detail.taskId !== taskId) return; if (statusUpdateInterval) { clearInterval(statusUpdateInterval); statusUpdateInterval = null; } this.stopLogStreaming(taskId); window.removeEventListener('taskUpdated', onUpdate); window.removeEventListener('taskCompleted', onComplete); if (this.taskMonitors) this.taskMonitors.delete(taskId); }; window.addEventListener('taskUpdated', onUpdate); window.addEventListener('taskCompleted', onComplete); // Track this monitor so the tasks module's unmount() can release it if the // user navigates away while the task is still running (these window // listeners + interval otherwise outlive the page until the task completes). (this.taskMonitors = this.taskMonitors || new Map()).set(taskId, () => { if (statusUpdateInterval) { clearInterval(statusUpdateInterval); statusUpdateInterval = null; } window.removeEventListener('taskUpdated', onUpdate); window.removeEventListener('taskCompleted', onComplete); }); }, // Auto-expand a task when it's created/started. Waits for its row to land in // the DOM, then hands off to selectTask, which collapses any other open panel // first. Opening the row directly here (as this used to) skipped that // collapse and never set highlightedTaskId, so a burst of monitored tasks — // e.g. a multi-app first install — stacked every panel open instead of // showing just the active one. autoExpandTask(taskId) { let attempts = 0; const maxAttempts = 10; const tryExpand = () => { attempts++; // selectTask returns false until the row exists; once it succeeds it has // also attached the right log view (live stream vs snapshot), set // highlightedTaskId and scrolled the row into view — nothing more to do. if (this.selectTask(taskId)) return; if (attempts < maxAttempts) { setTimeout(tryExpand, 500); } else { console.warn(`⚠️ Could not find task element for ${taskId} after ${maxAttempts} attempts`); } }; tryExpand(); }, });