|
|
|
@@ -1,7 +1,7 @@
|
|
|
|
|
const state = {
|
|
|
|
|
scope: 'none', currentView: 'board', theme: 'default', selectedTask: null,
|
|
|
|
|
tasks: [], refreshCount: 0, autoRefresh: true, showWaves: true,
|
|
|
|
|
filterPhase: 'all', filterReview: 'all', filterWave: 'all', searchQuery: '', filterVisible: false,
|
|
|
|
|
filterPhase: 'all', filterWave: 'all', searchQuery: '', filterVisible: false,
|
|
|
|
|
refreshInterval: null, projectName: null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@@ -48,14 +48,8 @@ function getPhaseGroupForState(state) {
|
|
|
|
|
return group ? group.id : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get display group for a task, accounting for review status.
|
|
|
|
|
// Approved planning tasks advance to Design; rejected ones go to Blocked.
|
|
|
|
|
// Get display group for a task based on its phase state.
|
|
|
|
|
function getTaskDisplayGroup(task) {
|
|
|
|
|
const review = task.review ? task.review.status : 'pending';
|
|
|
|
|
if ((task.state === 'research' || task.state === 'decomposition' || task.state === 'backlog')) {
|
|
|
|
|
if (review === 'approved') return 'design';
|
|
|
|
|
if (review === 'changes_requested') return 'blocked';
|
|
|
|
|
}
|
|
|
|
|
return getPhaseGroupForState(task.state);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -89,8 +83,7 @@ function renderHeader() {
|
|
|
|
|
wipTasks.textContent = filtered.filter(t => wipStates.includes(t.state)).length;
|
|
|
|
|
doneTasks.textContent = filtered.filter(t => t.state === 'done').length;
|
|
|
|
|
blockedTasks.textContent = filtered.filter(t => t.state === 'blocked').length;
|
|
|
|
|
const pendingReview = document.getElementById('pending-review');
|
|
|
|
|
if (pendingReview) pendingReview.textContent = filtered.filter(t => (t.state !== 'done' && t.state !== 'blocked') && (!t.review || t.review.status === 'pending')).length;
|
|
|
|
|
|
|
|
|
|
// Project name display
|
|
|
|
|
const projectName = state.projectName;
|
|
|
|
|
if (projectName) {
|
|
|
|
@@ -163,10 +156,6 @@ function renderTaskCard(task) {
|
|
|
|
|
const statusClass = task.state === 'done' ? 'done' : task.state === 'blocked' ? 'blocked' : 'in_progress';
|
|
|
|
|
const statusIcon = task.state === 'done' ? '✅' : task.state === 'blocked' ? '❌' : '🔄';
|
|
|
|
|
const subLabel = getSubLabel(task.state);
|
|
|
|
|
const reviewStatus = task.review ? task.review.status : 'pending';
|
|
|
|
|
const reviewBadge = reviewStatus === 'approved' ? '<span class="review-badge approved" title="Approved">✅</span>'
|
|
|
|
|
: reviewStatus === 'changes_requested' ? '<span class="review-badge changes" title="Changes requested">❌</span>'
|
|
|
|
|
: '<span class="review-badge pending" title="Pending review">🟡</span>';
|
|
|
|
|
const progressHtml = task.sub_tasks.length > 0
|
|
|
|
|
? `<span class="subtask-progress">${task.sub_tasks.filter(st => st.has_verdict && st.verdict_status === 'PASS').length}/${task.sub_tasks.length}</span>`
|
|
|
|
|
: '';
|
|
|
|
@@ -177,14 +166,12 @@ function renderTaskCard(task) {
|
|
|
|
|
return `<div class="subtask-item"><span class="subtask-status ${stStatus}">${stIcon}</span><span>${st.name}</span></div>`;
|
|
|
|
|
}).join('')}</div>`
|
|
|
|
|
: '';
|
|
|
|
|
const artifactsHtml = (reviewStatus === 'pending' || reviewStatus === 'changes_requested')
|
|
|
|
|
? `<div class="task-card-artifacts">${COLUMNS.filter(col => task.artifacts[col.id]).map(col => {
|
|
|
|
|
const artifactsHtml = `<div class="task-card-artifacts">${COLUMNS.filter(col => task.artifacts[col.id]).map(col => {
|
|
|
|
|
const label = ARTIFACT_LABELS[col.id] || col.label;
|
|
|
|
|
return `<span class="artifact-badge" title="${col.label}">${label}</span>`;
|
|
|
|
|
}).join('')}</div>`
|
|
|
|
|
: '';
|
|
|
|
|
}).join('')}</div>`;
|
|
|
|
|
return `<div class="task-card" data-task="${task.name}" data-status="${statusClass}">
|
|
|
|
|
<div class="task-card-header"><span class="task-card-name">${escapeHtml(task.display_name)}</span><span style="display:flex;align-items:center;gap:4px">${reviewBadge}<span class="task-card-status ${statusClass}">${statusIcon}</span></span></div>
|
|
|
|
|
<div class="task-card-header"><span class="task-card-name">${escapeHtml(task.display_name)}</span><span class="task-card-status ${statusClass}">${statusIcon}</span></div>
|
|
|
|
|
<div class="task-card-sublabel">${subLabel}</div>
|
|
|
|
|
${task.status_reason ? `<div class="task-card-reason">${escapeHtml(task.status_reason)}</div>` : ''}
|
|
|
|
|
${artifactsHtml}
|
|
|
|
@@ -211,18 +198,6 @@ function renderDetail(task) {
|
|
|
|
|
return `<span class="detail-artifact"><span class="${cls}">${icon}</span>${col.label}</span>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
const phaseGroupHtml = phaseGroup ? `<span class="detail-phase-badge" style="background: ${phaseGroupColor}20; color: ${phaseGroupColor}">${PHASE_GROUPS.find(g => g.id === phaseGroup).label}</span>` : '';
|
|
|
|
|
const reviewStatus = task.review ? task.review.status : 'pending';
|
|
|
|
|
const reviewStatusText = reviewStatus === 'approved' ? '✅ Approved' : reviewStatus === 'changes_requested' ? '❌ Changes Requested' : '🟡 Pending Review';
|
|
|
|
|
const reviewComment = task.review && task.review.comment ? `<p class="review-comment">${escapeHtml(task.review.comment)}</p>` : '';
|
|
|
|
|
let reviewActionsHtml;
|
|
|
|
|
if (reviewStatus === 'approved') {
|
|
|
|
|
reviewActionsHtml = '<button class="review-btn revoke" data-task="' + escapeHtml(task.name) + '" data-status="pending">↩ Revoke Approval</button>';
|
|
|
|
|
} else if (reviewStatus === 'changes_requested') {
|
|
|
|
|
reviewActionsHtml = '<button class="review-btn revoke" data-task="' + escapeHtml(task.name) + '" data-status="pending">↩ Revoke Changes</button>';
|
|
|
|
|
} else {
|
|
|
|
|
reviewActionsHtml = '<button class="review-btn approve" data-task="' + escapeHtml(task.name) + '" data-status="approved">✅ Approve</button>' +
|
|
|
|
|
'<button class="review-btn changes" data-task="' + escapeHtml(task.name) + '" data-status="changes_requested">❌ Request Changes</button>';
|
|
|
|
|
}
|
|
|
|
|
const approvePhaseBtn = task.is_approval_gated
|
|
|
|
|
? `<button class="approve-phase-btn" data-task="${escapeHtml(task.name)}">🔓 Approve Phase</button>`
|
|
|
|
|
: '';
|
|
|
|
@@ -233,14 +208,6 @@ function renderDetail(task) {
|
|
|
|
|
${task.phase_guidance ? `<div class="detail-section detail-guidance"><h4>▶ What's Next</h4><pre class="detail-guidance-text">${escapeHtml(task.phase_guidance)}</pre></div>` : ''}
|
|
|
|
|
${task.state === 'blocked' && task.blocked_action_items && task.blocked_action_items.length > 0 ? `<div class="detail-section detail-action-items"><h4>📋 Action Items</h4><ul class="detail-action-list">${task.blocked_action_items.map(item => `<li>${escapeHtml(item)}</li>`).join('')}</ul></div>` : ''}
|
|
|
|
|
<div class="detail-section"><h4>Artifacts</h4><div class="detail-artifacts">${artifactsHtml}</div></div>
|
|
|
|
|
<div class="detail-section"><h4>Review</h4>
|
|
|
|
|
<span class="review-badge ${reviewStatus}">${reviewStatusText}</span>
|
|
|
|
|
${reviewComment}
|
|
|
|
|
<textarea class="review-textarea" id="review-comment-${task.name}" placeholder="Optional comment..." rows="2"></textarea>
|
|
|
|
|
<div class="review-actions">
|
|
|
|
|
${reviewActionsHtml}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
${task.sub_tasks.length > 0 ? `<div class="detail-section"><h4>Sub-tasks (${task.sub_tasks.filter(st => st.has_verdict).length}/${task.sub_tasks.length})</h4>
|
|
|
|
|
<ul class="detail-subtask-list">${task.sub_tasks.map(st => {
|
|
|
|
|
const stStatus = st.has_verdict && st.verdict_status === 'PASS' ? 'pass' : st.has_verdict && st.verdict_status === 'FAIL' ? 'fail' : 'incomplete';
|
|
|
|
@@ -522,25 +489,6 @@ async function renderAgentTab() {
|
|
|
|
|
_startAgentPoller();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function submitReview(taskName, status) {
|
|
|
|
|
const textarea = document.getElementById(`review-comment-${taskName}`);
|
|
|
|
|
const comment = textarea ? textarea.value : '';
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/task/${taskName}/review`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ status, comment }),
|
|
|
|
|
});
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
if (data.success) {
|
|
|
|
|
closeDetail();
|
|
|
|
|
await refreshData();
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Review submission failed:', err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function approvePhase(taskName) {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/approve/${taskName}`, { method: 'POST' });
|
|
|
|
@@ -559,9 +507,6 @@ async function approvePhase(taskName) {
|
|
|
|
|
function getFilteredTasks() {
|
|
|
|
|
let filtered = [...state.tasks];
|
|
|
|
|
if (state.filterPhase !== 'all') filtered = filtered.filter(t => t.state === state.filterPhase);
|
|
|
|
|
if (state.filterReview === 'pending') filtered = filtered.filter(t => !t.review || t.review.status === 'pending');
|
|
|
|
|
else if (state.filterReview === 'approved') filtered = filtered.filter(t => t.review && t.review.status === 'approved');
|
|
|
|
|
else if (state.filterReview === 'changes_requested') filtered = filtered.filter(t => t.review && t.review.status === 'changes_requested');
|
|
|
|
|
if (state.filterWave === 'has-waves') filtered = filtered.filter(t => t.sub_tasks.length > 0);
|
|
|
|
|
else if (state.filterWave === 'no-waves') filtered = filtered.filter(t => t.sub_tasks.length === 0);
|
|
|
|
|
if (state.searchQuery) {
|
|
|
|
@@ -640,16 +585,9 @@ function setupUI() {
|
|
|
|
|
document.getElementById('btn-close-detail').addEventListener('click', closeDetail);
|
|
|
|
|
document.getElementById('detail-overlay').addEventListener('click', (e) => { if (e.target === e.currentTarget) closeDetail(); });
|
|
|
|
|
document.getElementById('filter-phase').addEventListener('change', (e) => { state.filterPhase = e.target.value; renderCurrentView(); });
|
|
|
|
|
document.getElementById('filter-review').addEventListener('change', (e) => { state.filterReview = e.target.value; renderCurrentView(); });
|
|
|
|
|
document.getElementById('filter-wave').addEventListener('change', (e) => { state.filterWave = e.target.value; renderCurrentView(); });
|
|
|
|
|
document.getElementById('search-input').addEventListener('input', (e) => { state.searchQuery = e.target.value; renderCurrentView(); });
|
|
|
|
|
document.addEventListener('click', (e) => {
|
|
|
|
|
const btn = e.target.closest('.review-btn');
|
|
|
|
|
if (btn) {
|
|
|
|
|
const taskName = btn.dataset.task;
|
|
|
|
|
const status = btn.dataset.status;
|
|
|
|
|
if (taskName && status) submitReview(taskName, status);
|
|
|
|
|
}
|
|
|
|
|
const approveBtn = e.target.closest('.approve-phase-btn');
|
|
|
|
|
if (approveBtn) {
|
|
|
|
|
const taskName = approveBtn.dataset.task;
|
|
|
|
|