diff --git a/dashboard/api.py b/dashboard/api.py index 6f0241b..37abb86 100644 --- a/dashboard/api.py +++ b/dashboard/api.py @@ -82,6 +82,8 @@ class TaskBoardHandler(SimpleHTTPRequestHandler): self.handle_eco_stop() elif self.path == '/api/eco/sessions/clear': self.handle_eco_sessions_clear() + elif self.path == '/api/eco/git-commit': + self.handle_eco_git_commit() else: self.send_error(404) @@ -311,6 +313,8 @@ class TaskBoardHandler(SimpleHTTPRequestHandler): self.handle_eco_logs() elif self.path == '/api/eco/doctor': self.handle_eco_doctor() + elif self.path == '/api/eco/git' or self.path.startswith('/api/eco/git?'): + self.handle_eco_git_status() elif self.path.startswith('/api/'): self.send_error(404) else: @@ -2309,6 +2313,96 @@ class TaskBoardHandler(SimpleHTTPRequestHandler): self.send_json({'checks': checks}) + def handle_eco_git_status(self): + """Get git status for echo-core repo.""" + try: + workspace = ECHO_CORE_DIR + + branch = subprocess.run( + ['git', 'branch', '--show-current'], + cwd=workspace, capture_output=True, text=True, timeout=5 + ).stdout.strip() + + last_commit = subprocess.run( + ['git', 'log', '-1', '--format=%h|%s|%cr'], + cwd=workspace, capture_output=True, text=True, timeout=5 + ).stdout.strip() + + commit_parts = last_commit.split('|') if last_commit else ['', '', ''] + + status_output = subprocess.run( + ['git', 'status', '--short'], + cwd=workspace, capture_output=True, text=True, timeout=5 + ).stdout.strip() + + uncommitted = [f for f in status_output.split('\n') if f.strip()] if status_output else [] + + uncommitted_parsed = [] + for line in uncommitted: + if len(line) >= 2: + status = line[:2].strip() + filepath = line[2:].strip() + if filepath: + uncommitted_parsed.append({'status': status, 'path': filepath}) + + self.send_json({ + 'branch': branch, + 'clean': len(uncommitted) == 0, + 'uncommittedCount': len(uncommitted), + 'uncommittedParsed': uncommitted_parsed, + 'lastCommit': { + 'hash': commit_parts[0] if len(commit_parts) > 0 else '', + 'message': commit_parts[1] if len(commit_parts) > 1 else '', + 'time': commit_parts[2] if len(commit_parts) > 2 else '', + }, + }) + except Exception as e: + self.send_json({'error': str(e)}, 500) + + def handle_eco_git_commit(self): + """Run git add, commit, and push for echo-core repo.""" + try: + workspace = ECHO_CORE_DIR + + # Stage all changes + subprocess.run( + ['git', 'add', '-A'], + cwd=workspace, capture_output=True, text=True, timeout=10 + ) + + # Check if there's anything to commit + status = subprocess.run( + ['git', 'status', '--porcelain'], + cwd=workspace, capture_output=True, text=True, timeout=5 + ).stdout.strip() + + if not status: + self.send_json({'success': True, 'files': 0, 'output': 'Nothing to commit'}) + return + + files_count = len([l for l in status.split('\n') if l.strip()]) + + # Commit + commit_result = subprocess.run( + ['git', 'commit', '-m', 'chore: auto-commit from dashboard'], + cwd=workspace, capture_output=True, text=True, timeout=30 + ) + + # Push + push_result = subprocess.run( + ['git', 'push'], + cwd=workspace, capture_output=True, text=True, timeout=30 + ) + + output = commit_result.stdout + commit_result.stderr + push_result.stdout + push_result.stderr + + if commit_result.returncode == 0: + self.send_json({'success': True, 'files': files_count, 'output': output}) + else: + self.send_json({'success': False, 'error': output or 'Commit failed'}) + except Exception as e: + self.send_json({'success': False, 'error': str(e)}, 500) + def handle_eco_sessions_clear(self): """Clear active sessions (all or specific channel).""" try: diff --git a/dashboard/eco.html b/dashboard/eco.html index 4762693..3485e0f 100644 --- a/dashboard/eco.html +++ b/dashboard/eco.html @@ -454,6 +454,100 @@ font-size: var(--text-xs); } + /* Git status */ + .git-card { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; + } + + .git-header { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-4); + cursor: pointer; + user-select: none; + } + + .git-header:hover { background: var(--bg-elevated); } + + .git-icon { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); + background: rgba(249, 115, 22, 0.15); + color: #f97316; + flex-shrink: 0; + } + + .git-icon svg { width: 18px; height: 18px; } + + .git-info { flex: 1; min-width: 0; } + + .git-title { + font-size: var(--text-sm); + font-weight: 600; + color: var(--text-primary); + display: flex; + align-items: center; + gap: var(--space-2); + } + + .git-subtitle { + font-size: 13px; + color: var(--text-secondary); + margin-top: 2px; + } + + .git-badge { + padding: 3px 10px; + border-radius: var(--radius-sm); + font-size: 12px; + font-weight: 600; + } + + .git-badge.ok { background: rgba(34, 197, 94, 0.15); color: #22c55e; } + .git-badge.warning { background: rgba(249, 115, 22, 0.15); color: #f97316; } + .git-badge.error { background: rgba(239, 68, 68, 0.15); color: #ef4444; } + + .git-actions { + display: flex; + gap: var(--space-2); + margin-left: auto; + } + + .git-toggle { + width: 16px; + height: 16px; + color: var(--text-muted); + transition: transform var(--transition-fast); + } + + .git-card.collapsed .git-toggle { transform: rotate(-90deg); } + .git-card.collapsed .git-details { display: none; } + + .git-details { + padding: 0 var(--space-4) var(--space-3); + padding-left: calc(var(--space-4) + 32px + var(--space-3)); + } + + .git-detail-item { + display: flex; + align-items: center; + gap: var(--space-2); + font-size: 13px; + color: var(--text-primary); + padding: 2px 0; + } + + .git-detail-item svg { width: 14px; height: 14px; color: var(--text-secondary); } + .git-detail-item.uncommitted { color: #f97316; } + /* Spinner */ .spinner { display: inline-block; @@ -573,6 +667,45 @@ + +