summaryrefslogtreecommitdiff
path: root/web/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'web/app.js')
-rw-r--r--web/app.js152
1 files changed, 152 insertions, 0 deletions
diff --git a/web/app.js b/web/app.js
new file mode 100644
index 0000000..e66c878
--- /dev/null
+++ b/web/app.js
@@ -0,0 +1,152 @@
+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 = '<div id="loading">No tasks found.</div>';
+ 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 =
+ '<div id="loading">Could not reach server.</div>';
+ }
+}
+
+function startPolling(intervalMs = 10_000) {
+ poll();
+ setInterval(poll, intervalMs);
+}
+
+// ── Boot ──────────────────────────────────────────────────────────────────────
+
+startPolling();