summaryrefslogtreecommitdiff
path: root/web/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'web/app.js')
-rw-r--r--web/app.js64
1 files changed, 41 insertions, 23 deletions
diff --git a/web/app.js b/web/app.js
index c192d20..5444511 100644
--- a/web/app.js
+++ b/web/app.js
@@ -275,42 +275,49 @@ function truncateToWordBoundary(text, maxLen = 120) {
return (cut > 0 ? text.slice(0, cut) : text.slice(0, maxLen)) + '…';
}
-function createTaskCard(task) {
- const card = document.createElement('div');
+export function createTaskCard(task, doc = document) {
+ const card = doc.createElement('div');
card.className = 'task-card';
card.dataset.taskId = task.id;
// Header: name + state badge
- const header = document.createElement('div');
+ const header = doc.createElement('div');
header.className = 'task-card-header';
- const name = document.createElement('span');
+ const name = doc.createElement('span');
name.className = 'task-name';
name.textContent = task.name;
- const badge = document.createElement('span');
+ const badge = doc.createElement('span');
badge.className = 'state-badge';
badge.dataset.state = task.state;
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
- const meta = document.createElement('div');
+ const meta = doc.createElement('div');
meta.className = 'task-meta';
if (task.priority) {
- const prio = document.createElement('span');
+ const prio = doc.createElement('span');
prio.textContent = task.priority;
meta.appendChild(prio);
}
if (task.created_at) {
- const when = document.createElement('span');
+ const when = doc.createElement('span');
when.textContent = formatDate(task.created_at);
meta.appendChild(when);
}
if (task.project) {
- const proj = document.createElement('span');
+ const proj = doc.createElement('span');
proj.className = 'task-project';
proj.textContent = task.project;
meta.appendChild(proj);
@@ -319,7 +326,7 @@ function createTaskCard(task) {
// Description (truncated via CSS)
if (task.description) {
- const desc = document.createElement('div');
+ const desc = doc.createElement('div');
desc.className = 'task-description';
desc.textContent = task.description;
card.appendChild(desc);
@@ -328,7 +335,7 @@ function createTaskCard(task) {
// Error message for failed tasks
const FAILED_STATES = new Set(['FAILED', 'BUDGET_EXCEEDED', 'TIMED_OUT']);
if (FAILED_STATES.has(task.state) && task.error_msg) {
- const errEl = document.createElement('div');
+ const errEl = doc.createElement('div');
errEl.className = 'task-error-msg';
errEl.textContent = task.error_msg;
errEl.title = task.error_msg;
@@ -337,12 +344,12 @@ function createTaskCard(task) {
// Checker report for READY tasks where the checker flagged a problem.
if (task.state === 'READY' && task.checker_report) {
- const reportEl = document.createElement('div');
+ const reportEl = doc.createElement('div');
reportEl.className = 'task-checker-report';
- const label = document.createElement('span');
+ const label = doc.createElement('span');
label.className = 'task-checker-report-label';
label.textContent = '⚠ Checker flagged:';
- const text = document.createElement('span');
+ const text = doc.createElement('span');
text.textContent = task.checker_report;
reportEl.appendChild(label);
reportEl.appendChild(text);
@@ -363,17 +370,28 @@ function createTaskCard(task) {
if (depBadge) card.appendChild(depBadge);
}
+ // Log-tail placeholder for PENDING/QUEUED tasks (no execution exists yet).
+ if (task.state === 'PENDING' || task.state === 'QUEUED') {
+ 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);
+ card.appendChild(logTail);
+ }
+
// Footer: action buttons based on state
// Interrupted states (CANCELLED, FAILED, BUDGET_EXCEEDED) show both Resume and Restart.
// TIMED_OUT shows Resume only. Others show a single action.
const RESUME_STATES = new Set(['TIMED_OUT', 'CANCELLED', 'FAILED', 'BUDGET_EXCEEDED']);
const RESTART_STATES = new Set(['CANCELLED', 'FAILED', 'BUDGET_EXCEEDED']);
if (task.state === 'PENDING' || task.state === 'RUNNING' || task.state === 'READY' || task.state === 'BLOCKED' || RESUME_STATES.has(task.state)) {
- const footer = document.createElement('div');
+ const footer = doc.createElement('div');
footer.className = 'task-card-footer';
if (task.state === 'PENDING') {
- const btn = document.createElement('button');
+ const btn = doc.createElement('button');
btn.className = 'btn-run';
btn.textContent = 'Run';
btn.addEventListener('click', (e) => {
@@ -382,7 +400,7 @@ function createTaskCard(task) {
});
footer.appendChild(btn);
} else if (task.state === 'RUNNING') {
- const btn = document.createElement('button');
+ const btn = doc.createElement('button');
btn.className = 'btn-cancel';
btn.textContent = 'Cancel';
btn.addEventListener('click', (e) => {
@@ -392,14 +410,14 @@ function createTaskCard(task) {
footer.appendChild(btn);
} else if (task.state === 'READY') {
renderSubtaskRollup(task, footer);
- const acceptBtn = document.createElement('button');
+ const acceptBtn = doc.createElement('button');
acceptBtn.className = 'btn-accept';
acceptBtn.textContent = 'Accept';
acceptBtn.addEventListener('click', (e) => {
e.stopPropagation();
handleAccept(task.id, acceptBtn, footer);
});
- const rejectBtn = document.createElement('button');
+ const rejectBtn = doc.createElement('button');
rejectBtn.className = 'btn-reject';
rejectBtn.textContent = 'Reject';
rejectBtn.addEventListener('click', (e) => {
@@ -414,7 +432,7 @@ function createTaskCard(task) {
} else {
renderSubtaskRollup(task, footer);
}
- const cancelBtn = document.createElement('button');
+ const cancelBtn = doc.createElement('button');
cancelBtn.className = 'btn-cancel';
cancelBtn.textContent = 'Cancel';
cancelBtn.addEventListener('click', (e) => {
@@ -423,7 +441,7 @@ function createTaskCard(task) {
});
footer.appendChild(cancelBtn);
} else if (RESUME_STATES.has(task.state)) {
- const resumeBtn = document.createElement('button');
+ const resumeBtn = doc.createElement('button');
resumeBtn.className = 'btn-resume';
resumeBtn.textContent = 'Resume';
resumeBtn.addEventListener('click', (e) => {
@@ -432,7 +450,7 @@ function createTaskCard(task) {
});
footer.appendChild(resumeBtn);
if (RESTART_STATES.has(task.state)) {
- const restartBtn = document.createElement('button');
+ const restartBtn = doc.createElement('button');
restartBtn.className = 'btn-restart';
restartBtn.textContent = 'Restart';
restartBtn.addEventListener('click', (e) => {
@@ -447,7 +465,7 @@ function createTaskCard(task) {
}
if (!NON_DELETABLE_STATES.has(task.state)) {
- const delBtn = document.createElement('button');
+ const delBtn = doc.createElement('button');
delBtn.className = 'btn-delete-task';
delBtn.title = 'Delete task';
delBtn.textContent = '✕';