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 <noreply@anthropic.com>
This commit is contained in:
Echo
2026-02-09 12:21:13 +00:00
parent e47f2179ea
commit e92284645c
10 changed files with 1771 additions and 117 deletions

124
AGENTS.md
View File

@@ -32,118 +32,24 @@ When I receive errors, bugs, or new feature requests:
1. **Planning → Opus**: Architecture, strategy, design decisions 1. **Planning → Opus**: Architecture, strategy, design decisions
2. **Execution → Sonnet**: Implementation, coding, debugging, testing 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) ### Context Proiecte Prioritare
- Impact mare pentru Marius → apoi pentru clienți - **roa2web** (gitea.romfast.ro/romfast/roa2web) - rapoarte, interfata web, notificari ERP ROA
- Proiecte de "joacă" pentru el mai întâi (să vadă cum îl ajută) - Rapoarte ROA noi → feature in roa2web (NU proiect separat)
- Din ce îl interesează (USER.md) - **Chatbot Maria** (Flowise, LXC 104) - documentatie, raspunsuri clienti
- Inspirat din conținut procesat (memory/kb/youtube/, articole/, insights/) - Imbunatatiri chatbot → documentatie + configurare Flowise
- **NU orice** - doar cu valoare concretă - **Proiecte independente** → ~/workspace/ cu Ralph autonom
### Workflow Complet ### Surse Inspiratie
- Intrebari frecvente clienti (validari ANAF D406/D394, facturare valuta, taxare inversa)
**1. SEARA (20:00) - evening-report:** - Note din memory/kb/ (youtube, insights, articole)
- Propun 1-2 proiecte NOI (P1, P2) - Probleme repetitive ale lui Marius
- 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)
## Memory ## Memory

View File

@@ -5,10 +5,12 @@ Handles YouTube summarization requests.
""" """
import json import json
import shutil
import subprocess import subprocess
import sys import sys
import re import re
import os import os
import signal
from http.server import HTTPServer, SimpleHTTPRequestHandler from http.server import HTTPServer, SimpleHTTPRequestHandler
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
from datetime import datetime from datetime import datetime
@@ -18,6 +20,20 @@ BASE_DIR = Path(__file__).parent.parent
TOOLS_DIR = BASE_DIR / 'tools' TOOLS_DIR = BASE_DIR / 'tools'
NOTES_DIR = BASE_DIR / 'kb' / 'youtube' NOTES_DIR = BASE_DIR / 'kb' / 'youtube'
KANBAN_DIR = BASE_DIR / 'dashboard' 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): class TaskBoardHandler(SimpleHTTPRequestHandler):
@@ -32,6 +48,16 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
self.handle_git_commit() self.handle_git_commit()
elif self.path == '/api/pdf': elif self.path == '/api/pdf':
self.handle_pdf_post() 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: else:
self.send_error(404) self.send_error(404)
@@ -229,6 +255,12 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
self.handle_files_get() self.handle_files_get()
elif self.path.startswith('/api/diff'): elif self.path.startswith('/api/diff'):
self.handle_git_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/'): elif self.path.startswith('/api/'):
self.send_error(404) self.send_error(404)
else: else:
@@ -726,6 +758,596 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
else: else:
self.send_json({'error': 'Unknown action'}, 400) 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): def handle_youtube(self):
try: try:
content_length = int(self.headers['Content-Length']) content_length = int(self.headers['Content-Length'])

View File

@@ -838,6 +838,10 @@
<i data-lucide="layout-list"></i> <i data-lucide="layout-list"></i>
<span>Tasks</span> <span>Tasks</span>
</a> </a>
<a href="/echo/workspace.html" class="nav-item">
<i data-lucide="code"></i>
<span>Workspace</span>
</a>
<a href="/echo/notes.html" class="nav-item"> <a href="/echo/notes.html" class="nav-item">
<i data-lucide="file-text"></i> <i data-lucide="file-text"></i>
<span>KB</span> <span>KB</span>

View File

@@ -1063,6 +1063,10 @@
<i data-lucide="layout-dashboard"></i> <i data-lucide="layout-dashboard"></i>
<span>Dashboard</span> <span>Dashboard</span>
</a> </a>
<a href="/echo/workspace.html" class="nav-item">
<i data-lucide="code"></i>
<span>Workspace</span>
</a>
<a href="/echo/notes.html" class="nav-item"> <a href="/echo/notes.html" class="nav-item">
<i data-lucide="file-text"></i> <i data-lucide="file-text"></i>
<span>KB</span> <span>KB</span>

View File

@@ -688,6 +688,10 @@
<i data-lucide="layout-list"></i> <i data-lucide="layout-list"></i>
<span>Tasks</span> <span>Tasks</span>
</a> </a>
<a href="/echo/workspace.html" class="nav-item">
<i data-lucide="code"></i>
<span>Workspace</span>
</a>
<a href="/echo/notes.html" class="nav-item active"> <a href="/echo/notes.html" class="nav-item active">
<i data-lucide="file-text"></i> <i data-lucide="file-text"></i>
<span>KB</span> <span>KB</span>

1021
dashboard/workspace.html Normal file

File diff suppressed because it is too large Load Diff

29
memory/2026-02-09.md Normal file
View File

@@ -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

View File

@@ -21,6 +21,34 @@
"video": "", "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..." "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", "file": "notes-data/coaching/2026-02-08-dimineata.md",
"title": "Gândul de dimineață - 2026-02-08", "title": "Gândul de dimineață - 2026-02-08",
@@ -2193,8 +2221,8 @@
"title": "Proiect: Vending Master - Integrare Website → ROA", "title": "Proiect: Vending Master - Integrare Website → ROA",
"date": "2026-01-30", "date": "2026-01-30",
"tags": [ "tags": [
"vending-master", "integrare",
"integrare" "vending-master"
], ],
"domains": [ "domains": [
"work" "work"
@@ -2644,7 +2672,7 @@
} }
], ],
"stats": { "stats": {
"total": 149, "total": 151,
"by_domain": { "by_domain": {
"work": 41, "work": 41,
"health": 25, "health": 25,
@@ -2663,9 +2691,9 @@
"projects": 44, "projects": 44,
"reflectii": 3, "reflectii": 3,
"retete": 1, "retete": 1,
"tools": 4, "tools": 5,
"youtube": 42, "youtube": 42,
"memory": 15 "memory": 16
} }
}, },
"domains": [ "domains": [

View File

@@ -71,9 +71,9 @@
### 3 Întrebări Anti-Blocaj ### 3 Întrebări Anti-Blocaj
- Când te simți blocat/victimă, întreabă-te rapid: - Când te simți blocat/victimă, întreabă-te rapid:
1. "Ce depinde de MINE acum?" 1. **Ce pot controla eu în situația asta?**
2. "Care e cel mai mic pas pe care pot să-l inițiez?" 2. **Ce aleg să fac acum?**
3. "Ce pot face EU chiar 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 - **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) - **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)

View File

@@ -83,6 +83,42 @@ fi
# Creează directoare necesare # Creează directoare necesare
mkdir -p "$SCRIPT_DIR/logs" "$SCRIPT_DIR/archive" "$SCRIPT_DIR/screenshots" 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ă # Inițializare progress file dacă nu există
if [ ! -f "$PROGRESS_FILE" ]; then if [ ! -f "$PROGRESS_FILE" ]; then
echo "# Ralph Progress Log" > "$PROGRESS_FILE" echo "# Ralph Progress Log" > "$PROGRESS_FILE"