const API_BASE = window.location.origin; // ── Fetch ───────────────────────────────────────────────────────────────────── async function fetchTasks() { const res = await fetch(`${API_BASE}/api/tasks`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } // ── Render ──────────────────────────────────────────────────────────────────── function formatDate(iso) { if (!iso) return ''; return new Date(iso).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); } function createTaskCard(task) { const card = document.createElement('div'); card.className = 'task-card'; card.dataset.taskId = task.id; // Header: name + state badge const header = document.createElement('div'); header.className = 'task-card-header'; const name = document.createElement('span'); name.className = 'task-name'; name.textContent = task.name; const badge = document.createElement('span'); badge.className = 'state-badge'; badge.dataset.state = task.state; badge.textContent = task.state.replace(/_/g, ' '); header.append(name, badge); card.appendChild(header); // Meta: priority + created_at const meta = document.createElement('div'); meta.className = 'task-meta'; if (task.priority) { const prio = document.createElement('span'); prio.textContent = task.priority; meta.appendChild(prio); } if (task.created_at) { const when = document.createElement('span'); when.textContent = formatDate(task.created_at); meta.appendChild(when); } if (meta.children.length) card.appendChild(meta); // Description (truncated via CSS) if (task.description) { const desc = document.createElement('div'); desc.className = 'task-description'; desc.textContent = task.description; card.appendChild(desc); } // Footer: Run button (only for PENDING / FAILED) if (task.state === 'PENDING' || task.state === 'FAILED') { const footer = document.createElement('div'); footer.className = 'task-card-footer'; const btn = document.createElement('button'); btn.className = 'btn-run'; btn.textContent = 'Run'; btn.addEventListener('click', () => handleRun(task.id, btn, footer)); footer.appendChild(btn); card.appendChild(footer); } return card; } function renderTaskList(tasks) { const container = document.querySelector('.task-list'); if (!tasks || tasks.length === 0) { container.innerHTML = '
No tasks found.
'; return; } // Replace contents with task cards container.innerHTML = ''; for (const task of tasks) { container.appendChild(createTaskCard(task)); } } // ── Run action ──────────────────────────────────────────────────────────────── async function runTask(taskId) { const res = await fetch(`${API_BASE}/api/tasks/${taskId}/run`, { method: 'POST' }); if (!res.ok) { let msg = `HTTP ${res.status}`; try { const body = await res.json(); msg = body.error || body.message || msg; } catch {} throw new Error(msg); } return res.json(); } async function handleRun(taskId, btn, footer) { btn.disabled = true; btn.textContent = 'Queuing…'; // Remove any previous error const prev = footer.querySelector('.task-error'); if (prev) prev.remove(); try { await runTask(taskId); // Refresh list immediately so state flips to QUEUED const tasks = await fetchTasks(); renderTaskList(tasks); } catch (err) { btn.disabled = false; btn.textContent = 'Run'; const errEl = document.createElement('span'); errEl.className = 'task-error'; errEl.textContent = `Failed to queue: ${err.message}`; footer.appendChild(errEl); } } // ── Polling ─────────────────────────────────────────────────────────────────── async function poll() { try { const tasks = await fetchTasks(); renderTaskList(tasks); } catch { document.querySelector('.task-list').innerHTML = '
Could not reach server.
'; } } function startPolling(intervalMs = 10_000) { poll(); setInterval(poll, intervalMs); } // ── Boot ────────────────────────────────────────────────────────────────────── startPolling();