From fd42a54d96fcd3342941caaeb61a4b0d5d3f1b4f Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Fri, 6 Mar 2026 23:55:07 +0000 Subject: recover: restore untracked work from recovery branch (no Gemini changes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- web/app.js | 103 +++++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 70 insertions(+), 33 deletions(-) (limited to 'web/app.js') diff --git a/web/app.js b/web/app.js index ce1394d..97721d3 100644 --- a/web/app.js +++ b/web/app.js @@ -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 = '
No active tasks.
'; + return; + } + const active = sortTasksByDate(filterActiveTasks(tasks)); + container.innerHTML = ''; + if (active.length === 0) { + container.innerHTML = '
No active tasks.
'; + 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 = '
Could not reach server.
'; @@ -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() { -- cgit v1.2.3