diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-06 23:55:07 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-06 23:55:07 +0000 |
| commit | fd42a54d96fcd3342941caaeb61a4b0d5d3f1b4f (patch) | |
| tree | 0b9ef3b7f0ac3981aa310435d014c9f5e21089d4 /web/app.js | |
| parent | 7d4890cde802974b94db24071f63e7733c3670fd (diff) | |
recover: restore untracked work from recovery branch (no Gemini changes)
Recovered files with no Claude→Agent contamination:
- docs/adr/002-task-state-machine.md
- internal/api/logs.go/logs_test.go: task-level log streaming endpoint
- internal/api/validate.go/validate_test.go: POST /api/tasks/validate
- internal/api/server_test.go, storage/db_test.go: expanded test coverage
- scripts/reset-failed-tasks, reset-running-tasks
- web/app.js, index.html, style.css: frontend improvements
- web/test/: active-tasks-tab, delete-button, filter-tabs, sort-tasks tests
Manually applied from server.go diff (skipping Claude→Agent rename):
- taskLogStore field + validateCmdPath field
- DELETE /api/tasks/{id} route + handleDeleteTask
- GET /api/tasks/{id}/logs/stream route
- POST /api/tasks/{id}/resume route + handleResumeTimedOutTask
- handleCancelTask: allow cancelling PENDING/QUEUED tasks directly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'web/app.js')
| -rw-r--r-- | web/app.js | 103 |
1 files changed, 70 insertions, 33 deletions
@@ -1,5 +1,5 @@ -const BASE_PATH = document.querySelector('meta[name="base-path"]')?.content ?? ''; -const API_BASE = window.location.origin + BASE_PATH; +const BASE_PATH = (typeof document !== 'undefined') ? document.querySelector('meta[name="base-path"]')?.content ?? '' : ''; +const API_BASE = (typeof window !== 'undefined') ? window.location.origin + BASE_PATH : ''; // ── Fetch ───────────────────────────────────────────────────────────────────── @@ -160,17 +160,56 @@ function createTaskCard(task) { return card; } +// ── Sort ────────────────────────────────────────────────────────────────────── + +function sortTasksByDate(tasks) { + return [...tasks].sort((a, b) => { + if (!a.created_at && !b.created_at) return 0; + if (!a.created_at) return 1; + if (!b.created_at) return -1; + return new Date(a.created_at) - new Date(b.created_at); + }); +} + // ── Filter ──────────────────────────────────────────────────────────────────── const HIDE_STATES = new Set(['COMPLETED', 'FAILED']); +const ACTIVE_STATES = new Set(['PENDING', 'QUEUED', 'RUNNING', 'READY', 'BLOCKED']); +const DONE_STATES = new Set(['COMPLETED', 'FAILED', 'TIMED_OUT', 'CANCELLED', 'BUDGET_EXCEEDED']); -let showHiddenFold = false; +// filterActiveTasks uses its own set (excludes PENDING — tasks "in-flight" only) +const _PANEL_ACTIVE_STATES = new Set(['RUNNING', 'READY', 'QUEUED', 'BLOCKED']); -function filterTasks(tasks, hideCompletedFailed = false) { +export function filterTasks(tasks, hideCompletedFailed = false) { if (!hideCompletedFailed) return tasks; return tasks.filter(t => !HIDE_STATES.has(t.state)); } +export function filterActiveTasks(tasks) { + return tasks.filter(t => _PANEL_ACTIVE_STATES.has(t.state)); +} + +export function filterTasksByTab(tasks, tab) { + if (tab === 'active') return tasks.filter(t => ACTIVE_STATES.has(t.state)); + if (tab === 'done') return tasks.filter(t => DONE_STATES.has(t.state)); + return tasks; +} + +export function getTaskFilterTab() { + return localStorage.getItem('taskFilterTab') ?? 'active'; +} + +export function setTaskFilterTab(tab) { + localStorage.setItem('taskFilterTab', tab); +} + +export function updateFilterTabs() { + const current = getTaskFilterTab(); + document.querySelectorAll('.filter-tab[data-filter]').forEach(el => { + el.classList.toggle('active', el.dataset.filter === current); + }); +} + function getHideCompletedFailed() { const stored = localStorage.getItem('hideCompletedFailed'); return stored === null ? true : stored === 'true'; @@ -196,36 +235,30 @@ function renderTaskList(tasks) { return; } - const hide = getHideCompletedFailed(); - const visible = filterTasks(tasks, hide); - const hiddenCount = tasks.length - visible.length; + const visible = sortTasksByDate(filterTasksByTab(tasks, getTaskFilterTab())); // Replace contents with task cards container.innerHTML = ''; for (const task of visible) { container.appendChild(createTaskCard(task)); } +} - if (hiddenCount > 0) { - const info = document.createElement('button'); - info.className = 'hidden-tasks-info'; - const arrow = showHiddenFold ? '▼' : '▶'; - info.textContent = `${arrow} ${hiddenCount} hidden task${hiddenCount === 1 ? '' : 's'}`; - info.addEventListener('click', () => { - showHiddenFold = !showHiddenFold; - renderTaskList(tasks); - }); - container.appendChild(info); - - if (showHiddenFold) { - const fold = document.createElement('div'); - fold.className = 'hidden-tasks-fold'; - const hiddenTasks = tasks.filter(t => HIDE_STATES.has(t.state)); - for (const task of hiddenTasks) { - fold.appendChild(createTaskCard(task)); - } - container.appendChild(fold); - } +function renderActiveTaskList(tasks) { + const container = document.querySelector('.active-task-list'); + if (!container) return; + if (!tasks || tasks.length === 0) { + container.innerHTML = '<div id="loading">No active tasks.</div>'; + return; + } + const active = sortTasksByDate(filterActiveTasks(tasks)); + container.innerHTML = ''; + if (active.length === 0) { + container.innerHTML = '<div id="loading">No active tasks.</div>'; + return; + } + for (const task of active) { + container.appendChild(createTaskCard(task)); } } @@ -762,6 +795,7 @@ async function poll() { try { const tasks = await fetchTasks(); renderTaskList(tasks); + renderActiveTaskList(tasks); } catch { document.querySelector('.task-list').innerHTML = '<div id="loading">Could not reach server.</div>'; @@ -1539,12 +1573,15 @@ function switchTab(name) { // ── Boot ────────────────────────────────────────────────────────────────────── -document.addEventListener('DOMContentLoaded', () => { - updateToggleButton(); - document.getElementById('btn-toggle-completed').addEventListener('click', async () => { - setHideCompletedFailed(!getHideCompletedFailed()); - updateToggleButton(); - await poll(); +if (typeof document !== 'undefined') document.addEventListener('DOMContentLoaded', () => { + updateFilterTabs(); + + document.querySelectorAll(".filter-tab[data-filter]").forEach(btn => { + btn.addEventListener("click", () => { + setTaskFilterTab(btn.dataset.filter); + updateFilterTabs(); + poll(); + }); }); document.getElementById('btn-start-next').addEventListener('click', function() { |
