summaryrefslogtreecommitdiff
path: root/web/app.js
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator>2026-03-13 09:31:31 +0000
committerClaudomator Agent <agent@claudomator>2026-03-13 09:31:31 +0000
commit03f8b0e8b1aef2429f825b300c427147c30d4b0b (patch)
tree7c7f689543178d0ae2d0e5b477df1caafb1c0291 /web/app.js
parentc602ddd799d94bf3bbd35a57b98ad09e28df8ee9 (diff)
feat: reorganize web UI to 7-tab layout (Queue, Interrupted, Ready, Running, All, Stats, Settings)
- Replace Tasks/Active tabs with Queue (QUEUED+PENDING), Interrupted, Ready top-level tabs - Add All tab (COMPLETED, TIMED_OUT, BUDGET_EXCEEDED within last 24h) and Settings placeholder - Export filterQueueTasks, filterReadyTasks, filterAllDoneTasks from app.js - Refactor poll() to dispatch to active tab's render function instead of always rendering all panels - Add renderQueuePanel, renderInterruptedPanel, renderReadyPanel, renderAllPanel helpers - Add tests in web/test/tab-filters.test.mjs covering all new filter functions (16 tests) - All 165 JS tests and all Go tests pass Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'web/app.js')
-rw-r--r--web/app.js179
1 files changed, 95 insertions, 84 deletions
diff --git a/web/app.js b/web/app.js
index 187795f..afb6a75 100644
--- a/web/app.js
+++ b/web/app.js
@@ -296,6 +296,29 @@ export function filterTasksByTab(tasks, tab) {
return tasks;
}
+// Returns tasks with state QUEUED or PENDING.
+export function filterQueueTasks(tasks) {
+ return tasks.filter(t => t.state === 'QUEUED' || t.state === 'PENDING');
+}
+
+// Returns tasks with state READY.
+export function filterReadyTasks(tasks) {
+ return tasks.filter(t => t.state === 'READY');
+}
+
+// Returns COMPLETED, TIMED_OUT, BUDGET_EXCEEDED tasks from last 24h.
+// Pass since24h=false to disable the time filter.
+export function filterAllDoneTasks(tasks, since24h = true) {
+ const DONE_TAB_STATES = new Set(['COMPLETED', 'TIMED_OUT', 'BUDGET_EXCEEDED']);
+ return tasks.filter(t => {
+ if (!DONE_TAB_STATES.has(t.state)) return false;
+ if (!since24h) return true;
+ if (!t.created_at) return true; // defensive: keep if no date
+ const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
+ return new Date(t.created_at) > twentyFourHoursAgo;
+ });
+}
+
export function getTaskFilterTab() {
return localStorage.getItem('taskFilterTab') ?? 'active';
}
@@ -379,41 +402,44 @@ function updateToggleButton() {
: 'Hide completed/failed';
}
-function renderTaskList(tasks) {
- const container = document.querySelector('.task-list');
-
+// Shared helper: renders an array of tasks as cards into a container element.
+function renderTasksIntoContainer(tasks, container, emptyMsg) {
if (!tasks || tasks.length === 0) {
- container.innerHTML = '<div id="loading">No tasks found.</div>';
+ container.innerHTML = `<div class="task-empty">${emptyMsg}</div>`;
return;
}
-
- const tab = getTaskFilterTab();
- const descend = (tab === 'done' || tab === 'interrupted');
- const visible = sortTasksByDate(filterTasksByTab(tasks, tab), descend);
-
- // Replace contents with task cards
container.innerHTML = '';
- for (const task of visible) {
+ for (const task of tasks) {
container.appendChild(createTaskCard(task));
}
}
-function renderActiveTaskList(tasks) {
- const container = document.querySelector('.active-task-list');
+function renderQueuePanel(tasks) {
+ const container = document.querySelector('[data-panel="queue"] .panel-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));
- }
+ const visible = sortTasksByDate(filterQueueTasks(tasks));
+ renderTasksIntoContainer(visible, container, 'No tasks queued.');
+}
+
+function renderInterruptedPanel(tasks) {
+ const container = document.querySelector('[data-panel="interrupted"] .panel-task-list');
+ if (!container) return;
+ const visible = sortTasksByDate(tasks.filter(t => INTERRUPTED_STATES.has(t.state)), true);
+ renderTasksIntoContainer(visible, container, 'No interrupted tasks.');
+}
+
+function renderReadyPanel(tasks) {
+ const container = document.querySelector('[data-panel="ready"] .panel-task-list');
+ if (!container) return;
+ const visible = sortTasksByDate(filterReadyTasks(tasks));
+ renderTasksIntoContainer(visible, container, 'No tasks awaiting review.');
+}
+
+function renderAllPanel(tasks) {
+ const container = document.querySelector('[data-panel="all"] .all-history');
+ if (!container) return;
+ const visible = sortTasksByDate(filterAllDoneTasks(tasks), true);
+ renderTasksIntoContainer(visible, container, 'No completed tasks in the last 24h.');
}
// ── Run action ────────────────────────────────────────────────────────────────
@@ -438,9 +464,8 @@ async function handleRun(taskId, btn, footer) {
try {
await runTask(taskId);
- // Refresh list immediately so state flips to QUEUED
- const tasks = await fetchTasks();
- renderTaskList(tasks);
+ // Refresh active panel so state flips to QUEUED
+ await poll();
} catch (err) {
btn.disabled = false;
btn.textContent = 'Run';
@@ -871,34 +896,51 @@ async function handleStartNextTask(btn) {
// ── Polling ───────────────────────────────────────────────────────────────────
-function isStatsTabActive() {
- const panel = document.querySelector('[data-panel="stats"]');
- return panel && !panel.hasAttribute('hidden');
+function getActiveTab() {
+ const active = document.querySelector('.tab.active');
+ return active ? active.dataset.tab : 'queue';
}
async function poll() {
try {
const tasks = await fetchTasks();
if (isUserEditing()) return;
- renderTaskList(tasks);
- renderActiveTaskList(tasks);
- if (isRunningTabActive()) {
- renderRunningView(tasks);
- fetchRecentExecutions(BASE_PATH, fetch)
- .then(execs => renderRunningHistory(execs))
- .catch(() => {
- const histEl = document.querySelector('.running-history');
- if (histEl) histEl.innerHTML = '<p class="task-meta">Could not load execution history.</p>';
- });
- }
- if (isStatsTabActive()) {
- fetchRecentExecutions(BASE_PATH, fetch)
- .then(execs => renderStatsPanel(tasks, execs))
- .catch(() => {});
+
+ const activeTab = getActiveTab();
+ switch (activeTab) {
+ case 'queue':
+ renderQueuePanel(tasks);
+ break;
+ case 'interrupted':
+ renderInterruptedPanel(tasks);
+ break;
+ case 'ready':
+ renderReadyPanel(tasks);
+ break;
+ case 'running':
+ renderRunningView(tasks);
+ fetchRecentExecutions(BASE_PATH, fetch)
+ .then(execs => renderRunningHistory(execs))
+ .catch(() => {
+ const histEl = document.querySelector('.running-history');
+ if (histEl) histEl.innerHTML = '<p class="task-meta">Could not load execution history.</p>';
+ });
+ break;
+ case 'all':
+ renderAllPanel(tasks);
+ break;
+ case 'stats':
+ fetchRecentExecutions(BASE_PATH, fetch)
+ .then(execs => renderStatsPanel(tasks, execs))
+ .catch(() => {});
+ break;
+ case 'settings':
+ // nothing to render
+ break;
}
} catch {
- document.querySelector('.task-list').innerHTML =
- '<div id="loading">Could not reach server.</div>';
+ const panel = document.querySelector('[data-panel="queue"] .panel-task-list');
+ if (panel) panel.innerHTML = '<div class="task-empty">Could not reach server.</div>';
}
}
@@ -1232,8 +1274,7 @@ async function createTask(formData) {
}
closeTaskModal();
- const tasks = await fetchTasks();
- renderTaskList(tasks);
+ await poll();
}
// ── Task side panel ───────────────────────────────────────────────────────────
@@ -2201,48 +2242,18 @@ function switchTab(name) {
}
});
- if (name === 'running') {
- fetchTasks().then(renderRunningView).catch(() => {
- const currentEl = document.querySelector('.running-current');
- if (currentEl) currentEl.innerHTML = '<p class="task-meta">Could not reach server.</p>';
- });
- fetchRecentExecutions(BASE_PATH, fetch)
- .then(execs => renderRunningHistory(execs))
- .catch(() => {
- const histEl = document.querySelector('.running-history');
- if (histEl) histEl.innerHTML = '<p class="task-meta">Could not load execution history.</p>';
- });
- }
-
- if (name === 'stats') {
- Promise.all([
- fetchTasks(),
- fetchRecentExecutions(BASE_PATH, fetch),
- ]).then(([tasks, execs]) => renderStatsPanel(tasks, execs)).catch(() => {
- const panel = document.querySelector('[data-panel="stats"]');
- if (panel) panel.innerHTML = '<p class="task-meta">Could not load stats.</p>';
- });
- }
+ // Trigger immediate render for the newly active tab
+ poll();
}
// ── Boot ──────────────────────────────────────────────────────────────────────
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() {
handleStartNextTask(this);
});
- switchTab('tasks');
+ switchTab('queue');
startPolling();
connectWebSocket();