autoExpandTask (the monitorTask path) opened its row directly without collapsing the others and never set highlightedTaskId — unlike every other opener (toggleTaskDetails, selectTask), which enforce a single open row. So a burst of monitored task creations, e.g. a multi-app first install, stacked every panel open at once. Wait for the row to render, then delegate to selectTask, which collapses any other open panel, sets highlightedTaskId, attaches the right log view (live stream vs snapshot) and scrolls into view. Setting highlightedTaskId also makes monitorTask's own guard trip after the first task, so the running-task auto-follow takes over from there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
226 lines
9.9 KiB
JavaScript
226 lines
9.9 KiB
JavaScript
// 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();
|
|
},
|
|
});
|