Add phase approval endpoint and button to dashboard
CI / build (push) Has been cancelled

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:
Lap Tran
2026-06-25 11:01:47 -04:00
parent 7a5aa1f3a4
commit 54f65f9861
3 changed files with 54 additions and 1 deletions
+24 -1
View File
@@ -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);
}
}); });
} }
+2
View File
@@ -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); }
+28
View File
@@ -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: