From e92284645cc830f32ce5cbb991b620a83632040e Mon Sep 17 00:00:00 2001 From: Echo Date: Mon, 9 Feb 2026 12:21:13 +0000 Subject: [PATCH] feat: Workspace page enhancements - git ops, Gitea auto-create, compact stories - Add workspace.html with project cards, Ralph status, git info - Backend: git diff/commit/push endpoints, project delete with confirmation - Push auto-creates Gitea repo (romfast org) when no remote configured - GITEA_TOKEN read from dashboard/.env file - Compact collapsible user stories (emoji row + expand on click) - Action buttons: Diff (with count badge), Commit, Push, README, Delete - Fix openPrd/openReadme to use hash navigation for files.html - Add .gitignore template to ralph.sh for new projects - Unify branches: merge main into master, delete main Co-Authored-By: Claude Opus 4.6 --- AGENTS.md | 124 +--- dashboard/api.py | 622 +++++++++++++++++++ dashboard/files.html | 4 + dashboard/index.html | 4 + dashboard/notes.html | 4 + dashboard/workspace.html | 1021 +++++++++++++++++++++++++++++++ memory/2026-02-09.md | 29 + memory/kb/index.json | 38 +- memory/kb/tehnici-pauza.md | 6 +- skills/ralph/templates/ralph.sh | 36 ++ 10 files changed, 1771 insertions(+), 117 deletions(-) create mode 100644 dashboard/workspace.html create mode 100644 memory/2026-02-09.md diff --git a/AGENTS.md b/AGENTS.md index fbfd20c..dacf9ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,118 +32,24 @@ When I receive errors, bugs, or new feature requests: 1. **Planning → Opus**: Architecture, strategy, design decisions 2. **Execution → Sonnet**: Implementation, coding, debugging, testing -## Proiecte/Features Workflow (MANDATORY) +## Proiecte/Features Workflow -**Scop:** Propun și creez programe/proiecte în cod care îl ajută pe Marius (80/20), inspirate din Discovery (YouTube, articole, bloguri). +**Scop:** Propun proiecte 80/20 in evening-report, implementez cu Ralph in night-execute. +**Tools:** tools/ralph_prd_generator.py, tools/ralph_workflow.py +**Workspace:** ~/workspace/ | **Tracking:** memory/approved-tasks.md +**Model strategy:** Opus (planning/PRD) → Sonnet (implementare Ralph) -### Criterii Propuneri (80/20 STRICT) -- Impact mare pentru Marius → apoi pentru clienți -- Proiecte de "joacă" pentru el mai întâi (să vadă cum îl ajută) -- Din ce îl interesează (USER.md) -- Inspirat din conținut procesat (memory/kb/youtube/, articole/, insights/) -- **NU orice** - doar cu valoare concretă +### Context Proiecte Prioritare +- **roa2web** (gitea.romfast.ro/romfast/roa2web) - rapoarte, interfata web, notificari ERP ROA + - Rapoarte ROA noi → feature in roa2web (NU proiect separat) +- **Chatbot Maria** (Flowise, LXC 104) - documentatie, raspunsuri clienti + - Imbunatatiri chatbot → documentatie + configurare Flowise +- **Proiecte independente** → ~/workspace/ cu Ralph autonom -### Workflow Complet - -**1. SEARA (20:00) - evening-report:** -- Propun 1-2 proiecte NOI (P1, P2) -- Propun 2-3 features pentru proiecte EXISTENTE (F1, F2, F3) -- Format: context, impact, efort, stack simplu -- Marius aprobă: "P pentru P1,P2" sau "F pentru F1,F3" - -**2. NOAPTE (23:00, 03:00) - night-execute:** - -**A. Planning cu OPUS (eu, Echo) - pe moltbot:** -```python -from tools.ralph_prd_generator import create_prd_and_json -from tools.ralph_workflow import run_ralph -import subprocess - -# Pentru fiecare proiect aprobat -prd_file, prd_json = create_prd_and_json( - project_name="project-name", # kebab-case - description="Descriere completă cu Features:\n- Feature 1\n- Feature 2", - workspace_dir=Path.home() / "workspace" -) - -# Git init + push -project_dir = prd_json.parent.parent.parent -subprocess.run(["git", "init"], cwd=project_dir) -subprocess.run(["git", "add", "."], cwd=project_dir) -subprocess.run(["git", "commit", "-m", "Initial commit with PRD"], cwd=project_dir) -subprocess.run(["git", "remote", "add", "origin", f"https://gitea.romfast.ro/romfast/PROJECT"], cwd=project_dir) -subprocess.run(["git", "push", "-u", "origin", "main"], cwd=project_dir) - -# Lansează Ralph -run_ralph(prd_json, max_iterations=20, background=True) - -# Marchează [x] în approved-tasks.md -``` - -**B. Implementare cu Ralph (Sonnet) - automat în background:** -- ralph.sh rulează autonom: - 1. Selectează story (priority minimă, passes=false) - 2. Rulează Claude Code (Sonnet) pentru implementare - 3. Quality checks: typecheck, lint, test - 4. Git commit dacă OK → passes: true - 5. Update progress.txt cu learnings - 6. Repetă până complete sau max 20 iterații -- Git push automat - -**3. DIMINEAȚĂ (08:30) - morning-report:** -```python -from tools.ralph_workflow import check_status -from pathlib import Path - -status = check_status(Path.home() / "workspace" / "PROJECT-NAME") -# Raportez: stories complete/incomplete, learnings, link gitea -``` - -- Status per story: ✅ complet / 🔄 în progres / ⚠️ blocat -- Learnings din progress.txt -- Link gitea: `https://gitea.romfast.ro/romfast/PROJECT-NAME` - -### Mașină Development - -**moltbot (LXC 110) - LOCAL:** -- User: moltbot -- Workspace: `~/workspace/` -- Claude Code: `~/.local/bin/claude` (v2.1.37) -- Ralph tools: - - `~/clawd/tools/ralph_prd_generator.py` - Generează PRD și prd.json (Python) - - `~/clawd/tools/ralph_workflow.py` - Wrapper (lansare Ralph) - - `~/clawd/skills/ralph/templates/` - ralph.sh, prompt.md (copiate în proiecte) -- Templates copiate automat în fiecare proiect - -### Model Strategy (OBLIGATORIU) -- **Opus** → Planning, PRD, stories (eu, Echo în night-execute) -- **Sonnet** → Coding, debugging, implementare (Ralph loop) - -### Structură Proiect -``` -/workspace/PROJECT-NAME/ -├── tasks/ -│ └── prd-PROJECT-NAME.md # PRD generat de /prd skill -├── scripts/ -│ └── ralph/ -│ ├── prd.json # Stories pentru Ralph -│ ├── progress.txt # Learnings per iterație -│ └── ralph.sh # Loop autonom -├── src/ # Cod implementat de Ralph -└── .git/ # Git repo → gitea -``` - -### Tracking -- `memory/approved-tasks.md` - include proiecte (P1, P2) și features (F1, F2) -- Secțiuni: "Noaptea asta" + "Nopțile următoare" -- Format: `[ ] P1 - Nume Proiect: descriere scurtă` - -### Exemple Domenii -- Automatizări pentru ROA (scripturile lui Marius) -- Unelte productivitate (task tracking, reminder-uri) -- Mini-tools pentru clienți (rapoarte, validări) -- Experimente NLP/coaching (exerciții interactive) -- Tracking sănătate (dureri, pauze respirație) +### Surse Inspiratie +- Intrebari frecvente clienti (validari ANAF D406/D394, facturare valuta, taxare inversa) +- Note din memory/kb/ (youtube, insights, articole) +- Probleme repetitive ale lui Marius ## Memory diff --git a/dashboard/api.py b/dashboard/api.py index dd99ff1..cd93951 100644 --- a/dashboard/api.py +++ b/dashboard/api.py @@ -5,10 +5,12 @@ Handles YouTube summarization requests. """ import json +import shutil import subprocess import sys import re import os +import signal from http.server import HTTPServer, SimpleHTTPRequestHandler from urllib.parse import parse_qs, urlparse from datetime import datetime @@ -18,6 +20,20 @@ BASE_DIR = Path(__file__).parent.parent TOOLS_DIR = BASE_DIR / 'tools' NOTES_DIR = BASE_DIR / 'kb' / 'youtube' KANBAN_DIR = BASE_DIR / 'dashboard' +WORKSPACE_DIR = Path('/home/moltbot/workspace') + +# Load .env file if present +_env_file = Path(__file__).parent / '.env' +if _env_file.exists(): + for line in _env_file.read_text().splitlines(): + line = line.strip() + if line and not line.startswith('#') and '=' in line: + k, v = line.split('=', 1) + os.environ.setdefault(k.strip(), v.strip()) + +GITEA_URL = os.environ.get('GITEA_URL', 'https://gitea.romfast.ro') +GITEA_ORG = os.environ.get('GITEA_ORG', 'romfast') +GITEA_TOKEN = os.environ.get('GITEA_TOKEN', '') class TaskBoardHandler(SimpleHTTPRequestHandler): @@ -32,6 +48,16 @@ class TaskBoardHandler(SimpleHTTPRequestHandler): self.handle_git_commit() elif self.path == '/api/pdf': self.handle_pdf_post() + elif self.path == '/api/workspace/run': + self.handle_workspace_run() + elif self.path == '/api/workspace/stop': + self.handle_workspace_stop() + elif self.path == '/api/workspace/git/commit': + self.handle_workspace_git_commit() + elif self.path == '/api/workspace/git/push': + self.handle_workspace_git_push() + elif self.path == '/api/workspace/delete': + self.handle_workspace_delete() else: self.send_error(404) @@ -229,6 +255,12 @@ class TaskBoardHandler(SimpleHTTPRequestHandler): self.handle_files_get() elif self.path.startswith('/api/diff'): self.handle_git_diff() + elif self.path == '/api/workspace' or self.path.startswith('/api/workspace?'): + self.handle_workspace_list() + elif self.path.startswith('/api/workspace/git/diff'): + self.handle_workspace_git_diff() + elif self.path.startswith('/api/workspace/logs'): + self.handle_workspace_logs() elif self.path.startswith('/api/'): self.send_error(404) else: @@ -726,6 +758,596 @@ class TaskBoardHandler(SimpleHTTPRequestHandler): else: self.send_json({'error': 'Unknown action'}, 400) + def handle_workspace_list(self): + """List projects in ~/workspace/ with Ralph status, git info, etc.""" + try: + projects = [] + if not WORKSPACE_DIR.exists(): + self.send_json({'projects': []}) + return + + for project_dir in sorted(WORKSPACE_DIR.iterdir()): + if not project_dir.is_dir() or project_dir.name.startswith('.'): + continue + + ralph_dir = project_dir / 'scripts' / 'ralph' + prd_json = ralph_dir / 'prd.json' + tasks_dir = project_dir / 'tasks' + + proj = { + 'name': project_dir.name, + 'path': str(project_dir), + 'hasRalph': ralph_dir.exists(), + 'hasPrd': any(tasks_dir.glob('prd-*.md')) if tasks_dir.exists() else False, + 'hasMain': (project_dir / 'main.py').exists(), + 'hasVenv': (project_dir / 'venv').exists(), + 'hasReadme': (project_dir / 'README.md').exists(), + 'ralph': None, + 'process': {'running': False, 'pid': None, 'port': None}, + 'git': None + } + + # Ralph status + if prd_json.exists(): + try: + prd = json.loads(prd_json.read_text()) + stories = prd.get('userStories', []) + complete = sum(1 for s in stories if s.get('passes')) + + # Check ralph PID + ralph_pid = None + ralph_running = False + pid_file = ralph_dir / '.ralph.pid' + if pid_file.exists(): + try: + pid = int(pid_file.read_text().strip()) + os.kill(pid, 0) # Check if alive + ralph_running = True + ralph_pid = pid + except (ValueError, ProcessLookupError, PermissionError): + pass + + # Last iteration time from logs + last_iter = None + logs_dir = ralph_dir / 'logs' + if logs_dir.exists(): + log_files = sorted(logs_dir.glob('iteration-*.log'), key=lambda f: f.stat().st_mtime, reverse=True) + if log_files: + mtime = log_files[0].stat().st_mtime + last_iter = datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M') + + tech = prd.get('techStack', {}) + proj['ralph'] = { + 'running': ralph_running, + 'pid': ralph_pid, + 'storiesTotal': len(stories), + 'storiesComplete': complete, + 'lastIteration': last_iter, + 'stories': [ + {'id': s.get('id', ''), 'title': s.get('title', ''), 'passes': s.get('passes', False)} + for s in stories + ] + } + proj['techStack'] = { + 'type': tech.get('type', ''), + 'commands': tech.get('commands', {}), + 'port': tech.get('port'), + } + except (json.JSONDecodeError, IOError): + pass + + # Check if main.py is running + if proj['hasMain']: + try: + result = subprocess.run( + ['pgrep', '-f', f'python.*{project_dir.name}/main.py'], + capture_output=True, text=True, timeout=3 + ) + if result.stdout.strip(): + pids = result.stdout.strip().split('\n') + port = None + if prd_json.exists(): + try: + prd_data = json.loads(prd_json.read_text()) + port = prd_data.get('techStack', {}).get('port') + except (json.JSONDecodeError, IOError): + pass + proj['process'] = { + 'running': True, + 'pid': int(pids[0]), + 'port': port + } + except Exception: + pass + + # Git info + if (project_dir / '.git').exists(): + try: + branch = subprocess.run( + ['git', 'branch', '--show-current'], + cwd=project_dir, capture_output=True, text=True, timeout=5 + ).stdout.strip() + + last_commit = subprocess.run( + ['git', 'log', '-1', '--format=%h - %s'], + cwd=project_dir, capture_output=True, text=True, timeout=5 + ).stdout.strip() + + status_out = subprocess.run( + ['git', 'status', '--short'], + cwd=project_dir, capture_output=True, text=True, timeout=5 + ).stdout.strip() + uncommitted = len([l for l in status_out.split('\n') if l.strip()]) if status_out else 0 + + proj['git'] = { + 'branch': branch, + 'lastCommit': last_commit, + 'uncommitted': uncommitted + } + except Exception: + pass + + projects.append(proj) + + self.send_json({'projects': projects}) + except Exception as e: + self.send_json({'error': str(e)}, 500) + + def _read_post_json(self): + """Helper to read JSON POST body.""" + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length).decode('utf-8') + return json.loads(post_data) + + def _validate_project(self, name): + """Validate project name and return its path, or None.""" + if not name or '/' in name or '..' in name: + return None + project_dir = WORKSPACE_DIR / name + if not project_dir.exists() or not project_dir.is_dir(): + return None + # Ensure it resolves within workspace + if not str(project_dir.resolve()).startswith(str(WORKSPACE_DIR)): + return None + return project_dir + + def handle_workspace_run(self): + """Start a project process (main.py, ralph.sh, or pytest).""" + try: + data = self._read_post_json() + project_name = data.get('project', '') + command = data.get('command', '') + + project_dir = self._validate_project(project_name) + if not project_dir: + self.send_json({'success': False, 'error': 'Invalid project'}, 400) + return + + allowed_commands = {'main', 'ralph', 'test'} + if command not in allowed_commands: + self.send_json({'success': False, 'error': f'Invalid command. Allowed: {", ".join(allowed_commands)}'}, 400) + return + + ralph_dir = project_dir / 'scripts' / 'ralph' + + if command == 'main': + main_py = project_dir / 'main.py' + if not main_py.exists(): + self.send_json({'success': False, 'error': 'No main.py found'}, 404) + return + + # Use venv python if available + venv_python = project_dir / 'venv' / 'bin' / 'python' + python_cmd = str(venv_python) if venv_python.exists() else sys.executable + + log_path = ralph_dir / 'logs' / 'main.log' if ralph_dir.exists() else project_dir / 'main.log' + log_path.parent.mkdir(parents=True, exist_ok=True) + + with open(log_path, 'a') as log_file: + proc = subprocess.Popen( + [python_cmd, 'main.py'], + cwd=str(project_dir), + stdout=log_file, + stderr=log_file, + start_new_session=True + ) + + self.send_json({'success': True, 'pid': proc.pid, 'log': str(log_path)}) + + elif command == 'ralph': + ralph_sh = ralph_dir / 'ralph.sh' + if not ralph_sh.exists(): + self.send_json({'success': False, 'error': 'No ralph.sh found'}, 404) + return + + log_path = ralph_dir / 'logs' / 'ralph.log' + log_path.parent.mkdir(parents=True, exist_ok=True) + + with open(log_path, 'a') as log_file: + proc = subprocess.Popen( + ['bash', str(ralph_sh)], + cwd=str(project_dir), + stdout=log_file, + stderr=log_file, + start_new_session=True + ) + + # Write PID + pid_file = ralph_dir / '.ralph.pid' + pid_file.write_text(str(proc.pid)) + + self.send_json({'success': True, 'pid': proc.pid, 'log': str(log_path)}) + + elif command == 'test': + # Run pytest synchronously (with timeout) + venv_python = project_dir / 'venv' / 'bin' / 'python' + python_cmd = str(venv_python) if venv_python.exists() else sys.executable + + result = subprocess.run( + [python_cmd, '-m', 'pytest', '-v', '--tb=short'], + cwd=str(project_dir), + capture_output=True, text=True, + timeout=120 + ) + + self.send_json({ + 'success': result.returncode == 0, + 'output': result.stdout + result.stderr, + 'returncode': result.returncode + }) + + except subprocess.TimeoutExpired: + self.send_json({'success': False, 'error': 'Test timeout (120s)'}, 500) + except Exception as e: + self.send_json({'success': False, 'error': str(e)}, 500) + + def handle_workspace_stop(self): + """Stop a project process.""" + try: + data = self._read_post_json() + project_name = data.get('project', '') + target = data.get('target', '') + + project_dir = self._validate_project(project_name) + if not project_dir: + self.send_json({'success': False, 'error': 'Invalid project'}, 400) + return + + if target not in ('main', 'ralph'): + self.send_json({'success': False, 'error': 'Invalid target. Use: main, ralph'}, 400) + return + + if target == 'ralph': + pid_file = project_dir / 'scripts' / 'ralph' / '.ralph.pid' + if pid_file.exists(): + try: + pid = int(pid_file.read_text().strip()) + # Verify the process belongs to our user and is within workspace + proc_cwd = Path(f'/proc/{pid}/cwd').resolve() + if str(proc_cwd).startswith(str(WORKSPACE_DIR)): + os.killpg(os.getpgid(pid), signal.SIGTERM) + self.send_json({'success': True, 'message': f'Ralph stopped (PID {pid})'}) + else: + self.send_json({'success': False, 'error': 'Process not in workspace'}, 403) + except ProcessLookupError: + self.send_json({'success': True, 'message': 'Process already stopped'}) + except PermissionError: + self.send_json({'success': False, 'error': 'Permission denied'}, 403) + else: + self.send_json({'success': False, 'error': 'No PID file found'}, 404) + + elif target == 'main': + # Find main.py process for this project + try: + result = subprocess.run( + ['pgrep', '-f', f'python.*{project_dir.name}/main.py'], + capture_output=True, text=True, timeout=3 + ) + if result.stdout.strip(): + pid = int(result.stdout.strip().split('\n')[0]) + proc_cwd = Path(f'/proc/{pid}/cwd').resolve() + if str(proc_cwd).startswith(str(WORKSPACE_DIR)): + os.kill(pid, signal.SIGTERM) + self.send_json({'success': True, 'message': f'Main stopped (PID {pid})'}) + else: + self.send_json({'success': False, 'error': 'Process not in workspace'}, 403) + else: + self.send_json({'success': True, 'message': 'No running process found'}) + except Exception as e: + self.send_json({'success': False, 'error': str(e)}, 500) + + except Exception as e: + self.send_json({'success': False, 'error': str(e)}, 500) + + def handle_workspace_git_diff(self): + """Get git diff for a workspace project.""" + try: + parsed = urlparse(self.path) + params = parse_qs(parsed.query) + project_name = params.get('project', [''])[0] + + project_dir = self._validate_project(project_name) + if not project_dir: + self.send_json({'error': 'Invalid project'}, 400) + return + + if not (project_dir / '.git').exists(): + self.send_json({'error': 'Not a git repository'}, 400) + return + + status = subprocess.run( + ['git', 'status', '--short'], + cwd=str(project_dir), capture_output=True, text=True, timeout=10 + ).stdout.strip() + + diff = subprocess.run( + ['git', 'diff'], + cwd=str(project_dir), capture_output=True, text=True, timeout=10 + ).stdout + + diff_cached = subprocess.run( + ['git', 'diff', '--cached'], + cwd=str(project_dir), capture_output=True, text=True, timeout=10 + ).stdout + + combined_diff = '' + if diff_cached: + combined_diff += '=== Staged Changes ===\n' + diff_cached + if diff: + if combined_diff: + combined_diff += '\n' + combined_diff += '=== Unstaged Changes ===\n' + diff + + self.send_json({ + 'project': project_name, + 'status': status, + 'diff': combined_diff, + 'hasDiff': bool(status) + }) + except subprocess.TimeoutExpired: + self.send_json({'error': 'Timeout'}, 500) + except Exception as e: + self.send_json({'error': str(e)}, 500) + + def handle_workspace_git_commit(self): + """Commit all changes in a workspace project.""" + try: + data = self._read_post_json() + project_name = data.get('project', '') + message = data.get('message', '').strip() + + project_dir = self._validate_project(project_name) + if not project_dir: + self.send_json({'success': False, 'error': 'Invalid project'}, 400) + return + + if not (project_dir / '.git').exists(): + self.send_json({'success': False, 'error': 'Not a git repository'}, 400) + return + + # Check if there's anything to commit + porcelain = subprocess.run( + ['git', 'status', '--porcelain'], + cwd=str(project_dir), capture_output=True, text=True, timeout=10 + ).stdout.strip() + + if not porcelain: + self.send_json({'success': False, 'error': 'Nothing to commit'}) + return + + files_changed = len([l for l in porcelain.split('\n') if l.strip()]) + + # Auto-message if empty + if not message: + now = datetime.now().strftime('%Y-%m-%d %H:%M') + message = f'Update: {now} ({files_changed} files)' + + # Stage all and commit + subprocess.run( + ['git', 'add', '-A'], + cwd=str(project_dir), capture_output=True, text=True, timeout=10 + ) + + result = subprocess.run( + ['git', 'commit', '-m', message], + cwd=str(project_dir), capture_output=True, text=True, timeout=30 + ) + + output = result.stdout + result.stderr + + if result.returncode == 0: + self.send_json({ + 'success': True, + 'message': message, + 'output': output, + 'filesChanged': files_changed + }) + else: + self.send_json({'success': False, 'error': output or 'Commit failed'}) + except subprocess.TimeoutExpired: + self.send_json({'success': False, 'error': 'Timeout'}, 500) + except Exception as e: + self.send_json({'success': False, 'error': str(e)}, 500) + + def _ensure_gitea_remote(self, project_dir, project_name): + """Create Gitea repo and add remote if no origin exists. Returns (ok, message).""" + import urllib.request + + if not GITEA_TOKEN: + return False, 'GITEA_TOKEN not set' + + # Create repo via Gitea API + api_url = f'{GITEA_URL}/api/v1/orgs/{GITEA_ORG}/repos' + payload = json.dumps({'name': project_name, 'private': True, 'auto_init': False}).encode() + req = urllib.request.Request(api_url, data=payload, method='POST', headers={ + 'Authorization': f'token {GITEA_TOKEN}', + 'Content-Type': 'application/json' + }) + try: + resp = urllib.request.urlopen(req, timeout=15) + resp.read() + except urllib.error.HTTPError as e: + body = e.read().decode(errors='replace') + if e.code == 409: + pass # repo already exists, fine + else: + return False, f'Gitea API error {e.code}: {body}' + + # Add remote with token auth + remote_url = f'{GITEA_URL}/{GITEA_ORG}/{project_name}.git' + # Insert token into URL for push auth + auth_url = remote_url.replace('https://', f'https://gitea:{GITEA_TOKEN}@') + subprocess.run( + ['git', 'remote', 'add', 'origin', auth_url], + cwd=str(project_dir), capture_output=True, text=True, timeout=5 + ) + return True, f'Created repo {GITEA_ORG}/{project_name}' + + def handle_workspace_git_push(self): + """Push a workspace project to its remote, creating Gitea repo if needed.""" + try: + data = self._read_post_json() + project_name = data.get('project', '') + + project_dir = self._validate_project(project_name) + if not project_dir: + self.send_json({'success': False, 'error': 'Invalid project'}, 400) + return + + if not (project_dir / '.git').exists(): + self.send_json({'success': False, 'error': 'Not a git repository'}, 400) + return + + created_msg = '' + # Check remote exists, create if not + remote_check = subprocess.run( + ['git', 'remote', 'get-url', 'origin'], + cwd=str(project_dir), capture_output=True, text=True, timeout=10 + ) + if remote_check.returncode != 0: + ok, msg = self._ensure_gitea_remote(project_dir, project_name) + if not ok: + self.send_json({'success': False, 'error': msg}) + return + created_msg = msg + '\n' + + # Push (set upstream on first push) + result = subprocess.run( + ['git', 'push', '-u', 'origin', 'HEAD'], + cwd=str(project_dir), capture_output=True, text=True, timeout=60 + ) + + output = result.stdout + result.stderr + + if result.returncode == 0: + self.send_json({'success': True, 'output': created_msg + (output or 'Pushed successfully')}) + else: + self.send_json({'success': False, 'error': output or 'Push failed'}) + except subprocess.TimeoutExpired: + self.send_json({'success': False, 'error': 'Push timeout (60s)'}, 500) + except Exception as e: + self.send_json({'success': False, 'error': str(e)}, 500) + + def handle_workspace_delete(self): + """Delete a workspace project.""" + try: + data = self._read_post_json() + project_name = data.get('project', '') + confirm = data.get('confirm', '') + + project_dir = self._validate_project(project_name) + if not project_dir: + self.send_json({'success': False, 'error': 'Invalid project'}, 400) + return + + if confirm != project_name: + self.send_json({'success': False, 'error': 'Confirmation does not match project name'}, 400) + return + + # Check for running processes + try: + result = subprocess.run( + ['pgrep', '-f', f'{project_dir.name}/(main\\.py|ralph)'], + capture_output=True, text=True, timeout=5 + ) + if result.stdout.strip(): + self.send_json({'success': False, 'error': 'Project has running processes. Stop them first.'}) + return + except subprocess.TimeoutExpired: + pass + + shutil.rmtree(str(project_dir)) + + self.send_json({ + 'success': True, + 'message': f'Project {project_name} deleted' + }) + except Exception as e: + self.send_json({'success': False, 'error': str(e)}, 500) + + def handle_workspace_logs(self): + """Get last N lines from a project log.""" + try: + parsed = urlparse(self.path) + params = parse_qs(parsed.query) + + project_name = params.get('project', [''])[0] + log_type = params.get('type', ['ralph'])[0] + lines_count = min(int(params.get('lines', ['100'])[0]), 500) + + project_dir = self._validate_project(project_name) + if not project_dir: + self.send_json({'error': 'Invalid project'}, 400) + return + + ralph_dir = project_dir / 'scripts' / 'ralph' + + # Determine log file + if log_type == 'ralph': + log_file = ralph_dir / 'logs' / 'ralph.log' + if not log_file.exists(): + # Try ralph-test.log + log_file = ralph_dir / 'logs' / 'ralph-test.log' + elif log_type == 'main': + log_file = ralph_dir / 'logs' / 'main.log' if ralph_dir.exists() else project_dir / 'main.log' + elif log_type == 'progress': + log_file = ralph_dir / 'progress.txt' + else: + # Try iteration log + if log_type.startswith('iteration-'): + log_file = ralph_dir / 'logs' / f'{log_type}.log' + else: + self.send_json({'error': 'Invalid log type'}, 400) + return + + if not log_file.exists(): + self.send_json({ + 'project': project_name, + 'type': log_type, + 'lines': [], + 'total': 0 + }) + return + + # Security: ensure path is within workspace + if not str(log_file.resolve()).startswith(str(WORKSPACE_DIR)): + self.send_json({'error': 'Access denied'}, 403) + return + + content = log_file.read_text(encoding='utf-8', errors='replace') + all_lines = content.split('\n') + total = len(all_lines) + last_lines = all_lines[-lines_count:] if len(all_lines) > lines_count else all_lines + + self.send_json({ + 'project': project_name, + 'type': log_type, + 'lines': last_lines, + 'total': total + }) + except Exception as e: + self.send_json({'error': str(e)}, 500) + def handle_youtube(self): try: content_length = int(self.headers['Content-Length']) diff --git a/dashboard/files.html b/dashboard/files.html index c7414f8..dff270f 100644 --- a/dashboard/files.html +++ b/dashboard/files.html @@ -838,6 +838,10 @@ Tasks + + + Workspace + KB diff --git a/dashboard/index.html b/dashboard/index.html index f6a6cfa..e15431c 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -1063,6 +1063,10 @@ Dashboard + + + Workspace + KB diff --git a/dashboard/notes.html b/dashboard/notes.html index fd64afe..6918f3b 100644 --- a/dashboard/notes.html +++ b/dashboard/notes.html @@ -688,6 +688,10 @@ Tasks + + + Workspace + KB diff --git a/dashboard/workspace.html b/dashboard/workspace.html new file mode 100644 index 0000000..198d938 --- /dev/null +++ b/dashboard/workspace.html @@ -0,0 +1,1021 @@ + + + + + + + Echo · Workspace + + + + + + +
+ + +
+ +
+ + +
+
Loading...
+
+
+ + + + + + + diff --git a/memory/2026-02-09.md b/memory/2026-02-09.md new file mode 100644 index 0000000..9586b85 --- /dev/null +++ b/memory/2026-02-09.md @@ -0,0 +1,29 @@ +# 2026-02-09 (Duminica) + +## Test Ralph Workflow - SUCCES +- Creat proiect test `file-monitor` in ~/workspace/ +- Ralph a completat 4/4 stories in ~12 minute, 4 iteratii +- Cost: ~$3.50 (Sonnet pentru implementare) +- 8 commits git, 28+ teste pytest +- Workflow: PRD generator → prd.json → ralph.sh → Claude Code autonom → commit + +## Simplificare AGENTS.md +- Redus sectiunea "Proiecte/Features Workflow" de la ~110 linii la ~15 linii +- Detaliile raman in cron jobs (evening-report, night-execute) + +## Actualizare Evening Report +- Adaugat context proiecte prioritare: roa2web, Chatbot Maria +- Regula: rapoarte ROA → feature roa2web, NU proiect separat +- Surse inspiratie: intrebari frecvente clienti, notes, insights +- Features existente prioritare fata de proiecte noi + +## Context Marius (interviu proiecte) +- **ROA clienti:** rapoarte, interfata web, notificari +- **Pain point:** raspunde la intrebari similare de la clienti +- **Chatbot Maria:** Flowise LXC 104, document store XML/MD, Groq+Ollama, ngrok→romfast.ro +- **Angajat nou:** poate mentine documentatia chatbot (scrie TXT) +- **roa2web:** are balanta, facturi, trezorerie; lipsesc validari ANAF, facturare valuta +- **Vending Master:** trebuie verificat rapoarte financiare XLSX + +## Prompt creat +- `prompts/workspace-panel.md` - panou Workspace Projects pentru dashboard Echo diff --git a/memory/kb/index.json b/memory/kb/index.json index 4931893..c37fb6f 100644 --- a/memory/kb/index.json +++ b/memory/kb/index.json @@ -21,6 +21,34 @@ "video": "", "tldr": "Monica Ion lucrează cu Mark pe relația cu angajații și frica de pierdere. Mark vine agitat cu teama de a pierde un angajat tehnic genial. Monica folosește **legea transformării** pentru a vindeca dure..." }, + { + "file": "notes-data/tools/ralph-workflow.md", + "title": "Ralph Workflow - Sistem Complet", + "date": "2026-02-09", + "tags": [], + "domains": [], + "types": [], + "category": "tools", + "project": null, + "subdir": null, + "video": "", + "tldr": "**Next:** Integrare night-execute" + }, + { + "file": "memory/2026-02-09.md", + "title": "2026-02-09 (Duminica)", + "date": "2026-02-09", + "tags": [], + "domains": [], + "types": [ + "memory" + ], + "category": "memory", + "project": null, + "subdir": null, + "video": "", + "tldr": "- `prompts/workspace-panel.md` - panou Workspace Projects pentru dashboard Echo" + }, { "file": "notes-data/coaching/2026-02-08-dimineata.md", "title": "Gândul de dimineață - 2026-02-08", @@ -2193,8 +2221,8 @@ "title": "Proiect: Vending Master - Integrare Website → ROA", "date": "2026-01-30", "tags": [ - "vending-master", - "integrare" + "integrare", + "vending-master" ], "domains": [ "work" @@ -2644,7 +2672,7 @@ } ], "stats": { - "total": 149, + "total": 151, "by_domain": { "work": 41, "health": 25, @@ -2663,9 +2691,9 @@ "projects": 44, "reflectii": 3, "retete": 1, - "tools": 4, + "tools": 5, "youtube": 42, - "memory": 15 + "memory": 16 } }, "domains": [ diff --git a/memory/kb/tehnici-pauza.md b/memory/kb/tehnici-pauza.md index ae38c9d..2a9766e 100644 --- a/memory/kb/tehnici-pauza.md +++ b/memory/kb/tehnici-pauza.md @@ -71,9 +71,9 @@ ### 3 Întrebări Anti-Blocaj - Când te simți blocat/victimă, întreabă-te rapid: - 1. "Ce depinde de MINE acum?" - 2. "Care e cel mai mic pas pe care pot să-l inițiez?" - 3. "Ce pot face EU chiar acum?" + 1. **Ce pot controla eu în situația asta?** + 2. **Ce aleg să fac acum?** + 3. **Ce vreau să fie diferit - și ce fac pentru asta?** - **Rezultat:** Mută atenția de la ce NU poți controla la ce POȚI - scoate din paralizie instant - **Sursă:** [Zoltan Vereș - Starea de Victimă](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-02-02_zoltan-veres-victima-complet.md) diff --git a/skills/ralph/templates/ralph.sh b/skills/ralph/templates/ralph.sh index b335ea7..0ab755e 100755 --- a/skills/ralph/templates/ralph.sh +++ b/skills/ralph/templates/ralph.sh @@ -83,6 +83,42 @@ fi # Creează directoare necesare mkdir -p "$SCRIPT_DIR/logs" "$SCRIPT_DIR/archive" "$SCRIPT_DIR/screenshots" +# Creează .gitignore dacă nu există +if [ ! -f "$PROJECT_DIR/.gitignore" ]; then + cat > "$PROJECT_DIR/.gitignore" << 'GITIGNORE' +# Python +__pycache__/ +*.py[cod] +*.pyo +*.egg-info/ +dist/ +build/ +.coverage +htmlcov/ +.pytest_cache/ + +# Virtual environment +venv/ +.venv/ + +# Ralph runtime +scripts/ralph/.ralph.pid +scripts/ralph/.last-branch +scripts/ralph/logs/ +scripts/ralph/screenshots/ +scripts/ralph/archive/ + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db +GITIGNORE + echo "Created .gitignore" +fi + # Inițializare progress file dacă nu există if [ ! -f "$PROGRESS_FILE" ]; then echo "# Ralph Progress Log" > "$PROGRESS_FILE"