summaryrefslogtreecommitdiff
path: root/web/app.js
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator.local>2026-07-06 05:26:24 +0000
committerClaudomator Agent <agent@claudomator.local>2026-07-06 05:26:24 +0000
commitd2cbe9aa07afdc0aac33c959803d6a6aaef69b8e (patch)
treec7861cc4c1a8472d48dff13be98b7d634090191a /web/app.js
parentd9db12a25a42d00e0f64ac87e10ac0b612b841a8 (diff)
fix(web): correct elapsed timer class/location, log-tail coverage, and tests (task 4 reviewer findings)
- Move elapsed timer span from card header to after meta block; add task-elapsed class alongside running-elapsed (Finding 1) - Replace PENDING/QUEUED-only log-tail div with full if/else: placeholder div.task-log-tail-placeholder for queue states, div.task-log-tail with dataset.logTarget for all other states (Finding 2) - Delete web/test/task-card-di.test.mjs; append correct tests to web/test/tasks-board.test.mjs covering log-tail and elapsed timer contracts (Finding 3) - Thread doc parameter through createEditForm, renderSubtaskRollup, and renderQuestionFooter so createTaskCard is fully testable without a browser DOM Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'web/app.js')
-rw-r--r--web/app.js92
1 files changed, 51 insertions, 41 deletions
diff --git a/web/app.js b/web/app.js
index 5444511..8bc6a9f 100644
--- a/web/app.js
+++ b/web/app.js
@@ -294,13 +294,6 @@ export function createTaskCard(task, doc = document) {
badge.textContent = task.state.replace(/_/g, ' ');
header.append(name, badge);
- if (task.state === 'RUNNING') {
- const elapsed = doc.createElement('span');
- elapsed.className = 'running-elapsed';
- elapsed.dataset.startedAt = task.updated_at ?? '';
- elapsed.textContent = formatElapsed(task.updated_at);
- header.append(elapsed);
- }
card.appendChild(header);
// Meta: priority + created_at
@@ -324,6 +317,17 @@ export function createTaskCard(task, doc = document) {
}
if (meta.children.length) card.appendChild(meta);
+ // Elapsed timer for RUNNING tasks (no separate ticking interval — updated
+ // on each poll tick alongside the rest of the board, same convention the
+ // old Running-tab card used).
+ if (task.state === 'RUNNING') {
+ const elapsed = doc.createElement('span');
+ elapsed.className = 'task-elapsed running-elapsed';
+ elapsed.dataset.startedAt = task.updated_at ?? '';
+ elapsed.textContent = formatElapsed(task.updated_at);
+ card.appendChild(elapsed);
+ }
+
// Description (truncated via CSS)
if (task.description) {
const desc = doc.createElement('div');
@@ -370,14 +374,21 @@ export function createTaskCard(task, doc = document) {
if (depBadge) card.appendChild(depBadge);
}
- // Log-tail placeholder for PENDING/QUEUED tasks (no execution exists yet).
+ // Inline log tail (no click required) — see docs/superpowers/specs/2026-07-06-tasks-board-design.md
+ // section 4. PENDING/QUEUED tasks have no execution row yet, so there is
+ // nothing to tail; every other state gets a .task-log-tail element that
+ // Task 6's ensureTaskLogStream() attaches an SSE stream to.
if (task.state === 'PENDING' || task.state === 'QUEUED') {
+ const placeholder = doc.createElement('div');
+ placeholder.className = 'task-log-tail-placeholder';
+ placeholder.textContent = 'Waiting to start…';
+ card.appendChild(placeholder);
+ } else {
const logTail = doc.createElement('div');
- logTail.className = 'task-log-tail';
- const placeholder = doc.createElement('p');
- placeholder.className = 'task-log-placeholder';
- placeholder.textContent = 'Waiting to start\u2026';
- logTail.appendChild(placeholder);
+ logTail.className = task.state === 'RUNNING' || task.state === 'BLOCKED'
+ ? 'task-log-tail running-log'
+ : 'task-log-tail';
+ logTail.dataset.logTarget = task.id;
card.appendChild(logTail);
}
@@ -409,7 +420,7 @@ export function createTaskCard(task, doc = document) {
});
footer.appendChild(btn);
} else if (task.state === 'READY') {
- renderSubtaskRollup(task, footer);
+ renderSubtaskRollup(task, footer, doc);
const acceptBtn = doc.createElement('button');
acceptBtn.className = 'btn-accept';
acceptBtn.textContent = 'Accept';
@@ -428,9 +439,9 @@ export function createTaskCard(task, doc = document) {
footer.appendChild(rejectBtn);
} else if (task.state === 'BLOCKED') {
if (task.question) {
- renderQuestionFooter(task, footer);
+ renderQuestionFooter(task, footer, doc);
} else {
- renderSubtaskRollup(task, footer);
+ renderSubtaskRollup(task, footer, doc);
}
const cancelBtn = doc.createElement('button');
cancelBtn.className = 'btn-cancel';
@@ -478,7 +489,7 @@ export function createTaskCard(task, doc = document) {
if (EDITABLE_STATES.has(task.state)) {
card.classList.add('task-card--editable');
- const editForm = createEditForm(task);
+ const editForm = createEditForm(task, doc);
editForm.hidden = true;
card.appendChild(editForm);
card.addEventListener('click', () => { editForm.hidden = !editForm.hidden; });
@@ -950,21 +961,20 @@ async function updateTask(taskId, body) {
return res.json();
}
-function createEditForm(task) {
+function createEditForm(task, doc = document) {
const a = task.agent || {};
- const form = document.createElement('div');
+ const form = doc.createElement('div');
form.className = 'task-inline-edit';
// Prevent card-level click from toggling this form while user interacts inside it.
form.addEventListener('click', (e) => e.stopPropagation());
function makeField(labelText, tag, attrs) {
- const label = document.createElement('label');
+ const label = doc.createElement('label');
label.textContent = labelText;
- const el = document.createElement(tag);
+ const el = doc.createElement(tag);
for (const [k, v] of Object.entries(attrs)) {
- if (k === 'value') el.value = v;
- else el.setAttribute(k, v);
+ el[k] = v;
}
label.appendChild(el);
return label;
@@ -973,17 +983,17 @@ function createEditForm(task) {
form.appendChild(makeField('Name', 'input', { type: 'text', name: 'name', value: task.name || '' }));
form.appendChild(makeField('Description', 'textarea', { name: 'description', rows: '2', value: task.description || '' }));
form.appendChild(makeField('Instructions', 'textarea', { name: 'instructions', rows: '4', value: a.instructions || '' }));
-
+
form.appendChild(makeField('Project Directory', 'input', { type: 'text', name: 'project_dir', value: a.project_dir || a.working_dir || '', placeholder: '/path/to/repo' }));
form.appendChild(makeField('Max Budget (USD)', 'input', { type: 'number', name: 'max_budget_usd', step: '0.01', value: a.max_budget_usd != null ? String(a.max_budget_usd) : '1.00' }));
form.appendChild(makeField('Timeout', 'input', { type: 'text', name: 'timeout', value: formatDurationForInput(task.timeout) || '15m', placeholder: '15m' }));
- const prioLabel = document.createElement('label');
+ const prioLabel = doc.createElement('label');
prioLabel.textContent = 'Priority';
- const prioSel = document.createElement('select');
+ const prioSel = doc.createElement('select');
prioSel.name = 'priority';
for (const val of ['high', 'normal', 'low']) {
- const opt = document.createElement('option');
+ const opt = doc.createElement('option');
opt.value = val;
opt.textContent = val.charAt(0).toUpperCase() + val.slice(1);
if (val === (task.priority || 'normal')) opt.selected = true;
@@ -992,20 +1002,20 @@ function createEditForm(task) {
prioLabel.appendChild(prioSel);
form.appendChild(prioLabel);
- const errEl = document.createElement('div');
+ const errEl = doc.createElement('div');
errEl.className = 'inline-edit-error';
errEl.hidden = true;
form.appendChild(errEl);
- const actions = document.createElement('div');
+ const actions = doc.createElement('div');
actions.className = 'inline-edit-actions';
- const cancelBtn = document.createElement('button');
+ const cancelBtn = doc.createElement('button');
cancelBtn.type = 'button';
cancelBtn.textContent = 'Cancel';
cancelBtn.addEventListener('click', () => { form.hidden = true; });
- const saveBtn = document.createElement('button');
+ const saveBtn = doc.createElement('button');
saveBtn.type = 'button';
saveBtn.className = 'btn-primary btn-sm';
saveBtn.textContent = 'Save';
@@ -1059,7 +1069,7 @@ async function handleEditSave(taskId, form, saveBtn) {
}
}
-function renderQuestionFooter(task, footer) {
+function renderQuestionFooter(task, footer, doc = document) {
// Prevent any tap inside the question footer from opening the detail panel.
footer.addEventListener('click', (e) => e.stopPropagation());
@@ -1068,14 +1078,14 @@ function renderQuestionFooter(task, footer) {
try { question = JSON.parse(task.question); } catch {}
}
- const questionEl = document.createElement('p');
+ const questionEl = doc.createElement('p');
questionEl.className = 'task-question-text';
questionEl.textContent = question.text;
footer.appendChild(questionEl);
if (question.options && question.options.length > 0) {
question.options.forEach(opt => {
- const btn = document.createElement('button');
+ const btn = doc.createElement('button');
btn.className = 'btn-answer';
btn.textContent = opt;
btn.addEventListener('click', (e) => {
@@ -1085,13 +1095,13 @@ function renderQuestionFooter(task, footer) {
footer.appendChild(btn);
});
} else {
- const row = document.createElement('div');
+ const row = doc.createElement('div');
row.className = 'task-answer-row';
- const input = document.createElement('input');
+ const input = doc.createElement('input');
input.type = 'text';
input.className = 'task-answer-input';
input.placeholder = 'Your answer…';
- const btn = document.createElement('button');
+ const btn = doc.createElement('button');
btn.className = 'btn-answer';
btn.textContent = 'Submit';
btn.addEventListener('click', (e) => {
@@ -1115,9 +1125,9 @@ const STATE_EMOJI = {
READY: '👀', BLOCKED: '⏸',
};
-async function renderSubtaskRollup(task, footer) {
+async function renderSubtaskRollup(task, footer, doc = document) {
footer.addEventListener('click', (e) => e.stopPropagation());
- const container = document.createElement('div');
+ const container = doc.createElement('div');
container.className = 'subtask-rollup';
footer.prepend(container);
@@ -1129,10 +1139,10 @@ async function renderSubtaskRollup(task, footer) {
container.textContent = blurb ? truncateToWordBoundary(blurb) : 'Waiting for subtasks…';
return;
}
- const ul = document.createElement('ul');
+ const ul = doc.createElement('ul');
ul.className = 'subtask-list';
for (const st of subtasks) {
- const li = document.createElement('li');
+ const li = doc.createElement('li');
li.className = `subtask-item subtask-${st.state.toLowerCase()}`;
li.textContent = `${STATE_EMOJI[st.state] || '•'} ${st.name}`;
ul.appendChild(li);