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: