From 54f65f98617314b52ace2c1bd60f3a37469cd185 Mon Sep 17 00:00:00 2001 From: Lap Tran Date: Thu, 25 Jun 2026 11:01:47 -0400 Subject: [PATCH] Add phase approval endpoint and button to dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- automaton/dashboard/html/dashboard.js | 25 +++++++++++++++++++++++- automaton/dashboard/html/styles.css | 2 ++ automaton/dashboard/ui/app.py | 28 +++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/automaton/dashboard/html/dashboard.js b/automaton/dashboard/html/dashboard.js index 9cd7fb8..e4f51dd 100644 --- a/automaton/dashboard/html/dashboard.js +++ b/automaton/dashboard/html/dashboard.js @@ -223,8 +223,11 @@ function renderDetail(task) { reviewActionsHtml = '' + ''; } + const approvePhaseBtn = task.is_approval_gated + ? `` + : ''; content.innerHTML = ` -

Status

${statusText}${phaseGroupHtml}${task.is_edit_phase ? '✏️ Edit Allowed' : ''}${task.is_approval_gated ? '🔒 Requires Approval' : ''}
+

Status

${statusText}${phaseGroupHtml}${task.is_edit_phase ? '✏️ Edit Allowed' : ''}${task.is_approval_gated ? '🔒 Requires Approval' : ''}${approvePhaseBtn}
${statusReason ? `
${escapeHtml(statusReason)}
` : ''} ${task.blocker ? `

⚠️ What's Blocking

${escapeHtml(task.blocker)}

` : ''} ${task.phase_guidance ? `

▶ What's Next

${escapeHtml(task.phase_guidance)}
` : ''} @@ -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() { let filtered = [...state.tasks]; if (state.filterPhase !== 'all') filtered = filtered.filter(t => t.state === state.filterPhase); @@ -632,6 +650,11 @@ function setupUI() { 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; + if (taskName) approvePhase(taskName); + } }); } diff --git a/automaton/dashboard/html/styles.css b/automaton/dashboard/html/styles.css index 68e8f4e..c21e4c6 100644 --- a/automaton/dashboard/html/styles.css +++ b/automaton/dashboard/html/styles.css @@ -315,6 +315,8 @@ kbd { .review-btn.approve:hover { background: var(--success-bg); } .review-btn.changes { border-color: var(--error); color: var(--error); } .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-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); } diff --git a/automaton/dashboard/ui/app.py b/automaton/dashboard/ui/app.py index 2ddfa91..114ef6b 100644 --- a/automaton/dashboard/ui/app.py +++ b/automaton/dashboard/ui/app.py @@ -117,6 +117,9 @@ class DashboardHandler(SimpleHTTPRequestHandler): self._send_error(413, "Payload too large") return 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: self._send_error(404, "Not found") @@ -388,6 +391,31 @@ class DashboardHandler(SimpleHTTPRequestHandler): except json.JSONDecodeError: 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): project_root = self.project_root if not project_root: