The dashboard had no way to run status.py --approve. The existing
'Approve' button only wrote to REVIEW.md (code review status), not
approve the task's phase gate.
Changes:
- /api/approve/{task_name} POST endpoint in app.py calls status.py --approve
- '🔓 Approve Phase' button in detail panel for approval-gated tasks
- approvePhase() JS function sends the POST and refreshes the board
- CSS for the new button (warning-yellow styling)
This enables the full automation flow: user clicks Approve Phase →
autopilot picks up the approved task on next tick → drives it forward.
This commit is contained in:
@@ -223,8 +223,11 @@ function renderDetail(task) {
|
|||||||
reviewActionsHtml = '<button class="review-btn approve" data-task="' + escapeHtml(task.name) + '" data-status="approved">✅ Approve</button>' +
|
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>';
|
'<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>`
|
||||||
|
: '';
|
||||||
content.innerHTML = `
|
content.innerHTML = `
|
||||||
<div class="detail-section"><h4>Status</h4><span class="detail-status-badge ${statusClass}">${statusText}</span>${phaseGroupHtml}${task.is_edit_phase ? '<span class="detail-edit-badge">✏️ Edit Allowed</span>' : ''}${task.is_approval_gated ? '<span class="detail-approval-badge">🔒 Requires Approval</span>' : ''}</div>
|
<div class="detail-section"><h4>Status</h4><span class="detail-status-badge ${statusClass}">${statusText}</span>${phaseGroupHtml}${task.is_edit_phase ? '<span class="detail-edit-badge">✏️ Edit Allowed</span>' : ''}${task.is_approval_gated ? '<span class="detail-approval-badge">🔒 Requires Approval</span>' : ''}${approvePhaseBtn}</div>
|
||||||
${statusReason ? `<div class="detail-section detail-status-reason">${escapeHtml(statusReason)}</div>` : ''}
|
${statusReason ? `<div class="detail-section detail-status-reason">${escapeHtml(statusReason)}</div>` : ''}
|
||||||
${task.blocker ? `<div class="detail-section detail-blocker"><h4>⚠️ What's Blocking</h4><p class="detail-blocker-text">${escapeHtml(task.blocker)}</p></div>` : ''}
|
${task.blocker ? `<div class="detail-section detail-blocker"><h4>⚠️ What's Blocking</h4><p class="detail-blocker-text">${escapeHtml(task.blocker)}</p></div>` : ''}
|
||||||
${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.phase_guidance ? `<div class="detail-section detail-guidance"><h4>▶ What's Next</h4><pre class="detail-guidance-text">${escapeHtml(task.phase_guidance)}</pre></div>` : ''}
|
||||||
@@ -538,6 +541,21 @@ async function submitReview(taskName, status) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function approvePhase(taskName) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/approve/${taskName}`, { method: 'POST' });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
closeDetail();
|
||||||
|
await refreshData();
|
||||||
|
} else {
|
||||||
|
console.error('Phase approval failed:', data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Phase approval failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getFilteredTasks() {
|
function getFilteredTasks() {
|
||||||
let filtered = [...state.tasks];
|
let filtered = [...state.tasks];
|
||||||
if (state.filterPhase !== 'all') filtered = filtered.filter(t => t.state === state.filterPhase);
|
if (state.filterPhase !== 'all') filtered = filtered.filter(t => t.state === state.filterPhase);
|
||||||
@@ -632,6 +650,11 @@ function setupUI() {
|
|||||||
const status = btn.dataset.status;
|
const status = btn.dataset.status;
|
||||||
if (taskName && status) submitReview(taskName, status);
|
if (taskName && status) submitReview(taskName, status);
|
||||||
}
|
}
|
||||||
|
const approveBtn = e.target.closest('.approve-phase-btn');
|
||||||
|
if (approveBtn) {
|
||||||
|
const taskName = approveBtn.dataset.task;
|
||||||
|
if (taskName) approvePhase(taskName);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -315,6 +315,8 @@ kbd {
|
|||||||
.review-btn.approve:hover { background: var(--success-bg); }
|
.review-btn.approve:hover { background: var(--success-bg); }
|
||||||
.review-btn.changes { border-color: var(--error); color: var(--error); }
|
.review-btn.changes { border-color: var(--error); color: var(--error); }
|
||||||
.review-btn.changes:hover { background: var(--error-bg); }
|
.review-btn.changes:hover { background: var(--error-bg); }
|
||||||
|
.approve-phase-btn { display: inline-block; margin-left: 8px; padding: 4px 12px; border: 1px solid var(--warning); border-radius: var(--radius-sm); cursor: pointer; font-size: 11px; font-weight: 500; background: var(--warning-bg); color: var(--warning); font-family: inherit; transition: all 0.15s; }
|
||||||
|
.approve-phase-btn:hover { filter: brightness(1.1); }
|
||||||
.review-comment { font-size: 12px; color: var(--text-secondary); padding: 10px 12px; background: var(--bg-primary); border-radius: var(--radius-sm); margin-top: 6px; border-left: 2px solid var(--border-color); }
|
.review-comment { font-size: 12px; color: var(--text-secondary); padding: 10px 12px; background: var(--bg-primary); border-radius: var(--radius-sm); margin-top: 6px; border-left: 2px solid var(--border-color); }
|
||||||
.review-textarea { width: 100%; margin-top: 8px; padding: 10px; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 12px; font-family: inherit; resize: vertical; }
|
.review-textarea { width: 100%; margin-top: 8px; padding: 10px; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 12px; font-family: inherit; resize: vertical; }
|
||||||
.review-textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px rgba(138,180,248,0.2); }
|
.review-textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px rgba(138,180,248,0.2); }
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ class DashboardHandler(SimpleHTTPRequestHandler):
|
|||||||
self._send_error(413, "Payload too large")
|
self._send_error(413, "Payload too large")
|
||||||
return
|
return
|
||||||
self._handle_review(task_name)
|
self._handle_review(task_name)
|
||||||
|
elif self.path.startswith("/api/approve/"):
|
||||||
|
task_name = unquote(self.path.split("/api/approve/")[1])
|
||||||
|
self._handle_phase_approval(task_name)
|
||||||
else:
|
else:
|
||||||
self._send_error(404, "Not found")
|
self._send_error(404, "Not found")
|
||||||
|
|
||||||
@@ -388,6 +391,31 @@ class DashboardHandler(SimpleHTTPRequestHandler):
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
self._send_error(400, "Invalid JSON")
|
self._send_error(400, "Invalid JSON")
|
||||||
|
|
||||||
|
def _handle_phase_approval(self, task_name: str):
|
||||||
|
"""Approve a task's current phase (--approve) via status.py."""
|
||||||
|
if not self._validate_task_name(task_name):
|
||||||
|
self._send_error(400, "Invalid task name")
|
||||||
|
return
|
||||||
|
project_root = self.project_root
|
||||||
|
if not project_root:
|
||||||
|
self._send_error(503, "Not in automaton project")
|
||||||
|
return
|
||||||
|
status_py = Path.home() / ".automaton" / "scripts" / "status.py"
|
||||||
|
if not status_py.exists():
|
||||||
|
self._send_error(500, "status.py not found")
|
||||||
|
return
|
||||||
|
import subprocess
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, str(status_py), "--approve", "--task", task_name,
|
||||||
|
"--project", str(project_root)],
|
||||||
|
capture_output=True, text=True, timeout=30,
|
||||||
|
)
|
||||||
|
_invalidate_task_cache()
|
||||||
|
if result.returncode == 0:
|
||||||
|
self._send_json({"success": True, "message": result.stdout.strip()})
|
||||||
|
else:
|
||||||
|
self._send_error(400, result.stderr.strip() or result.stdout.strip())
|
||||||
|
|
||||||
def _serve_review_summary(self):
|
def _serve_review_summary(self):
|
||||||
project_root = self.project_root
|
project_root = self.project_root
|
||||||
if not project_root:
|
if not project_root:
|
||||||
|
|||||||
Reference in New Issue
Block a user