summaryrefslogtreecommitdiff
path: root/web/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'web/app.js')
-rw-r--r--web/app.js703
1 files changed, 699 insertions, 4 deletions
diff --git a/web/app.js b/web/app.js
index e66c878..6289d00 100644
--- a/web/app.js
+++ b/web/app.js
@@ -1,4 +1,5 @@
-const API_BASE = window.location.origin;
+const BASE_PATH = document.querySelector('meta[name="base-path"]')?.content ?? '';
+const API_BASE = window.location.origin + BASE_PATH;
// ── Fetch ─────────────────────────────────────────────────────────────────────
@@ -8,6 +9,12 @@ async function fetchTasks() {
return res.json();
}
+async function fetchTemplates() {
+ const res = await fetch(`${API_BASE}/api/templates`);
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ return res.json();
+}
+
// ── Render ────────────────────────────────────────────────────────────────────
function formatDate(iso) {
@@ -70,15 +77,48 @@ function createTaskCard(task) {
const btn = document.createElement('button');
btn.className = 'btn-run';
btn.textContent = 'Run';
- btn.addEventListener('click', () => handleRun(task.id, btn, footer));
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ handleRun(task.id, btn, footer);
+ });
footer.appendChild(btn);
card.appendChild(footer);
}
+ card.addEventListener('click', () => openTaskPanel(task.id));
+
return card;
}
+// ── Filter ────────────────────────────────────────────────────────────────────
+
+const HIDE_STATES = new Set(['COMPLETED', 'FAILED']);
+
+let showHiddenFold = false;
+
+function filterTasks(tasks, hideCompletedFailed = false) {
+ if (!hideCompletedFailed) return tasks;
+ return tasks.filter(t => !HIDE_STATES.has(t.state));
+}
+
+function getHideCompletedFailed() {
+ const stored = localStorage.getItem('hideCompletedFailed');
+ return stored === null ? true : stored === 'true';
+}
+
+function setHideCompletedFailed(val) {
+ localStorage.setItem('hideCompletedFailed', String(val));
+}
+
+function updateToggleButton() {
+ const btn = document.getElementById('btn-toggle-completed');
+ if (!btn) return;
+ btn.textContent = getHideCompletedFailed()
+ ? 'Show completed/failed'
+ : 'Hide completed/failed';
+}
+
function renderTaskList(tasks) {
const container = document.querySelector('.task-list');
@@ -87,11 +127,93 @@ function renderTaskList(tasks) {
return;
}
+ const hide = getHideCompletedFailed();
+ const visible = filterTasks(tasks, hide);
+ const hiddenCount = tasks.length - visible.length;
+
// Replace contents with task cards
container.innerHTML = '';
- for (const task of tasks) {
+ 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 createTemplateCard(tmpl) {
+ const card = document.createElement('div');
+ card.className = 'template-card';
+
+ const name = document.createElement('div');
+ name.className = 'template-name';
+ name.textContent = tmpl.name;
+ card.appendChild(name);
+
+ if (tmpl.description) {
+ const desc = document.createElement('div');
+ desc.className = 'template-description';
+ desc.textContent = tmpl.description;
+ card.appendChild(desc);
+ }
+
+ if (tmpl.tags && tmpl.tags.length > 0) {
+ const tagsEl = document.createElement('div');
+ tagsEl.className = 'template-tags';
+ for (const tag of tmpl.tags) {
+ const chip = document.createElement('span');
+ chip.className = 'tag-chip';
+ chip.textContent = tag;
+ tagsEl.appendChild(chip);
+ }
+ card.appendChild(tagsEl);
+ }
+
+ const footer = document.createElement('div');
+ footer.className = 'template-card-footer';
+
+ const delBtn = document.createElement('button');
+ delBtn.className = 'btn-danger btn-sm';
+ delBtn.textContent = 'Delete';
+ delBtn.addEventListener('click', () => deleteTemplate(tmpl.id));
+
+ footer.appendChild(delBtn);
+ card.appendChild(footer);
+
+ return card;
+}
+
+function renderTemplateList(templates) {
+ const container = document.querySelector('.template-list');
+
+ if (!templates || templates.length === 0) {
+ container.innerHTML = '<div id="loading">No templates yet.</div>';
+ return;
+ }
+
+ container.innerHTML = '';
+ for (const tmpl of templates) {
+ container.appendChild(createTemplateCard(tmpl));
+ }
}
// ── Run action ────────────────────────────────────────────────────────────────
@@ -130,6 +252,23 @@ async function handleRun(taskId, btn, footer) {
}
}
+// ── Delete template ────────────────────────────────────────────────────────────
+
+async function deleteTemplate(id) {
+ if (!window.confirm('Delete this template?')) return;
+
+ const res = await fetch(`${API_BASE}/api/templates/${id}`, { method: 'DELETE' });
+ if (!res.ok) {
+ let msg = `HTTP ${res.status}`;
+ try { const body = await res.json(); msg = body.error || body.message || msg; } catch {}
+ alert(`Failed to delete: ${msg}`);
+ return;
+ }
+
+ const templates = await fetchTemplates();
+ renderTemplateList(templates);
+}
+
// ── Polling ───────────────────────────────────────────────────────────────────
async function poll() {
@@ -147,6 +286,562 @@ function startPolling(intervalMs = 10_000) {
setInterval(poll, intervalMs);
}
+// ── Elaborate (Draft with AI) ─────────────────────────────────────────────────
+
+async function elaborateTask(prompt) {
+ const res = await fetch(`${API_BASE}/api/tasks/elaborate`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ prompt }),
+ });
+ if (!res.ok) {
+ let msg = `HTTP ${res.status}`;
+ try { const body = await res.json(); msg = body.error || msg; } catch {}
+ throw new Error(msg);
+ }
+ return res.json();
+}
+
+// ── Task modal ────────────────────────────────────────────────────────────────
+
+function openTaskModal() {
+ document.getElementById('task-modal').showModal();
+}
+
+function closeTaskModal() {
+ document.getElementById('task-modal').close();
+ document.getElementById('task-form').reset();
+ document.getElementById('elaborate-prompt').value = '';
+}
+
+async function createTask(formData) {
+ const body = {
+ name: formData.get('name'),
+ description: '',
+ claude: {
+ model: formData.get('model'),
+ instructions: formData.get('instructions'),
+ working_dir: formData.get('working_dir'),
+ max_budget_usd: parseFloat(formData.get('max_budget_usd')),
+ },
+ timeout: formData.get('timeout'),
+ priority: formData.get('priority'),
+ tags: [],
+ };
+
+ const res = await fetch(`${API_BASE}/api/tasks`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(text || `HTTP ${res.status}`);
+ }
+
+ closeTaskModal();
+ const tasks = await fetchTasks();
+ renderTaskList(tasks);
+}
+
+// ── Template modal ────────────────────────────────────────────────────────────
+
+function openTemplateModal() {
+ document.getElementById('template-modal').showModal();
+}
+
+function closeTemplateModal() {
+ document.getElementById('template-modal').close();
+ document.getElementById('template-form').reset();
+}
+
+async function saveTemplate(formData) {
+ const splitTrim = val => val.split(',').map(s => s.trim()).filter(Boolean);
+
+ const body = {
+ name: formData.get('name'),
+ description: formData.get('description'),
+ claude: {
+ model: formData.get('model'),
+ instructions: formData.get('instructions'),
+ working_dir: formData.get('working_dir'),
+ max_budget_usd: parseFloat(formData.get('max_budget_usd')),
+ allowed_tools: splitTrim(formData.get('allowed_tools') || ''),
+ },
+ timeout: formData.get('timeout'),
+ priority: formData.get('priority'),
+ tags: splitTrim(formData.get('tags') || ''),
+ };
+
+ const res = await fetch(`${API_BASE}/api/templates`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(text || `HTTP ${res.status}`);
+ }
+
+ closeTemplateModal();
+ const templates = await fetchTemplates();
+ renderTemplateList(templates);
+}
+
+// ── Task side panel ───────────────────────────────────────────────────────────
+
+// Format Go's task.Duration JSON value {"Duration": <nanoseconds>} to human string.
+function formatDurationNs(timeout) {
+ const ns = timeout && timeout.Duration;
+ if (!ns) return '—';
+ const secs = ns / 1e9;
+ if (secs < 60) return `${secs.toFixed(1)}s`;
+ const mins = Math.floor(secs / 60);
+ const remSecs = Math.floor(secs % 60);
+ if (mins < 60) return remSecs > 0 ? `${mins}m ${remSecs}s` : `${mins}m`;
+ const hrs = Math.floor(mins / 60);
+ const remMins = mins % 60;
+ return remMins > 0 ? `${hrs}h ${remMins}m` : `${hrs}h`;
+}
+
+function formatDateLong(iso) {
+ if (!iso) return '—';
+ return new Date(iso).toLocaleString(undefined, {
+ year: 'numeric', month: 'short', day: 'numeric',
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
+ });
+}
+
+function openTaskPanel(taskId) {
+ const panel = document.getElementById('task-panel');
+ const backdrop = document.getElementById('task-panel-backdrop');
+ const content = document.getElementById('task-panel-content');
+ document.getElementById('task-panel-title').textContent = 'Task Details';
+
+ content.innerHTML = '';
+ const loading = document.createElement('div');
+ loading.className = 'panel-loading';
+ loading.textContent = 'Loading…';
+ content.appendChild(loading);
+
+ backdrop.hidden = false;
+ panel.classList.add('open');
+
+ Promise.all([
+ fetch(`${API_BASE}/api/tasks/${taskId}`).then(r => {
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
+ return r.json();
+ }),
+ fetch(`${API_BASE}/api/tasks/${taskId}/executions`).then(r => {
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
+ return r.json();
+ }),
+ ]).then(([task, executions]) => {
+ renderTaskPanel(task, executions);
+ }).catch(err => {
+ content.innerHTML = '';
+ const errEl = document.createElement('div');
+ errEl.className = 'panel-fetch-error';
+ errEl.textContent = `Failed to load: ${err.message}`;
+ content.appendChild(errEl);
+ });
+}
+
+function closeTaskPanel() {
+ document.getElementById('task-panel').classList.remove('open');
+ document.getElementById('task-panel-backdrop').hidden = true;
+}
+
+function makeSection(title) {
+ const section = document.createElement('div');
+ section.className = 'panel-section';
+ const hdr = document.createElement('div');
+ hdr.className = 'panel-section-title';
+ hdr.textContent = title;
+ section.appendChild(hdr);
+ return section;
+}
+
+function makeMetaItem(label, valueText, opts = {}) {
+ const item = document.createElement('div');
+ item.className = 'meta-item' + (opts.fullWidth ? ' full-width' : '');
+
+ const lbl = document.createElement('div');
+ lbl.className = 'meta-label';
+ lbl.textContent = label;
+ item.appendChild(lbl);
+
+ if (opts.badge) {
+ const badge = document.createElement('span');
+ badge.className = 'state-badge';
+ badge.dataset.state = valueText;
+ badge.textContent = valueText.replace(/_/g, ' ');
+ item.appendChild(badge);
+ } else if (opts.code) {
+ const pre = document.createElement('pre');
+ pre.className = 'panel-code';
+ pre.textContent = valueText;
+ item.appendChild(pre);
+ } else if (opts.tags) {
+ const wrap = document.createElement('div');
+ if (opts.tags.length > 0) {
+ wrap.className = 'panel-tags';
+ for (const tag of opts.tags) {
+ const chip = document.createElement('span');
+ chip.className = 'tag-chip';
+ chip.textContent = tag;
+ wrap.appendChild(chip);
+ }
+ } else {
+ wrap.className = 'meta-value muted';
+ wrap.textContent = '—';
+ }
+ item.appendChild(wrap);
+ } else {
+ const val = document.createElement('div');
+ val.className = 'meta-value' + (opts.mono ? ' mono' : '') + (opts.muted ? ' muted' : '');
+ val.textContent = valueText || '—';
+ item.appendChild(val);
+ }
+ return item;
+}
+
+function renderTaskPanel(task, executions) {
+ document.getElementById('task-panel-title').textContent = task.name;
+ const content = document.getElementById('task-panel-content');
+ content.innerHTML = '';
+
+ // ── Overview ──
+ const overview = makeSection('Overview');
+ const overviewGrid = document.createElement('div');
+ overviewGrid.className = 'meta-grid';
+ overviewGrid.append(
+ makeMetaItem('State', task.state, { badge: true }),
+ makeMetaItem('Priority', task.priority),
+ makeMetaItem('Created', formatDateLong(task.created_at)),
+ makeMetaItem('Updated', formatDateLong(task.updated_at)),
+ makeMetaItem('ID', task.id, { fullWidth: true, mono: true }),
+ );
+ if (task.parent_task_id) {
+ overviewGrid.append(makeMetaItem('Parent Task', task.parent_task_id, { fullWidth: true, mono: true }));
+ }
+ if (task.tags && task.tags.length >= 0) {
+ overviewGrid.append(makeMetaItem('Tags', '', { fullWidth: true, tags: task.tags || [] }));
+ }
+ if (task.description) {
+ overviewGrid.append(makeMetaItem('Description', task.description, { fullWidth: true }));
+ }
+ overview.appendChild(overviewGrid);
+ content.appendChild(overview);
+
+ // ── Claude Config ──
+ const c = task.claude || {};
+ const claudeSection = makeSection('Claude Config');
+ const claudeGrid = document.createElement('div');
+ claudeGrid.className = 'meta-grid';
+ claudeGrid.append(
+ makeMetaItem('Model', c.model),
+ makeMetaItem('Max Budget', c.max_budget_usd != null ? `$${c.max_budget_usd.toFixed(2)}` : '—'),
+ makeMetaItem('Working Dir', c.working_dir),
+ makeMetaItem('Permission Mode', c.permission_mode || 'default'),
+ );
+ if (c.allowed_tools && c.allowed_tools.length > 0) {
+ claudeGrid.append(makeMetaItem('Allowed Tools', c.allowed_tools.join(', '), { fullWidth: true }));
+ }
+ if (c.disallowed_tools && c.disallowed_tools.length > 0) {
+ claudeGrid.append(makeMetaItem('Disallowed Tools', c.disallowed_tools.join(', '), { fullWidth: true }));
+ }
+ if (c.instructions) {
+ claudeGrid.append(makeMetaItem('Instructions', c.instructions, { fullWidth: true, code: true }));
+ }
+ if (c.system_prompt_append) {
+ claudeGrid.append(makeMetaItem('System Prompt Append', c.system_prompt_append, { fullWidth: true, code: true }));
+ }
+ claudeSection.appendChild(claudeGrid);
+ content.appendChild(claudeSection);
+
+ // ── Execution Settings ──
+ const settingsSection = makeSection('Execution Settings');
+ const settingsGrid = document.createElement('div');
+ settingsGrid.className = 'meta-grid';
+ settingsGrid.append(
+ makeMetaItem('Timeout', formatDurationNs(task.timeout)),
+ makeMetaItem('Retry Attempts', String(task.retry ? task.retry.max_attempts : 1)),
+ makeMetaItem('Backoff', task.retry ? task.retry.backoff : '—'),
+ );
+ if (task.depends_on && task.depends_on.length > 0) {
+ settingsGrid.append(makeMetaItem('Depends On', task.depends_on.join(', '), { fullWidth: true, mono: true }));
+ }
+ settingsSection.appendChild(settingsGrid);
+ content.appendChild(settingsSection);
+
+ // ── Executions ──
+ const execSection = makeSection('Executions');
+ if (!executions || executions.length === 0) {
+ const none = document.createElement('div');
+ none.className = 'meta-value muted';
+ none.textContent = 'No executions yet.';
+ execSection.appendChild(none);
+ } else {
+ const list = document.createElement('div');
+ list.className = 'executions-list';
+ // Newest first
+ for (const exec of [...executions].reverse()) {
+ const row = document.createElement('div');
+ row.className = 'execution-row';
+
+ const shortId = document.createElement('span');
+ shortId.className = 'execution-id';
+ shortId.textContent = exec.ID ? exec.ID.slice(0, 8) : '—';
+ row.appendChild(shortId);
+
+ const badge = document.createElement('span');
+ badge.className = 'state-badge';
+ badge.dataset.state = exec.Status || '';
+ badge.textContent = (exec.Status || '—').replace(/_/g, ' ');
+ row.appendChild(badge);
+
+ const times = document.createElement('span');
+ times.className = 'execution-times';
+ const start = exec.StartTime ? formatDate(exec.StartTime) : '?';
+ const end = exec.EndTime && exec.EndTime !== '0001-01-01T00:00:00Z' ? formatDate(exec.EndTime) : '…';
+ times.textContent = `${start} → ${end}`;
+ row.appendChild(times);
+
+ if (exec.CostUSD != null && exec.CostUSD > 0) {
+ const cost = document.createElement('span');
+ cost.className = 'execution-cost';
+ cost.textContent = `$${exec.CostUSD.toFixed(4)}`;
+ row.appendChild(cost);
+ }
+
+ const exitEl = document.createElement('span');
+ exitEl.className = 'execution-exit';
+ exitEl.textContent = `exit: ${exec.ExitCode ?? '—'}`;
+ row.appendChild(exitEl);
+
+ const logsBtn = document.createElement('button');
+ logsBtn.className = 'btn-view-logs';
+ logsBtn.textContent = 'View Logs';
+ logsBtn.addEventListener('click', () => handleViewLogs(exec.ID));
+ row.appendChild(logsBtn);
+
+ list.appendChild(row);
+ }
+ execSection.appendChild(list);
+ }
+ content.appendChild(execSection);
+}
+
+async function handleViewLogs(execId) {
+ const modal = document.getElementById('logs-modal');
+ const body = document.getElementById('logs-modal-body');
+ document.getElementById('logs-modal-title').textContent = `Execution ${execId.slice(0, 8)}`;
+ body.innerHTML = '<div class="panel-loading">Loading…</div>';
+ modal.showModal();
+
+ try {
+ const res = await fetch(`${API_BASE}/api/executions/${execId}`);
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const exec = await res.json();
+
+ body.innerHTML = '';
+ const grid = document.createElement('div');
+ grid.className = 'meta-grid';
+
+ const entries = [
+ ['ID', exec.ID, { fullWidth: true, mono: true }],
+ ['Status', exec.Status, { badge: true }],
+ ['Exit Code', String(exec.ExitCode ?? '—'), {}],
+ ['Cost', exec.CostUSD > 0 ? `$${exec.CostUSD.toFixed(4)}` : '—', {}],
+ ['Start', formatDateLong(exec.StartTime), {}],
+ ['End', exec.EndTime && !exec.EndTime.startsWith('0001-') ? formatDateLong(exec.EndTime) : '—', {}],
+ ['Error', exec.ErrorMsg || '—', { fullWidth: true }],
+ ['Stdout', exec.StdoutPath || '—', { fullWidth: true, mono: true }],
+ ['Stderr', exec.StderrPath || '—', { fullWidth: true, mono: true }],
+ ];
+ for (const [label, value, opts] of entries) {
+ grid.appendChild(makeMetaItem(label, value, opts));
+ }
+ body.appendChild(grid);
+ } catch (err) {
+ body.innerHTML = `<div class="panel-fetch-error">Failed to load: ${err.message}</div>`;
+ }
+}
+
+// ── Tab switching ─────────────────────────────────────────────────────────────
+
+function switchTab(name) {
+ // Update tab button active state
+ document.querySelectorAll('.tab').forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.tab === name);
+ });
+
+ // Show/hide panels
+ document.querySelectorAll('[data-panel]').forEach(panel => {
+ if (panel.dataset.panel === name) {
+ panel.removeAttribute('hidden');
+ } else {
+ panel.setAttribute('hidden', '');
+ }
+ });
+
+ // Show/hide the header New Task button (only relevant on tasks tab)
+ document.getElementById('btn-new-task').style.display =
+ name === 'tasks' ? '' : 'none';
+
+ if (name === 'templates') {
+ fetchTemplates().then(renderTemplateList).catch(() => {
+ document.querySelector('.template-list').innerHTML =
+ '<div id="loading">Could not reach server.</div>';
+ });
+ }
+}
+
// ── Boot ──────────────────────────────────────────────────────────────────────
-startPolling();
+document.addEventListener('DOMContentLoaded', () => {
+ updateToggleButton();
+ document.getElementById('btn-toggle-completed').addEventListener('click', async () => {
+ setHideCompletedFailed(!getHideCompletedFailed());
+ updateToggleButton();
+ await poll();
+ });
+
+ startPolling();
+
+ // Side panel close
+ document.getElementById('btn-close-panel').addEventListener('click', closeTaskPanel);
+ document.getElementById('task-panel-backdrop').addEventListener('click', closeTaskPanel);
+
+ // Execution logs modal close
+ document.getElementById('btn-close-logs').addEventListener('click', () => {
+ document.getElementById('logs-modal').close();
+ });
+
+ // Tab bar
+ document.querySelectorAll('.tab').forEach(btn => {
+ btn.addEventListener('click', () => switchTab(btn.dataset.tab));
+ });
+
+ // Task modal
+ document.getElementById('btn-new-task').addEventListener('click', openTaskModal);
+ document.getElementById('btn-cancel-task').addEventListener('click', closeTaskModal);
+
+ // Draft with AI button
+ const btnElaborate = document.getElementById('btn-elaborate');
+ btnElaborate.addEventListener('click', async () => {
+ const prompt = document.getElementById('elaborate-prompt').value.trim();
+ if (!prompt) {
+ const form = document.getElementById('task-form');
+ // Remove previous error
+ const prev = form.querySelector('.form-error');
+ if (prev) prev.remove();
+ const errEl = document.createElement('p');
+ errEl.className = 'form-error';
+ errEl.textContent = 'Please enter a description before drafting.';
+ form.querySelector('.elaborate-section').appendChild(errEl);
+ return;
+ }
+
+ btnElaborate.disabled = true;
+ btnElaborate.textContent = 'Drafting…';
+
+ // Remove any previous errors or banners
+ const form = document.getElementById('task-form');
+ form.querySelectorAll('.form-error, .elaborate-banner').forEach(el => el.remove());
+
+ try {
+ const result = await elaborateTask(prompt);
+
+ // Populate form fields
+ const f = document.getElementById('task-form');
+ if (result.name)
+ f.querySelector('[name="name"]').value = result.name;
+ if (result.claude && result.claude.instructions)
+ f.querySelector('[name="instructions"]').value = result.claude.instructions;
+ if (result.claude && result.claude.working_dir)
+ f.querySelector('[name="working_dir"]').value = result.claude.working_dir;
+ if (result.claude && result.claude.model)
+ f.querySelector('[name="model"]').value = result.claude.model;
+ if (result.claude && result.claude.max_budget_usd != null)
+ f.querySelector('[name="max_budget_usd"]').value = result.claude.max_budget_usd;
+ if (result.timeout)
+ f.querySelector('[name="timeout"]').value = result.timeout;
+ if (result.priority) {
+ const sel = f.querySelector('[name="priority"]');
+ if ([...sel.options].some(o => o.value === result.priority)) {
+ sel.value = result.priority;
+ }
+ }
+
+ // Show success banner
+ const banner = document.createElement('p');
+ banner.className = 'elaborate-banner';
+ banner.textContent = 'AI draft ready — review and submit.';
+ document.getElementById('task-form').querySelector('.elaborate-section').appendChild(banner);
+ } catch (err) {
+ const errEl = document.createElement('p');
+ errEl.className = 'form-error';
+ errEl.textContent = `Elaboration failed: ${err.message}`;
+ document.getElementById('task-form').querySelector('.elaborate-section').appendChild(errEl);
+ } finally {
+ btnElaborate.disabled = false;
+ btnElaborate.textContent = 'Draft with AI ✦';
+ }
+ });
+
+ document.getElementById('task-form').addEventListener('submit', async e => {
+ e.preventDefault();
+
+ // Remove any previous error
+ const prev = e.target.querySelector('.form-error');
+ if (prev) prev.remove();
+
+ const btn = e.submitter;
+ btn.disabled = true;
+ btn.textContent = 'Creating…';
+
+ try {
+ await createTask(new FormData(e.target));
+ } catch (err) {
+ const errEl = document.createElement('p');
+ errEl.className = 'form-error';
+ errEl.textContent = err.message;
+ e.target.appendChild(errEl);
+ } finally {
+ btn.disabled = false;
+ btn.textContent = 'Create & Queue';
+ }
+ });
+
+ // Template modal
+ document.getElementById('btn-new-template').addEventListener('click', openTemplateModal);
+ document.getElementById('btn-cancel-template').addEventListener('click', closeTemplateModal);
+
+ document.getElementById('template-form').addEventListener('submit', async e => {
+ e.preventDefault();
+
+ // Remove any previous error
+ const prev = e.target.querySelector('.form-error');
+ if (prev) prev.remove();
+
+ const btn = e.submitter;
+ btn.disabled = true;
+ btn.textContent = 'Saving…';
+
+ try {
+ await saveTemplate(new FormData(e.target));
+ } catch (err) {
+ const errEl = document.createElement('p');
+ errEl.className = 'form-error';
+ errEl.textContent = err.message;
+ e.target.appendChild(errEl);
+ } finally {
+ btn.disabled = false;
+ btn.textContent = 'Save Template';
+ }
+ });
+});