Compare commits
151 Commits
9c1f9f94e7
...
voice/stt-
| Author | SHA1 | Date | |
|---|---|---|---|
| ec23d188ec | |||
| 392d1a5be2 | |||
| c8be07b1f6 | |||
| 97e34be863 | |||
| 5c9748ffb4 | |||
| 6e9dfd137c | |||
| a8d024944d | |||
| 55a175f78e | |||
| 735b282179 | |||
| c401204fa2 | |||
| 0ce8a5a04d | |||
| e79bed7afe | |||
| 4be70440e8 | |||
| 13931db953 | |||
| 23666f7910 | |||
| 217da65417 | |||
| 0cc01c1450 | |||
| c93c4f822e | |||
| 3af6bcaea4 | |||
| a3eefbc799 | |||
| a48562b2f5 | |||
| af5af8133f | |||
| c6d11bdf9f | |||
| 44cf0001bb | |||
| 574f9be5ea | |||
| 0d2d5b860d | |||
| 8fe39adc01 | |||
| 3dd2ddbd6a | |||
| 2a05f7cf49 | |||
| ba63e22277 | |||
| 990be00b70 | |||
| 8cb76e130d | |||
| 3570d9a625 | |||
| f04e033dbe | |||
| 63b7fcd00e | |||
| 246986b5ae | |||
| 608668d8a6 | |||
| 2bcefe1ab4 | |||
| a5cab9677a | |||
| f4880a2a18 | |||
| 8432fe3150 | |||
| d0faeed181 | |||
| e3c18f15ed | |||
| 176dc01aa6 | |||
| 6d1d4bfeb5 | |||
| 77df09974c | |||
| 38259f3cfd | |||
| b08f039917 | |||
| fb7ca74ca1 | |||
| 8594f98bff | |||
| 1462f98ae9 | |||
| 5e930ade02 | |||
| e771479d67 | |||
| 2830bf48f2 | |||
| 44c9bb4e61 | |||
| 03d875974b | |||
| 84f304f7be | |||
| 3c9322ba93 | |||
| 6d56356ada | |||
| ff9b9a0d1d | |||
| 3e7818286b | |||
| dedeedf024 | |||
| bf9380f2ad | |||
| 4b494eb2f2 | |||
| 36a38a1e26 | |||
| deb86c705f | |||
| 51e56af557 | |||
| 655ed3ae09 | |||
| e06a79d98c | |||
| b95395ec2c | |||
| 86384b38e3 | |||
| 094c6be5a9 | |||
| 479fcc4356 | |||
| b0535695f4 | |||
| 5745621e9b | |||
| 145e1eb3ab | |||
| 53c348f331 | |||
| 90c2a90b5e | |||
| bee409d164 | |||
| e4674b5dda | |||
| 0bfa652b31 | |||
| ad681c7a73 | |||
| 74d98553cc | |||
| d22ce49d76 | |||
| c146d68498 | |||
| 512aa5cd06 | |||
| f885d75528 | |||
| 1fbd624195 | |||
| e513c7fbf6 | |||
| f9a091133a | |||
| abadff4ea8 | |||
| d3196b0717 | |||
| 1b2b37a6bb | |||
| 277a43b81f | |||
| 04d49e7ea3 | |||
| 537bab465c | |||
| 0c02f0de50 | |||
| b9a5f733c2 | |||
| 42797c0bbb | |||
| bfc2283e6f | |||
| 51af0918a4 | |||
| 417de65069 | |||
| c2455e6245 | |||
| 56f6c0df01 | |||
| eb693a2e71 | |||
| 30678e6abf | |||
| 2dd5aee9a7 | |||
| a5d054d16f | |||
| 9bc5c3a3a2 | |||
| d741541e23 | |||
| fa7c0fd1c6 | |||
| bb917e0b33 | |||
| 0ac0f3b907 | |||
| e0abe5cdfc | |||
| bee21594f5 | |||
| dd8f40774f | |||
| df8ccc694b | |||
| 84ab27a6b5 | |||
| 55e34afd59 | |||
| 5678138cc5 | |||
| 9fce04f212 | |||
| e964777f69 | |||
| 5f87545b66 | |||
| 67d10c4c9a | |||
| b00d9d6fbd | |||
| af444d7066 | |||
| c82dbc5654 | |||
| b3ed653bb3 | |||
| e747491b85 | |||
| ca9167d129 | |||
| cd07e43533 | |||
| 4e78ef7219 | |||
| 929d2e9c81 | |||
| 000b406c8d | |||
| 9314d63aa0 | |||
| 9d447b9ff1 | |||
| 5fafc29dc1 | |||
| d9450ce70d | |||
| 006123a63b | |||
| 19e253ec43 | |||
| 08c330a371 | |||
| 95bd651377 | |||
|
|
e8492c3fa9 | ||
|
|
fd9d962ad2 | ||
|
|
8ce7ea3bd6 | ||
|
|
207b39f957 | ||
|
|
b3c06c0238 | ||
|
|
91707c5841 | ||
|
|
c8ce94611b | ||
|
|
8b76a2dbf7 | ||
|
|
f8ff971627 |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -6,6 +6,9 @@ __pycache__/
|
||||
*.egg-info/
|
||||
sessions/
|
||||
logs/
|
||||
memory/*
|
||||
!memory/kb/
|
||||
memory/kb/*.sqlite
|
||||
*.sqlite
|
||||
.env
|
||||
*.secret
|
||||
@@ -18,3 +21,11 @@ bridge/whatsapp/auth/
|
||||
credentials/
|
||||
.claude/
|
||||
*.pid
|
||||
memory.bak/
|
||||
.use_openrouter
|
||||
.gstack/
|
||||
|
||||
# Runtime state — auto-modified by dashboard/cron/heartbeat
|
||||
approved-tasks.json
|
||||
dashboard/status.json
|
||||
tools/anaf-monitor/monitor.log
|
||||
|
||||
1
.use_openrouter_no
Normal file
1
.use_openrouter_no
Normal file
@@ -0,0 +1 @@
|
||||
# OpenRouter mode enabled
|
||||
314
CLAUDE.md
314
CLAUDE.md
@@ -4,7 +4,7 @@
|
||||
|
||||
Nu ești un tool de cod. Ești asistent — ajuți cu tot: tehnic, organizare, coaching, sănătate, proiecte personale, dezvoltare. Cine ești și cum te comporți e definit în `personality/*.md`. **Respectă aceste fișiere întotdeauna.**
|
||||
|
||||
## How It Works
|
||||
## Cum funcționează
|
||||
|
||||
Mesajele ajung la tine prin adaptoare (Discord, Telegram, WhatsApp) → `router.py` → `claude_session.py` → Claude CLI subprocess → răspuns trimis înapoi.
|
||||
|
||||
@@ -16,14 +16,88 @@ Personalitatea ta se construiește din `personality/*.md`, concatenate în ordin
|
||||
- `HEARTBEAT.md` — verificări periodice
|
||||
- `TOOLS.md` — unelte disponibile
|
||||
|
||||
## Commands
|
||||
## Principii de Workflow
|
||||
|
||||
> **Aplicabilitate:** aceste principii se aplică pentru **modificări de cod** în acest repo sau în proiectele Ralph. Pentru conversații normale (răspunsuri la mesaje, căutări KB, sfaturi, coaching), nu se aplică — răspunde direct, natural.
|
||||
|
||||
### 1. Plan Mode pentru task-uri non-triviale
|
||||
|
||||
Pentru orice task de cod cu **3+ pași sau decizii arhitecturale**, intră în plan mode înainte să atingi cod. Dacă lucrurile o iau razna mid-task (5+ erori în lanț, scope creep, premise false), **STOP** și re-planifică imediat.
|
||||
|
||||
Folosește skill-urile gstack pentru review:
|
||||
- `/plan-eng-review` — arhitectură, edge cases, performance
|
||||
- `/plan-ceo-review` — scope, ambiție, 10-star product
|
||||
- `/plan-design-review` — UI/UX înainte de implementare
|
||||
- `/autoplan` — toate trei automat, cu approval gate la final
|
||||
|
||||
### 2. Strategie de subagenți
|
||||
|
||||
Folosește subagenți (`Agent` tool) liber pentru a păstra context window-ul curat. Offload research, exploration, parallel analysis. **Un singur task per subagent** — nu suprasolicita.
|
||||
|
||||
- `Explore` — căutări codebase
|
||||
- `general-purpose` — research multi-step
|
||||
- `Plan` — design de implementare
|
||||
|
||||
### 3. Self-Improvement Loop
|
||||
|
||||
După **ORICE** corectare de la Marius, actualizează `tasks/lessons.md` cu pattern-ul învățat. Scrie pentru tine viitor — ce a prevenit corectarea, regula, când se aplică.
|
||||
|
||||
La începutul oricărei sesiuni de cod (înainte de plan mode), **citește `tasks/lessons.md`** și aplică lecțiile relevante. Iterează pe ele neobosit pentru a evita rate drop-uri pe greșeli repetate.
|
||||
|
||||
Ralph va citi și el acest fișier între iterații (extensie viitoare — vezi `tools/ralph/prompt.md`).
|
||||
|
||||
### 4. Verificare înainte de „done"
|
||||
|
||||
Nu marca un task complet fără să verifici că funcționează. Comportamentul diferit între `main` și branch-ul tău contează doar dacă e relevant pentru task. Întreabă-te mereu: **„Ar aproba un staff engineer asta?"**
|
||||
|
||||
Folosește din gstack:
|
||||
- `/qa` — test + fix loop iterativ
|
||||
- `/qa-only` — doar raport de bug-uri
|
||||
- `/review` — pre-merge diff review
|
||||
- `/devex-review` — DX live audit
|
||||
- `/ship` — full pipeline (tests + CHANGELOG + PR)
|
||||
|
||||
### 5. Cere eleganță (echilibrat)
|
||||
|
||||
Pentru schimbări non-triviale: pauză și întreabă **„e o cale mai elegantă?"** Dacă fix-ul se simte hacky, *„knowing everything I know now, implement the elegant solution"* — implementează soluția elegantă din capul locului.
|
||||
|
||||
**Skip pentru fixes simple, schimbări obvii** — nu over-engineer. Provoacă-ți munca înainte să o prezinți.
|
||||
|
||||
Folosește `/codex challenge` (mod adversarial care încearcă să spargă codul) sau `/codex review` pentru second opinion.
|
||||
|
||||
### 6. Bug fixing autonom
|
||||
|
||||
Când Marius dă un bug report: **just fix it**. Fără hand-holding. Indică logs, errors, failing tests — apoi rezolvă-le. Zero context switching cerut de la user.
|
||||
|
||||
Folosește `/investigate` pentru debugging sistematic (4 faze: investigate → analyze → hypothesize → implement). **Iron Law: fără fix fără root cause.**
|
||||
|
||||
Ralph face exact asta noaptea, autonom, pe proiectele aprobate.
|
||||
|
||||
## Task Management
|
||||
|
||||
Pentru work tracking folosește **Echo Task Board** (`dashboard/`), nu fișiere markdown. Endpoints în `dashboard/handlers/`.
|
||||
|
||||
1. **Plan First** — task-uri cu checkboxes în plan mode
|
||||
2. **Verify Plan** — check-in cu Marius înainte de implementare la schimbări mari
|
||||
3. **Track Progress** — marchează task-urile complete pe măsură ce le faci
|
||||
4. **Explain Changes** — high-level summary la fiecare pas
|
||||
5. **Document Results** — la final, secțiune review în PR sau în `tasks/<task>.md`
|
||||
6. **Capture Lessons** — la corectări, update `tasks/lessons.md` (vezi principiul 3)
|
||||
|
||||
## Core Principles
|
||||
|
||||
- **Simplicitate înainte de toate** — fă cele mai simple schimbări posibile. Impact minim, cod minimal.
|
||||
- **Zero lene** — root causes, nu temporary fixes. Standard de senior developer.
|
||||
- **Impact minim** — atinge doar ce e necesar. Fără side effects la features noi.
|
||||
|
||||
## Comenzi
|
||||
|
||||
```bash
|
||||
# Tests
|
||||
source .venv/bin/activate && pytest tests/
|
||||
pytest tests/test_router.py::test_clear_command -v
|
||||
|
||||
# Start
|
||||
# Pornire
|
||||
systemctl --user start echo-core # systemd
|
||||
source .venv/bin/activate && python3 src/main.py # manual
|
||||
|
||||
@@ -34,46 +108,230 @@ systemctl --user start echo-whatsapp-bridge
|
||||
eco status
|
||||
eco doctor
|
||||
|
||||
# Dependencies
|
||||
# Dependențe
|
||||
source .venv/bin/activate && pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Architecture
|
||||
## Arhitectură
|
||||
|
||||
**Flow:** Adapter → `router.py` → `claude_session.py` → Claude CLI → response split → Adapter reply
|
||||
**Flow:** Adapter → `router.py` → `claude_session.py` → Claude CLI → split răspuns → reply pe Adapter
|
||||
|
||||
**Adapters** (concurrent, `asyncio.gather()` in `src/main.py`):
|
||||
- **Discord** (`src/adapters/discord_bot.py`) — slash commands, 2000 char split
|
||||
- **Telegram** (`src/adapters/telegram_bot.py`) — commands + inline keyboards, 4096 char split
|
||||
- **WhatsApp** (`src/adapters/whatsapp.py`) — polls Baileys bridge at `http://127.0.0.1:8098`, 4096 char split
|
||||
**Adaptoare** (concurente, `asyncio.gather()` în `src/main.py`):
|
||||
- **Discord** (`src/adapters/discord_bot.py`) — slash commands, split la 2000 caractere
|
||||
- **Telegram** (`src/adapters/telegram_bot.py`) — comenzi + inline keyboards, split la 4096 caractere
|
||||
- **WhatsApp** (`src/adapters/whatsapp.py`) — polling Baileys bridge la `http://127.0.0.1:8098`, split la 4096 caractere
|
||||
|
||||
**Sessions** (`src/claude_session.py`): One persistent session per channel. `claude --resume <session_id>`. External messages wrapped in `[EXTERNAL CONTENT]` injection markers.
|
||||
**Sesiuni** (`src/claude_session.py`): O sesiune persistentă per canal. `claude --resume <session_id>`. Mesajele externe sunt împachetate în markeri `[EXTERNAL CONTENT]`.
|
||||
|
||||
**State:** `sessions/active.json` — channel ID → `{session_id, model, message_count, ...}`
|
||||
|
||||
**Credentials** (`src/credential_store.py`): System keyring, service `"echo-core"`. Never secrets as CLI args.
|
||||
**Credențiale** (`src/credential_store.py`): Keyring de sistem, serviciu `"echo-core"`. Niciodată secrete ca argumente CLI.
|
||||
|
||||
**Config** (`src/config.py`): `config.json` with dot-notation. Namespaces: `channels`, `telegram_channels`, `whatsapp_channels`.
|
||||
**Config** (`src/config.py`): `config.json` cu dot-notation. Namespaces: `channels`, `telegram_channels`, `whatsapp_channels`.
|
||||
|
||||
**Scheduler** (`src/scheduler.py`): APScheduler + `cron/jobs.json`, isolated sessions.
|
||||
**Scheduler** (`src/scheduler.py`): APScheduler + `cron/jobs.json`, sesiuni izolate.
|
||||
|
||||
**Heartbeat** (`src/heartbeat.py`): Email, calendar, KB, git checks. Quiet hours 23-08.
|
||||
**Heartbeat** (`src/heartbeat.py`): Verificări email, calendar, KB, git. Ore tăcere 23-08.
|
||||
|
||||
**Memory** (`src/memory_search.py`): Ollama all-minilm embeddings (384 dim) + SQLite cosine similarity.
|
||||
**Ralph** (`tools/ralph/`): Sistem autonom de execuție. `ralph.sh` este un bash loop care cheamă `claude` CLI (subscription, nu API) per user story din `prd.json`. Generarea PRD se face cu `tools/ralph_prd_generator.py` (model Opus). Workspace-ul proiectelor e la `~/workspace/`.
|
||||
|
||||
## Import Convention
|
||||
**Memory** (`memory/` în acest repo — sursa unică de adevăr). Retrieval **hibrid**, două căi:
|
||||
1. **Navigare (întâi, pentru lookup pe subiect/parafrază):** citește `memory/kb/index.md` (router cu folderele), alege folderul relevant, apoi citește `memory/kb/<folder>/index.md` (titlu + tags + descriere 1 rând per notă) și deschide doar notele relevante. Ieftin și funcționează chiar dacă Ollama e picat. Generat de `tools/update_notes_index.py` (regenerat din heartbeat).
|
||||
2. **RAG semantic (pentru recall fuzzy):** `src/memory_search.py` — embeddings Ollama all-minilm (384 dim) + cosine pe SQLite. `search()` deduplică pe best-chunk-per-fișier și, dacă Ollama remote (`config.json → ollama.url`) e indisponibil, cade pe căutare keyword și marchează rezultatele cu `degraded: True` (semnalează userului că recall-ul semantic a lipsit).
|
||||
|
||||
Absolute imports via `sys.path.insert(0, PROJECT_ROOT)`: `from src.config import ...`, `from src.adapters.discord_bot import ...`. No circular imports.
|
||||
*Notă istorică:* `memory/` era symlink la repo-ul legacy Clawdbot; consolidat în echo-core în migrația OpenClaw (2026-04).
|
||||
|
||||
## Key Files
|
||||
**Dashboard** (`dashboard/`): Echo Task Board — HTTP API + UI static servit de `dashboard/api.py` pe portul 8088, de obicei în spatele unui reverse proxy la `/echo/`. Logica endpoint-urilor împărțită în mixin-uri `dashboard/handlers/*.py`; path-urile centralizate în `dashboard/constants.py`. Template systemd user unit la `dashboard/echo-taskboard.service`. `workspace.html` este hub-ul unificat de proiecte (fostul ralph.html + workspace.html); `/echo/ralph.html` → 302 redirect la `/echo/workspace.html`. Autentificare prin cookie httpOnly `dashboard=<token>`; `DASHBOARD_TOKEN` setat în `dashboard/.env`.
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| `src/main.py` | Entry point — adapters + scheduler + heartbeat |
|
||||
| `src/router.py` | Commands vs Claude messages |
|
||||
| `src/claude_session.py` | Claude CLI wrapper with `--resume` |
|
||||
| `src/credential_store.py` | Keyring secrets |
|
||||
| `cli.py` | CLI diagnostics (eco) |
|
||||
| `config.json` | Runtime config |
|
||||
| `bridge/whatsapp/index.js` | Baileys + Express bridge, port 8098 |
|
||||
## Dashboard — Note arhitecturale
|
||||
|
||||
**Cookie auth:** dashboard folosește httpOnly cookie `dashboard=...`; SameSite=Strict; Path=/echo/. EventSource SSE trimite cookie-ul automat. `DASHBOARD_TOKEN` din `dashboard/.env` — setează o dată, restart service. Resetare: schimbă valoarea din .env + restart.
|
||||
|
||||
**jsonlock helper (`src/jsonlock.py`):** folosește `read_locked(path)` / `write_locked(path, mutator)` pentru orice scriere la `approved-tasks.json`, `sessions/*.json`. Lock pe sidecar `<path>.lock` (inode stabil chiar și după os.replace). Ordine canonică lock-uri: alfabetic după filename. Re-entrant (threading.local refcount).
|
||||
|
||||
**Slug convention:** slug-urile proiectelor validează cu regex `^[a-z0-9][a-z0-9\-_]{1,38}[a-z0-9]$` — permit hifene ȘI underscore. Validare centralizată în `dashboard/handlers/_validators.py`.
|
||||
|
||||
**Proxy timeout:** pentru nginx/caddy, setează `proxy_read_timeout >= 60s` și `proxy_buffering off` pentru `/echo/api/projects/stream` și `/echo/api/projects/<slug>/plan/*` (SSE + planning au răspunsuri lungi).
|
||||
|
||||
**Planning fragmentation (known limit):** sesiunile de planning pornite din Discord/Telegram nu se fuzionează cu cele din dashboard. Dashboard afișează sesiunea cea mai recentă per slug indiferent de adapter. P3 follow-up.
|
||||
|
||||
## Ralph — Execuție autonomă de proiecte
|
||||
|
||||
Sistem de implementare autonomă care rulează noaptea. Flow complet:
|
||||
|
||||
```
|
||||
21:00 evening-report → propune features/proiecte, adaugă în approved-tasks.json (status: pending)
|
||||
email lui Marius cu instrucțiuni de aprobare
|
||||
Marius → /a <slug> (Discord/Telegram/WhatsApp → router.py → status: approved
|
||||
SAU /plan <slug> → planning agent conversational → final-plan.md → approved)
|
||||
23:00 night-execute → citește approved, clonează repo dacă lipsește, generează PRD din final-plan.md,
|
||||
lansează ralph.sh; actualizează approved-tasks.json (running, pid: PID)
|
||||
08:30 morning-report → citește approved-tasks.json + prd.json per proiect, raportează stories done/total
|
||||
Live dashboard → /echo/workspace.html — cards per proiect cu status, iter, ETA, log, stop; realtime SSE
|
||||
```
|
||||
|
||||
**Două căi de aprobare**:
|
||||
- **Direct**: `/a <slug>` — pentru proiecte simple unde descrierea e suficientă
|
||||
- **Conversational** (W2 — `/plan <slug>` SAU buton "Planifică" pe `/l`): Echo poartă o conversație multi-fază prin skills gstack (`/office-hours` → `/plan-ceo-review` → `/plan-eng-review` → opțional `/plan-design-review` dacă tags include "ui"), produce `~/workspace/<slug>/scripts/ralph/final-plan.md` și prezintă rezumat cu butonul "✅ Dau drumul tonight". `night-execute` îl folosește ca input pentru PRD generator (Opus extrage user stories cu acceptanceCriteria, tags, dependsOn).
|
||||
|
||||
**Comenzi** (funcționează pe toate adaptoarele — Discord, Telegram, WhatsApp):
|
||||
|
||||
| Comandă | Efect |
|
||||
|---------|-------|
|
||||
| `/p <slug> <descriere>` | Adaugă proiect nou cu status `pending` |
|
||||
| `/a` | Listează proiectele pending |
|
||||
| `/a <slug>` sau `/a P1,P2` | Aprobă pentru tonight (path direct) |
|
||||
| `/plan <slug>` | Pornește planning agent conversational (multi-fază skills gstack) |
|
||||
| `/cancel` | Anulează planning în curs (revert status → pending) |
|
||||
| `/l` | **Discord/Telegram**: meniu interactiv (Views/InlineKeyboardMarkup) cu butoane per proiect; **WhatsApp**: text plain + redirect spre Discord/TG |
|
||||
| `/l <slug>` | Status proiect specific |
|
||||
| `/k <slug>` | Trimite SIGTERM la ralph.sh PID |
|
||||
|
||||
**UX interactiv** (Discord/Telegram):
|
||||
- `/l` deschide `RalphRootView` (Discord) / InlineKeyboardMarkup (Telegram) cu butoane per workspace project
|
||||
- Click pe proiect → submeniu cu acțiuni: ➕ Propune feature (modal/ForceReply), 🧠 Planifică (W2), 👁 Vezi PRD, 📊 Status, ✅ Aprobă tonight, 🛑 Stop, 🔙 Înapoi
|
||||
- La sfârșitul planning: butoane ✅ Dau drumul tonight / ✏️ Mai gândim / 🛑 Anulează
|
||||
- State per `(adapter, channel)` în `sessions/ralph_flow.json` și `sessions/planning.json` (TTL 10min/60min)
|
||||
|
||||
Pe **Discord**: slash commands native cu autocomplete dinamic: `/p <tab>` listează workspace, `/a <tab>` pending, `/k <tab>` running. Modal cu `TextInput` pentru descriere. Critical pattern: `await interaction.response.defer(ephemeral=True)` în orice button callback cu I/O (Discord 3s timeout).
|
||||
Pe **Telegram**: `callback_ralph` cu pattern `^ralph:` rutează acțiuni; `ForceReply` pentru input text descriere.
|
||||
Pe **WhatsApp**: text-only — meniu redirect la Discord/Telegram. **Text-keyword shortcuts**: `aprob <slug>` → `/a <slug>`, `stop <slug>` → `/k <slug>`, `stare`/`stare <slug>` → `/l`/`/l <slug>` (case-insensitive, doar pe WhatsApp; Discord/Telegram nu sunt afectate). `propose` intentionally NOT covered — descrierea fragilă.
|
||||
|
||||
**Aliasuri legacy** (funcționează încă pentru backwards compat): `!propose`, `!approve`, `!status`, `!stop`.
|
||||
|
||||
**Fișiere cheie Ralph:**
|
||||
|
||||
| Path | Rol |
|
||||
|------|-----|
|
||||
| `approved-tasks.json` | Coordonare între cron jobs + UX. Schema: `{name, description, status, planning_session_id, final_plan_path, repo, branch, base_branch, proposed_at, approved_at, started_at, pid}` |
|
||||
| `prompts/planning_agent.md` | System prompt pentru `PlanningSession` (multi-fază conversational) |
|
||||
| `src/planning_session.py` | Wrapper subprocess `claude -p` cu working dir = `~/workspace/<slug>/`, `--add-dir` skills gstack + project artifacts. `--max-turns=20` cu retry pe `error_max_turns` |
|
||||
| `src/planning_orchestrator.py` | Coordonează fazele: fresh subprocess per skill phase; coordinează prin disk artifacts gstack convention; tag detection ui-scope |
|
||||
| `sessions/planning.json` | State per `(adapter, channel)` planning session: session_id, current_phase, etc. — pentru re-resume la restart |
|
||||
| `tools/ralph/ralph.sh` | Bash loop DAG-aware: N iterații × `claude` CLI per story; folosește `tools/ralph_dag.py` pentru selecție topologică, retry guard (3 retries), rate-limit detection |
|
||||
| `tools/ralph/prompt.md` | Smart gates dispatcher pe `story.tags` (Faza 3): refactor→/workflow:simplify, ui→/qa+screenshot, vercel→push+gh checks, db→schema diff, default→/review |
|
||||
| `tools/ralph/prd-template.json` | Template prd.json: stories cu `acceptanceCriteria[]`, `tags[]`, `dependsOn[]`, `passes`, `retries` |
|
||||
| `tools/ralph_prd_generator.py` | Generează prd.json. Cu `final_plan_path` (de la PlanningOrchestrator) → Opus extrage stories cu acceptance criteria. Fără → backwards-compat description-only |
|
||||
| `tools/ralph_dag.py` | Pure functions Python (testabile): `infer_tags_from_paths`, `force_include_tags`, `topological_eligible`, `mark_failed`, blocked propagation iterativă. CLI subcommands chemate din ralph.sh (`infer-tags`, `next-story`, `mark-failed`, `incr-retry`) |
|
||||
| `tools/ralph_usage.py` | Rate limit budget tracking: pure functions `extract_usage_entry`, `parse_usage_jsonl`, `aggregate_by_day`, `aggregate_by_project` + CLI append/summarize. Atomic write JSONL |
|
||||
| `~/workspace/<name>/scripts/ralph/usage.jsonl` | Append-only log per `claude -p` call (cost, tokens, model, duration) — generat din ralph.sh, agregat de `/api/ralph/usage` |
|
||||
| `~/workspace/<name>/scripts/ralph/final-plan.md` | Output planning agent — citit de PRD generator |
|
||||
| `~/workspace/<name>/scripts/ralph/prd.json` | PRD per proiect cu schema extinsă |
|
||||
| `~/workspace/<name>/scripts/ralph/logs/` | Loguri ralph.sh per rulare |
|
||||
| `dashboard/handlers/ralph.py` | Endpoints `/api/ralph/status`, `/<slug>/log`, `/<slug>/prd`, `/<slug>/stop`, `/<slug>/rollback`, `/usage[?days=N]`, `/stream` (SSE) |
|
||||
| `dashboard/handlers/projects.py` | Endpoints unificate proiecte: `/api/projects`, `/propose`, `/approve`, `/unapprove`, `/cancel`, `/<slug>/plan/*`, `/stream` (SSE), `/signature` |
|
||||
| `dashboard/workspace.html` | Hub unificat proiecte — cards status/iter/ETA, log, prd, stop/rollback. Realtime SSE cu fallback polling 5s. Înlocuiește ralph.html (care face 302 redirect aici) |
|
||||
| `dashboard/.env` | `GITEA_TOKEN` pentru clone HTTPS la `gitea.romfast.ro`; `DASHBOARD_TOKEN` pentru cookie auth |
|
||||
|
||||
**Status flow:** `pending` → (`planning` →) `approved` → `running` → `complete` / `failed` / `stopped` / `blocked` (DAG)
|
||||
**Story status (în prd.json):** `passes:false` + `retries:N` → `passes:true` SAU `failed:rate_limited|max_retries`
|
||||
|
||||
**Workspace proiecte** (`~/workspace/`): roa2web, gomag-vending, vending_data_intelligence_report, btgo-playwright, space-booking, romfast-website, game-library, wol, romfastsql
|
||||
|
||||
**Reguli importante:**
|
||||
- Ralph NU modifică niciodată `src/router.py`, `src/claude_session.py` sau alte fișiere core din echo-core
|
||||
- Self-improvement echo-core NUMAI pe branch `ralph/echo-improve`, niciodată pe master
|
||||
- Clone-urile folosesc `GITEA_TOKEN` din `dashboard/.env`: `https://moltbot:${TOKEN}@gitea.romfast.ro/romfast/<name>.git`
|
||||
|
||||
### Features pe repo-uri existente (worktree-aware)
|
||||
|
||||
Slug-ul proiectului nu trebuie să corespundă cu un repo Gitea. Pentru o feature pe un repo existent (ex: `roa2web-telegram-bonuri` ca feature pe `roa2web`), folosește câmpurile opționale `repo`, `branch`, `base_branch`:
|
||||
|
||||
- **`repo`** — numele repo-ului Gitea de clonat (default: slug-ul proiectului).
|
||||
- **`branch`** — feature branch nou care va fi creat după clone (default: niciunul, ralph lucrează pe HEAD-ul default).
|
||||
- **`base_branch`** — branch-ul de la care porneste `branch` (default: `main`).
|
||||
|
||||
Cum le setezi:
|
||||
- **CLI/chat:** `/p <slug> --repo <name> --branch <feature> [--base-branch <name>] <descriere>` (parser în `_ralph_propose` la `src/router.py`).
|
||||
- **Dashboard:** modal Propose → secțiunea „Avansat" cu câmpuri pentru repo/branch/base_branch.
|
||||
|
||||
Night-execute (`cron/jobs.json`) detectează câmpurile și clonează `repo` în `~/workspace/<slug>/`, apoi `git checkout -b <branch> <base_branch>` dacă `branch` e setat. Dacă clone-ul eșuează (repo inexistent), proiectul e marcat `failed` fără să mai pornească ralph.
|
||||
|
||||
### Approval guard — protejare împotriva re-planning accidental
|
||||
|
||||
`/plan/start` (POST `/api/projects/<slug>/plan/start`) refuză cu 409 `already_committed` dacă proiectul e deja `approved`/`running`/`complete`. Pentru a re-iniția planning-ul intenționat:
|
||||
|
||||
- **Dashboard:** butonul „Re-planifică" pe cards aprobate cere confirm explicit înainte să trimită `force=true` în body.
|
||||
- **API direct:** trimite `{"force": true, "description": "..."}` în body-ul de la `/plan/start`.
|
||||
|
||||
Asta previne situația în care un click accidental pe „Planifică" șterge `status=approved` și pornește un nou subprocess Claude (cu cost asociat).
|
||||
|
||||
## Convenție import-uri
|
||||
|
||||
Import-uri absolute via `sys.path.insert(0, PROJECT_ROOT)`: `from src.config import ...`, `from src.adapters.discord_bot import ...`. Fără import-uri circulare.
|
||||
|
||||
## Fișiere cheie
|
||||
|
||||
| Path | Rol |
|
||||
|------|-----|
|
||||
| `src/main.py` | Entry point — adaptoare + scheduler + heartbeat |
|
||||
| `src/router.py` | Comenzi vs mesaje Claude |
|
||||
| `src/claude_session.py` | Wrapper Claude CLI cu `--resume` |
|
||||
| `src/credential_store.py` | Secrete keyring |
|
||||
| `cli.py` | Diagnostice CLI (eco) |
|
||||
| `config.json` | Config runtime |
|
||||
| `bridge/whatsapp/index.js` | Bridge Baileys + Express, port 8098 |
|
||||
| `personality/*.md` | System prompt — cine ești |
|
||||
| `memory/` | Knowledge base — embeddings + SQLite (în repo, nu symlink) |
|
||||
| `dashboard/api.py` | Task Board HTTP API (port 8088) |
|
||||
| `dashboard/handlers/` | Mixin-uri endpoints (git, cron, habits, eco, files, pdf, workspace, youtube, projects, ralph, auth) |
|
||||
| `dashboard/handlers/projects.py` | Endpoints unificate proiecte: `/api/projects`, `/propose`, `/approve`, `/unapprove`, `/cancel`, `/<slug>/plan/*`, `/stream` (SSE) |
|
||||
| `dashboard/handlers/auth.py` | Login/logout cu cookie httpOnly `dashboard=<token>`; `DASHBOARD_TOKEN` din `.env` |
|
||||
| `dashboard/handlers/_validators.py` | Validatori slug/descriere partajați. Slug regex: `^[a-z0-9][a-z0-9\-_]{1,38}[a-z0-9]$` (permite hifene ȘI underscore) |
|
||||
| `dashboard/static/tokens.css` | Design tokens CSS (`--color-*`, `--space-*`, etc.) — shared variables pentru toate paginile |
|
||||
| `dashboard/DESIGN.md` | Design system source-of-truth: tokens, componente, regula no-emoji |
|
||||
| `dashboard/constants.py` | Path-uri centralizate + config Gitea pentru dashboard |
|
||||
| `dashboard/echo-taskboard.service` | Template systemd user unit |
|
||||
| `src/jsonlock.py` | Flock helper pentru scrieri concurente: `read_locked(path)`, `write_locked(path, mutator)`, `LockTimeoutError`. Sidecar `<path>.lock` (inode stabil). Re-entrant per thread. Ordine canonică: alfabetic |
|
||||
| `src/approved_tasks_cli.py` | CLI wrapper pentru shell scripts: scrie în `approved-tasks.json` prin jsonlock. Usage: `python3 -m src.approved_tasks_cli set-status --slug X --status Y` |
|
||||
| `cron/jobs.json` | Job-uri APScheduler (schemă plată, Europe/Bucharest) |
|
||||
| `approved-tasks.json` | Fișier coordonare Ralph — status proiecte autonome (extins cu `planning_session_id`, `final_plan_path`) |
|
||||
| `tasks/lessons.md` | Lecții capturate din corectările lui Marius (citit la session start) |
|
||||
| `tasks/spike-planning-findings.md` | Validare empirică Spike Step 0 (subprocess `claude -p` + skills gstack + `--resume` round-trip) |
|
||||
| `prompts/planning_agent.md` | System prompt pentru planning agent multi-fază (W2) |
|
||||
| `src/ralph_flow.py` | State per `(adapter, chat, user)` pentru UX flow (TTL 10min) |
|
||||
| `src/planning_session.py` | Wrapper Claude subprocess pentru planning agent |
|
||||
| `src/planning_orchestrator.py` | Orchestrare faze gstack skills (W2) |
|
||||
| `src/adapters/discord_views.py` | Discord Views/Modal pentru UX interactiv (W1) |
|
||||
| `tools/ralph/ralph.sh` | Bash loop DAG-aware (W3): N iter × claude CLI per story |
|
||||
| `tools/ralph_dag.py` | DAG helpers + CLI (W3) |
|
||||
| `tools/ralph_prd_generator.py` | Generează PRD + prd.json cu Opus |
|
||||
|
||||
## gstack
|
||||
|
||||
Folosește skill-ul `/browse` din gstack pentru orice navigare web. Nu folosi tool-uri `mcp__claude-in-chrome__*`.
|
||||
|
||||
Skill-uri disponibile:
|
||||
- `/office-hours`
|
||||
- `/plan-ceo-review`
|
||||
- `/plan-eng-review`
|
||||
- `/plan-design-review`
|
||||
- `/design-consultation`
|
||||
- `/design-shotgun`
|
||||
- `/design-html`
|
||||
- `/review`
|
||||
- `/ship`
|
||||
- `/land-and-deploy`
|
||||
- `/canary`
|
||||
- `/benchmark`
|
||||
- `/browse`
|
||||
- `/connect-chrome`
|
||||
- `/qa`
|
||||
- `/qa-only`
|
||||
- `/design-review`
|
||||
- `/setup-browser-cookies`
|
||||
- `/setup-deploy`
|
||||
- `/retro`
|
||||
- `/investigate`
|
||||
- `/document-release`
|
||||
- `/codex`
|
||||
- `/cso`
|
||||
- `/autoplan`
|
||||
- `/plan-devex-review`
|
||||
- `/devex-review`
|
||||
- `/careful`
|
||||
- `/freeze`
|
||||
- `/guard`
|
||||
- `/unfreeze`
|
||||
- `/gstack-upgrade`
|
||||
- `/learn`
|
||||
|
||||
244
MIGRATION-PLAYBOOK.md
Normal file
244
MIGRATION-PLAYBOOK.md
Normal file
@@ -0,0 +1,244 @@
|
||||
# OpenClaw → Echo-Core Migration Playbook
|
||||
|
||||
> **Status: EXECUTED 2026-04-21 10:04 UTC.** See "Post-migration state" below.
|
||||
> This playbook is kept as a living reference for rollback + cleanup.
|
||||
|
||||
Run this after the PR `feat/openclaw-consolidation-2026-04` has merged to master.
|
||||
Estimated downtime: 5–10 minutes. Rollback path at the bottom.
|
||||
|
||||
---
|
||||
|
||||
## Post-migration state (reference for rollback)
|
||||
|
||||
**Migration date:** 2026-04-21 10:04 UTC
|
||||
**Downtime window:** ~2 minutes (10:02–10:04 UTC)
|
||||
|
||||
### Git SHAs
|
||||
- **Pre-migration master tip:** `4e78ef7` ("claude gstack") — rollback target if everything goes wrong.
|
||||
- **Post-merge master tip:** `d741541` ("test(dashboard): cover constants, git helper, cron endpoint, files sandbox") — last commit of the migration PR.
|
||||
- **Current origin/master:** moved forward as Marius tested dashboard commit button post-cutover.
|
||||
|
||||
### Backups
|
||||
- **`/home/moltbot/clawd-backup-2026-04-21/`** — full copy of `clawd/dashboard` (807K) + `clawd/memory` (11M) taken during pre-flight step 3. Keep until 2026-05-21.
|
||||
|
||||
### Services at cutover
|
||||
- `echo-core.service` — active (running), uses echo-core/.venv + echo-core paths
|
||||
- `echo-taskboard.service` — active (running), new unit at `~/.config/systemd/user/echo-taskboard.service`, WorkingDirectory=`/home/moltbot/echo-core/dashboard`, ExecStart=`.venv/bin/python3 api.py`
|
||||
- `echo-whatsapp-bridge.service` — active (running), unchanged
|
||||
- `openclaw-gateway.service` — **inactive + disabled**, credentials stripped
|
||||
|
||||
### Credentials stripped from `~/.openclaw/`
|
||||
- `credentials/` dir (Discord + Telegram + WhatsApp pairing) — deleted
|
||||
- `identity/` dir (device auth) — deleted
|
||||
- `devices/` dir (paired devices) — deleted
|
||||
- `agents/*/agent/auth-profiles.json` (20 files) — deleted
|
||||
- `agents/*/sessions/sessions.json` (20 files) — deleted
|
||||
- **Preserved:** `cron/jobs.json` (+bak) as audit artifact; `openclaw.json` (main config, no known secrets); npm `lib/` (harmless).
|
||||
|
||||
### What's enabled after migration
|
||||
- **Shell jobs (5):** `anaf-monitor`, `security-audit-daily`, `kb-index-refresh`, `archive-tasks-daily`, `backup-config` — all enabled
|
||||
- **Claude jobs enabled (2):** `newsletter-test`, `heartbeat-2h`
|
||||
- **Claude jobs disabled (13):** morning-report, evening-report, morning-coaching, evening-coaching, weekly-planning-sun, content-discovery, provocare-reminder, exercise-snack-1/2/3, grup-sprijin-5feb, grup-sprijin-pregatire — ready to enable after Marius reviews each
|
||||
|
||||
### Crontab
|
||||
- `0 2 * * * /home/moltbot/echo-core/tools/backup_config.sh` (was clawd)
|
||||
- `10 14 * * 4,5,1 ... check_newsletter_cercetasi.py` (unchanged)
|
||||
- `0 9 21 5 * ...` — **May 21 2026 cleanup reminder** (writes to `$HOME/REMINDER-openclaw-cleanup.txt` and appends to `logs/migration-reminder.log`)
|
||||
|
||||
### Verification PASS
|
||||
- ANAF `status.json.anaf.lastCheck` moved from **03 Apr 2026, 22:07** → **21 Apr 2026, 10:04** with 3 real changes detected on first manual trigger.
|
||||
- `GSTACK-CRON: changes=3` marker emitted correctly; scheduler↔anaf contract verified.
|
||||
- `/api/cron` returns 7 jobs (enabled shell + claude).
|
||||
- `/api/agents` and `/api/activity` return 404 (removed as planned).
|
||||
- Dashboard /api/status OK.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Pre-flight (read-only)
|
||||
|
||||
1. Confirm clean git state on echo-core master:
|
||||
```
|
||||
cd /home/moltbot/echo-core && git status
|
||||
```
|
||||
|
||||
2. Verify tests pass:
|
||||
```
|
||||
cd /home/moltbot/echo-core
|
||||
source .venv/bin/activate && pytest tests/ -x
|
||||
```
|
||||
|
||||
3. Backup:
|
||||
```
|
||||
cp -rp /home/moltbot/clawd/dashboard /home/moltbot/clawd-backup-$(date +%Y-%m-%d)/
|
||||
cp -rp /home/moltbot/clawd/memory /home/moltbot/clawd-backup-$(date +%Y-%m-%d)/memory
|
||||
```
|
||||
|
||||
## Legacy consumer grep (decide on compat symlink)
|
||||
|
||||
4. Check whether anything still reads clawd/memory:
|
||||
```
|
||||
grep -rn 'clawd/memory' /home/moltbot/{bin,.config,.openclaw} 2>/dev/null
|
||||
grep -rn 'clawd/memory' /home/moltbot/echo-core 2>/dev/null
|
||||
```
|
||||
- If empty → skip **step 11** (no compat symlink needed).
|
||||
- If non-empty → keep **step 11**.
|
||||
|
||||
## Stop services
|
||||
|
||||
5. ```
|
||||
systemctl --user stop echo-core echo-taskboard echo-whatsapp-bridge openclaw-gateway
|
||||
```
|
||||
|
||||
## Copy ANAF live state
|
||||
|
||||
6. ```
|
||||
cp -rp /home/moltbot/clawd/tools/anaf-monitor/{hashes.json,versions.json,monitor.log} \
|
||||
/home/moltbot/echo-core/tools/anaf-monitor/ 2>/dev/null
|
||||
cp -rp /home/moltbot/clawd/tools/anaf-monitor/snapshots \
|
||||
/home/moltbot/echo-core/tools/anaf-monitor/
|
||||
diff -r /home/moltbot/clawd/tools/anaf-monitor/snapshots \
|
||||
/home/moltbot/echo-core/tools/anaf-monitor/snapshots
|
||||
```
|
||||
Diff should be empty (or only show new snapshots echo-core captured during testing).
|
||||
|
||||
## Dashboard migration
|
||||
|
||||
7. Delete echo-core dashboard placeholder content if any collisions, then:
|
||||
```
|
||||
cp -rp /home/moltbot/clawd/dashboard/{habits,issues,status,todos}.json \
|
||||
/home/moltbot/echo-core/dashboard/
|
||||
cp -rp /home/moltbot/clawd/dashboard/tests/ \
|
||||
/home/moltbot/echo-core/dashboard/tests/
|
||||
# Recreate the 4 dashboard symlinks pointing into echo-core:
|
||||
ln -sfn /home/moltbot/echo-core/memory /home/moltbot/echo-core/dashboard/memory
|
||||
ln -sfn /home/moltbot/echo-core/conversations /home/moltbot/echo-core/dashboard/conversations # create conversations/ first if you want this
|
||||
ln -sfn /home/moltbot/echo-core/memory/kb /home/moltbot/echo-core/dashboard/notes-data
|
||||
ln -sfn /home/moltbot/echo-core/memory/kb/youtube /home/moltbot/echo-core/dashboard/youtube-notes
|
||||
```
|
||||
|
||||
## Memory inversion
|
||||
|
||||
8. `rm /home/moltbot/echo-core/memory` *(removes symlink only, not target)*
|
||||
9. `cp -rp /home/moltbot/clawd/memory /home/moltbot/echo-core/memory`
|
||||
10. `diff -rq /home/moltbot/clawd/memory /home/moltbot/echo-core/memory` *(verify identical)*
|
||||
11. *(only if step 4 found consumers)*
|
||||
```
|
||||
mv /home/moltbot/clawd/memory /home/moltbot/clawd/memory.old-2026-04
|
||||
ln -s /home/moltbot/echo-core/memory /home/moltbot/clawd/memory
|
||||
```
|
||||
12. `rm -rf /home/moltbot/echo-core/memory.bak` *(leftover, safe to delete)*
|
||||
|
||||
## Systemd
|
||||
|
||||
13. Copy the template into place:
|
||||
```
|
||||
cp /home/moltbot/echo-core/dashboard/echo-taskboard.service \
|
||||
/home/moltbot/.config/systemd/user/echo-taskboard.service
|
||||
systemctl --user daemon-reload
|
||||
```
|
||||
|
||||
## Crontab
|
||||
|
||||
14. ```
|
||||
bash /home/moltbot/echo-core/scripts/update_crontab.sh
|
||||
```
|
||||
|
||||
## Decommission OpenClaw
|
||||
|
||||
15. `systemctl --user stop openclaw-gateway`
|
||||
16. `systemctl --user disable openclaw-gateway`
|
||||
17. Strip credentials from `~/.openclaw/` but keep `jobs.json.bak`:
|
||||
```
|
||||
cd /home/moltbot/.openclaw
|
||||
find . -name 'auth*' -o -name '*token*' -o -name '*.secret' | xargs rm -v 2>/dev/null
|
||||
ls -la agents/*/ # inspect for any remaining secrets, delete manually
|
||||
```
|
||||
18. **Note:** schedule a reminder for 2026-05-21 to `rm -rf /home/moltbot/.openclaw`
|
||||
entirely if nothing was restored.
|
||||
|
||||
## Restart
|
||||
|
||||
19. `systemctl --user start echo-core echo-taskboard echo-whatsapp-bridge`
|
||||
20. `systemctl --user status echo-core echo-taskboard echo-whatsapp-bridge` — all **active (running)**.
|
||||
21. `systemctl --user status openclaw-gateway` — **inactive (dead)**.
|
||||
|
||||
## Verification
|
||||
|
||||
22. `curl -s http://localhost:8088/api/status` → `{"status":"ok",...}`
|
||||
23. Visit `https://moltbot.tailf7372d.ts.net/echo/` — home page loads.
|
||||
24. `/api/cron` panel populated with echo-core jobs (anaf-monitor, morning-report, etc).
|
||||
25. `/api/agents` returns 404 (removed).
|
||||
26. Click **Commit** in `index.html` — creates commit on echo-core repo.
|
||||
27. Manually trigger anaf monitor:
|
||||
```
|
||||
cd /home/moltbot/echo-core && .venv/bin/python3 tools/anaf-monitor/monitor_v2.py
|
||||
```
|
||||
Verify `status.json` updates **and** stdout ends with
|
||||
`GSTACK-CRON: changes=N`.
|
||||
28. Wait for first scheduled anaf-monitor trigger (10:00 or 16:00 Mon-Fri).
|
||||
Check `echo-core.log` for execution.
|
||||
|
||||
---
|
||||
|
||||
## Rollback path (if anything breaks badly)
|
||||
|
||||
**Concrete values for this migration (executed 2026-04-21):**
|
||||
|
||||
```
|
||||
# Stop current services
|
||||
systemctl --user stop echo-core echo-taskboard echo-whatsapp-bridge
|
||||
|
||||
# Restore memory directory
|
||||
rm -rf /home/moltbot/echo-core/memory
|
||||
cp -rp /home/moltbot/clawd-backup-2026-04-21/memory /home/moltbot/clawd/memory
|
||||
ln -s /home/moltbot/clawd/memory /home/moltbot/echo-core/memory
|
||||
|
||||
# Restore dashboard source (symlinks will come back with it)
|
||||
cp -rp /home/moltbot/clawd-backup-2026-04-21/dashboard /home/moltbot/clawd/dashboard
|
||||
|
||||
# Restore old systemd unit (paths back to clawd/dashboard + /usr/bin/python3)
|
||||
cat > ~/.config/systemd/user/echo-taskboard.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Echo Task Board API
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/moltbot/clawd/dashboard
|
||||
ExecStart=/usr/bin/python3 /home/moltbot/clawd/dashboard/api.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
|
||||
# Revert git to pre-migration state
|
||||
git -C /home/moltbot/echo-core reset --hard 4e78ef7
|
||||
|
||||
# Restore crontab backup_config line (sed in reverse)
|
||||
crontab -l | sed -E 's#/home/moltbot/echo-core/tools/backup_config\.sh#/home/moltbot/clawd/tools/backup_config.sh#g' | crontab -
|
||||
|
||||
# Re-enable openclaw (credentials are GONE — you'll need to re-pair Discord/Telegram/WhatsApp)
|
||||
systemctl --user enable openclaw-gateway
|
||||
systemctl --user daemon-reload
|
||||
|
||||
# Restart everything
|
||||
systemctl --user start echo-core echo-taskboard echo-whatsapp-bridge openclaw-gateway
|
||||
```
|
||||
|
||||
**Note:** After rollback, OpenClaw credentials are gone (stripped during migration). Re-pairing requires going through OpenClaw's pairing flows for Discord/Telegram/WhatsApp. If you want clean rollback without losing pairing, do the rollback within the 30-day window **before** running the May 21 cleanup reminder.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Cron schedules are Bucharest local time**, not UTC.
|
||||
- **Most imported claude jobs arrive DISABLED** — enable them via `eco` / dashboard
|
||||
once you've verified each one produces the expected output.
|
||||
- `heartbeat-2h` is the **only imported claude job that stays enabled** (preserving
|
||||
its state from OpenClaw).
|
||||
- The 5 shell jobs (anaf-monitor, security-audit-daily, kb-index-refresh,
|
||||
archive-tasks-daily, backup-config) start **enabled** on day one.
|
||||
34
TODOS.md
Normal file
34
TODOS.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# TODOS — Echo Core deferred work
|
||||
|
||||
Captured during planning reviews. Re-evaluate after relevant features ship or dogfood data accumulates.
|
||||
|
||||
## Voice
|
||||
|
||||
### Bounded SSRC buffer for DAVE-active unknown-SSRC race
|
||||
|
||||
**What:** Replace the hard-drop of unknown-SSRC RTP packets in `_maybe_dave_decrypt` (vendor/discord-ext-voice-recv/.../reader.py) with a small bounded buffer per SSRC. Flush on SPEAKING event mapping the SSRC → user_id, then DAVE-decrypt and feed downstream.
|
||||
|
||||
**Why:** voice-recv vanilla feeds unknown-SSRC packets to opus decoder anyway (reader.py:178 logs `info` but still calls `feed_rtp`). The DAVE patch turns this into a hard drop because davey requires `user_id`. Net regression: 40-200ms (1-5 packets) lost on the FIRST utterance of each new speaker per session, when audio races ahead of SPEAKING event. Subsequent utterances unaffected.
|
||||
|
||||
**Pros:** Eliminates first-utterance audio loss. Whisper STT gets the complete prefix ("Echo, cât e ceasul?" instead of possibly "co, cât e ceasul?").
|
||||
|
||||
**Cons:** New state machine — queue per SSRC, TTL flush (~2s), ordering preservation, memory bound. New race surface between socket-reader thread (queueing) and asyncio loop (SPEAKING event → flush). 50 packets * ~1KB * N concurrent unknown SSRCs = memory footprint. Bug risk traded for UX win.
|
||||
|
||||
**Context:** Discovered during /plan-eng-review on `/home/moltbot/.claude/plans/wiggly-exploring-glade.md` (DAVE receive-side decrypt patch). Outside-voice reviewer flagged this as a regression vs voice-recv vanilla behavior. Accepted as tradeoff for v1 because SPEAKING typically arrives before audio in normal Discord flow — impact may be rare. **Depends on:** dogfood data from Pas 12 Etapa 2 #3-#13 confirming this IS observed in practice (i.e., Whisper transcripts repeatedly missing first word). If not observed, this TODO stays permanent. If observed in 3+ sessions, escalate.
|
||||
|
||||
**Where to start:** `_maybe_dave_decrypt` in `vendor/discord-ext-voice-recv/discord/ext/voice_recv/reader.py`. Add `_pending_packets: dict[ssrc, deque[bytes]]` on `AudioReader`. Hook SPEAKING event handler in voice_client.py to call new `flush_pending(ssrc, user_id)` method.
|
||||
|
||||
**Depends on / blocked by:** Pas 12 dogfood data. Re-evaluate after 3+ sessions of live use.
|
||||
|
||||
---
|
||||
|
||||
## (Other deferred items from voice review — already in plan's "Out of scope" section)
|
||||
|
||||
- Wake-word "Echo" cu porcupine (P3 — incompatible with /voice join continuous)
|
||||
- Telegram voice memo bidirectional (P2 — reuses src/voice/pipeline.py)
|
||||
- Full-session WAV recording (P3 — KB transcript sufficient v1)
|
||||
- Upstreaming the DAVE patch to imayhaveborkedit/discord-ext-voice-recv (separate community effort)
|
||||
- `threading.Lock` around davey.decrypt (conditional follow-up — only if dogfood reveals crashes)
|
||||
- DAVE verification UI (`voice_privacy_code`, pairwise fingerprints — useful but not blocking voice-to-voice)
|
||||
- Video E2E decrypt (Echo is audio-only, no video pipeline)
|
||||
- Pre-existent test failures: TestPromptInjectionProtection × 2 + TestOnMessage × 4 (separate ticket)
|
||||
0
approved-tasks.json.lock
Normal file
0
approved-tasks.json.lock
Normal file
BIN
assets/voice/beep_200ms.wav
Normal file
BIN
assets/voice/beep_200ms.wav
Normal file
Binary file not shown.
BIN
assets/voice/mhm.wav
Normal file
BIN
assets/voice/mhm.wav
Normal file
Binary file not shown.
BIN
assets/voice/thinking.wav
Normal file
BIN
assets/voice/thinking.wav
Normal file
Binary file not shown.
@@ -17,6 +17,7 @@ let sock = null;
|
||||
let connected = false;
|
||||
let phoneNumber = null;
|
||||
let currentQR = null;
|
||||
let currentPairingCode = null;
|
||||
let reconnectAttempts = 0;
|
||||
let messageQueue = [];
|
||||
let shuttingDown = false;
|
||||
@@ -112,7 +113,7 @@ async function startConnection() {
|
||||
// --- Express API ---
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
|
||||
app.get('/status', (_req, res) => {
|
||||
res.json({
|
||||
@@ -122,6 +123,34 @@ app.get('/status', (_req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/pair', async (req, res) => {
|
||||
if (connected) {
|
||||
return res.json({ error: 'already connected' });
|
||||
}
|
||||
const { phone } = req.body || {};
|
||||
if (!phone) {
|
||||
return res.status(400).json({ error: 'missing "phone" in body' });
|
||||
}
|
||||
if (!sock) {
|
||||
return res.status(503).json({ error: 'socket not ready yet, try again in a few seconds' });
|
||||
}
|
||||
try {
|
||||
const code = await sock.requestPairingCode(phone.replace(/\D/g, ''));
|
||||
currentPairingCode = code;
|
||||
console.log(`[whatsapp] Pairing code for ${phone}: ${code}`);
|
||||
res.json({ ok: true, code });
|
||||
} catch (err) {
|
||||
console.error('[whatsapp] Pairing code error:', err.message);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/pair-code', (_req, res) => {
|
||||
if (connected) return res.json({ error: 'already connected' });
|
||||
if (!currentPairingCode) return res.json({ error: 'no pairing code yet — POST /pair first' });
|
||||
res.json({ code: currentPairingCode });
|
||||
});
|
||||
|
||||
app.get('/qr', (_req, res) => {
|
||||
if (connected) {
|
||||
return res.json({ error: 'already connected' });
|
||||
@@ -158,6 +187,29 @@ app.post('/send', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/send-document', async (req, res) => {
|
||||
const { to, filename, mimetype, data_base64, caption } = req.body || {};
|
||||
if (!to || !filename || !data_base64) {
|
||||
return res.status(400).json({ ok: false, error: 'missing "to", "filename", or "data_base64"' });
|
||||
}
|
||||
if (!connected || !sock) {
|
||||
return res.status(503).json({ ok: false, error: 'not connected to WhatsApp' });
|
||||
}
|
||||
try {
|
||||
const buffer = Buffer.from(data_base64, 'base64');
|
||||
const result = await sock.sendMessage(to, {
|
||||
document: buffer,
|
||||
fileName: filename,
|
||||
mimetype: mimetype || 'application/octet-stream',
|
||||
caption: caption || '',
|
||||
});
|
||||
res.json({ ok: true, id: result.key.id });
|
||||
} catch (err) {
|
||||
console.error('[whatsapp] Send document failed:', err.message);
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/react', async (req, res) => {
|
||||
const { to, id, emoji, fromMe, participant } = req.body || {};
|
||||
|
||||
|
||||
157
cli.py
157
cli.py
@@ -114,6 +114,104 @@ def _load_sessions_file() -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def _voice_doctor_checks() -> list[tuple[str, bool]]:
|
||||
"""Voice-stack health checks (Pas 10).
|
||||
|
||||
Mirrors the logic in tools/voice_setup.py but returns (label, ok) tuples
|
||||
so they integrate with cmd_doctor's PASS/FAIL output. All checks degrade
|
||||
gracefully — ImportError on optional voice deps is reported as FAIL, never
|
||||
raised, so the rest of `eco doctor` is unaffected.
|
||||
"""
|
||||
import importlib.util
|
||||
import json as _json
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
results: list[tuple[str, bool]] = []
|
||||
|
||||
# 1. libopus0 loaded by discord.py
|
||||
try:
|
||||
import discord
|
||||
if not discord.opus.is_loaded():
|
||||
try:
|
||||
discord.opus._load_default()
|
||||
except Exception:
|
||||
pass
|
||||
results.append(("libopus loaded (discord.py)", discord.opus.is_loaded()))
|
||||
except ImportError:
|
||||
results.append(("libopus loaded (discord.py)", False))
|
||||
except Exception:
|
||||
results.append(("libopus loaded (discord.py)", False))
|
||||
|
||||
# 2. ffmpeg in PATH
|
||||
results.append(("ffmpeg in PATH", shutil.which("ffmpeg") is not None))
|
||||
|
||||
# 3. Supertonic TTS reachable at http://127.0.0.1:7788/
|
||||
supertonic_url = "http://127.0.0.1:7788/v1/audio/speech"
|
||||
supertonic_ok = False
|
||||
try:
|
||||
payload = _json.dumps({
|
||||
"model": "supertonic-3",
|
||||
"input": "test",
|
||||
"voice": "M2",
|
||||
"response_format": "wav",
|
||||
"lang": "ro",
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
supertonic_url,
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
supertonic_ok = resp.status == 200
|
||||
except (urllib.error.URLError, ConnectionError, OSError):
|
||||
supertonic_ok = False
|
||||
except Exception:
|
||||
supertonic_ok = False
|
||||
results.append(("Supertonic TTS reachable at :7788", supertonic_ok))
|
||||
|
||||
# 4. faster-whisper importable (don't load model — too slow)
|
||||
results.append((
|
||||
"faster-whisper importable",
|
||||
importlib.util.find_spec("faster_whisper") is not None,
|
||||
))
|
||||
|
||||
# 5. silero-vad importable
|
||||
results.append((
|
||||
"silero-vad importable",
|
||||
importlib.util.find_spec("silero_vad") is not None,
|
||||
))
|
||||
|
||||
# 6. discord.ext.voice_recv importable (vendor package)
|
||||
voice_recv_ok = False
|
||||
try:
|
||||
voice_recv_ok = importlib.util.find_spec("discord.ext.voice_recv") is not None
|
||||
except (ImportError, ValueError, ModuleNotFoundError):
|
||||
voice_recv_ok = False
|
||||
except Exception:
|
||||
voice_recv_ok = False
|
||||
results.append(("discord.ext.voice_recv importable", voice_recv_ok))
|
||||
|
||||
# 7-9. Voice assets present and non-trivial size
|
||||
voice_assets = [
|
||||
("assets/voice/thinking.wav", 1024),
|
||||
("assets/voice/beep_200ms.wav", 512),
|
||||
("assets/voice/mhm.wav", 512),
|
||||
]
|
||||
for rel_path, min_bytes in voice_assets:
|
||||
path = PROJECT_ROOT / rel_path
|
||||
ok = False
|
||||
try:
|
||||
ok = path.exists() and path.stat().st_size > min_bytes
|
||||
except OSError:
|
||||
ok = False
|
||||
label = f"{rel_path} (>{min_bytes}B)"
|
||||
results.append((label, ok))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def cmd_doctor(args):
|
||||
"""Run diagnostic checks."""
|
||||
import re
|
||||
@@ -227,6 +325,9 @@ def cmd_doctor(args):
|
||||
else:
|
||||
checks.append(("WhatsApp bridge (optional)", True))
|
||||
|
||||
# ---- Voice stack checks (Pas 10) ----
|
||||
checks.extend(_voice_doctor_checks())
|
||||
|
||||
# Print results
|
||||
all_pass = True
|
||||
for label, passed in checks:
|
||||
@@ -580,6 +681,51 @@ def cmd_whatsapp(args):
|
||||
_whatsapp_qr()
|
||||
|
||||
|
||||
def cmd_openrouter(args):
|
||||
"""Handle openrouter subcommand."""
|
||||
semaphore = PROJECT_ROOT / ".use_openrouter"
|
||||
|
||||
if args.openrouter_action == "on":
|
||||
env_file = Path.home() / ".claude-env.sh"
|
||||
if not env_file.exists():
|
||||
print(f"Error: {env_file} not found")
|
||||
sys.exit(1)
|
||||
# Verify required vars exist in file
|
||||
text = env_file.read_text()
|
||||
if "ANTHROPIC_BASE_URL" not in text or "OPENROUTER_API_KEY" not in text:
|
||||
print(f"Warning: {env_file} may be missing ANTHROPIC_BASE_URL or OPENROUTER_API_KEY")
|
||||
|
||||
semaphore.write_text("# OpenRouter mode enabled\n")
|
||||
print("OpenRouter mode: ENABLED")
|
||||
print("Restart echo-core for changes to take effect:")
|
||||
print(" systemctl --user restart echo-core")
|
||||
|
||||
elif args.openrouter_action == "off":
|
||||
if semaphore.exists():
|
||||
semaphore.unlink()
|
||||
print("OpenRouter mode: DISABLED")
|
||||
print("Restart echo-core to use Anthropic API:")
|
||||
print(" systemctl --user restart echo-core")
|
||||
else:
|
||||
print("OpenRouter mode: already disabled")
|
||||
|
||||
elif args.openrouter_action == "status":
|
||||
status = "ENABLED" if semaphore.exists() else "DISABLED"
|
||||
print(f"OpenRouter mode: {status}")
|
||||
if semaphore.exists():
|
||||
print(f"Semafor: {semaphore}")
|
||||
env_file = Path.home() / ".claude-env.sh"
|
||||
if env_file.exists():
|
||||
# Show which vars will be loaded
|
||||
print(f"Env file: {env_file}")
|
||||
text = env_file.read_text()
|
||||
for line in text.splitlines():
|
||||
if line.strip().startswith("export ") and not line.strip().startswith("#"):
|
||||
var_name = line.strip()[7:].split("=")[0] if "=" in line else "?"
|
||||
if any(x in var_name for x in ["ANTHROPIC", "OPENROUTER"]):
|
||||
print(f" {var_name}=***")
|
||||
|
||||
|
||||
def _whatsapp_status():
|
||||
"""Check WhatsApp bridge connection status."""
|
||||
import urllib.request
|
||||
@@ -806,6 +952,14 @@ def main():
|
||||
whatsapp_sub.add_parser("status", help="Check bridge connection status")
|
||||
whatsapp_sub.add_parser("qr", help="Show QR code instructions")
|
||||
|
||||
# openrouter
|
||||
openrouter_parser = sub.add_parser("openrouter", help="Toggle OpenRouter mode")
|
||||
openrouter_sub = openrouter_parser.add_subparsers(dest="openrouter_action")
|
||||
|
||||
openrouter_sub.add_parser("on", help="Enable OpenRouter mode")
|
||||
openrouter_sub.add_parser("off", help="Disable OpenRouter mode")
|
||||
openrouter_sub.add_parser("status", help="Check OpenRouter status")
|
||||
|
||||
# Parse and dispatch
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -839,6 +993,9 @@ def main():
|
||||
"whatsapp": lambda a: (
|
||||
cmd_whatsapp(a) if a.whatsapp_action else (whatsapp_parser.print_help() or sys.exit(0))
|
||||
),
|
||||
"openrouter": lambda a: (
|
||||
cmd_openrouter(a) if a.openrouter_action else (openrouter_parser.print_help() or sys.exit(0))
|
||||
),
|
||||
}
|
||||
|
||||
handler = dispatch.get(args.command)
|
||||
|
||||
101
config.json
101
config.json
@@ -1,14 +1,28 @@
|
||||
{
|
||||
"bot": {
|
||||
"name": "Echo",
|
||||
"default_model": "opus",
|
||||
"default_model": "sonnet",
|
||||
"owner": "949388626146517022",
|
||||
"admins": ["5040014994"]
|
||||
"admins": [
|
||||
"5040014994"
|
||||
]
|
||||
},
|
||||
"channels": {
|
||||
"echo-core": {
|
||||
"id": "1471916752119009432",
|
||||
"default_model": "opus"
|
||||
"default_model": "sonnet"
|
||||
},
|
||||
"echo-work": {
|
||||
"id": "1466726254312030259",
|
||||
"default_model": "sonnet"
|
||||
},
|
||||
"echo-sprijin": {
|
||||
"id": "1466739361503772864",
|
||||
"default_model": "sonnet"
|
||||
},
|
||||
"echo-self": {
|
||||
"id": "1466739112747864175",
|
||||
"default_model": "sonnet"
|
||||
}
|
||||
},
|
||||
"telegram_channels": {},
|
||||
@@ -21,53 +35,82 @@
|
||||
"whatsapp_channels": {
|
||||
"echo-test": {
|
||||
"id": "120363424350922235@g.us",
|
||||
"default_model": "opus"
|
||||
"default_model": "sonnet"
|
||||
}
|
||||
},
|
||||
"heartbeat": {
|
||||
"enabled": true,
|
||||
"interval_minutes": 30,
|
||||
"enabled": false,
|
||||
"interval_minutes": 120,
|
||||
"channel": "echo-core",
|
||||
"model": "haiku",
|
||||
"quiet_hours": [23, 8],
|
||||
"quiet_hours": [
|
||||
23,
|
||||
7
|
||||
],
|
||||
"checks": {
|
||||
"email": true,
|
||||
"calendar": true,
|
||||
"kb_index": true,
|
||||
"git": true
|
||||
"git": false
|
||||
},
|
||||
"cooldowns": {
|
||||
"email": 1800,
|
||||
"calendar": 0,
|
||||
"calendar": 1800,
|
||||
"kb_index": 14400,
|
||||
"git": 14400
|
||||
}
|
||||
},
|
||||
"newsletter_cercetasi": {
|
||||
"enabled": true,
|
||||
"cron": "0 17 * * 4,5,1",
|
||||
"channel": "echo-core"
|
||||
},
|
||||
"allowed_tools": [
|
||||
"Read", "Edit", "Write", "Glob", "Grep",
|
||||
"WebFetch", "WebSearch",
|
||||
"Bash(python3 *)", "Bash(.venv/bin/python3 *)",
|
||||
"Bash(pip *)", "Bash(pytest *)",
|
||||
"Bash(git add *)", "Bash(git commit *)",
|
||||
"Bash(git push)", "Bash(git push *)",
|
||||
"Bash(git pull)", "Bash(git pull *)",
|
||||
"Bash(git status)", "Bash(git status *)",
|
||||
"Bash(git diff)", "Bash(git diff *)",
|
||||
"Bash(git log)", "Bash(git log *)",
|
||||
"Bash(git checkout *)",
|
||||
"Bash(git branch)", "Bash(git branch *)",
|
||||
"Bash(git stash)", "Bash(git stash *)",
|
||||
"Bash(npm *)", "Bash(node *)", "Bash(npx *)",
|
||||
"Read",
|
||||
"Edit",
|
||||
"Write",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"WebFetch",
|
||||
"WebSearch",
|
||||
"Bash(python3 *)",
|
||||
"Bash(.venv/bin/python3 *)",
|
||||
"Bash(pip *)",
|
||||
"Bash(pytest *)",
|
||||
"Bash(git *)",
|
||||
"Bash(npm *)",
|
||||
"Bash(node *)",
|
||||
"Bash(npx *)",
|
||||
"Bash(systemctl --user *)",
|
||||
"Bash(trash *)", "Bash(mkdir *)", "Bash(cp *)",
|
||||
"Bash(mv *)", "Bash(ls *)", "Bash(cat *)", "Bash(chmod *)",
|
||||
"Bash(docker *)", "Bash(docker-compose *)", "Bash(docker compose *)",
|
||||
"Bash(ssh *@10.0.20.*)", "Bash(ssh root@10.0.20.*)",
|
||||
"Bash(trash *)",
|
||||
"Bash(mkdir *)",
|
||||
"Bash(cp *)",
|
||||
"Bash(mv *)",
|
||||
"Bash(ls *)",
|
||||
"Bash(cat *)",
|
||||
"Bash(chmod *)",
|
||||
"Bash(docker *)",
|
||||
"Bash(docker-compose *)",
|
||||
"Bash(docker compose *)",
|
||||
"Bash(ssh *@10.0.20.*)",
|
||||
"Bash(ssh root@10.0.20.*)",
|
||||
"Bash(ssh echo@10.0.20.*)",
|
||||
"Bash(scp *10.0.20.*)", "Bash(rsync *10.0.20.*)"
|
||||
"Bash(scp *10.0.20.*)",
|
||||
"Bash(rsync *10.0.20.*)"
|
||||
],
|
||||
"discord": {
|
||||
"email_webhook_url": "https://discord.com/api/webhooks/1496421990846697583/OM8z1eBsJC6-UB9-Zi5RkHP23NNv9UrEznRMx4Y3wSWOFmLazPoi-8_iEKMp0Qgsqr-m"
|
||||
},
|
||||
"ollama": {
|
||||
"url": "http://localhost:11434"
|
||||
"url": "http://10.0.20.161:11434"
|
||||
},
|
||||
"voice": {
|
||||
"allowed_user_ids": [
|
||||
"949388626146517022"
|
||||
],
|
||||
"user_name": "Marius",
|
||||
"default_voice": "F1",
|
||||
"auto_leave_minutes": 5
|
||||
},
|
||||
"paths": {
|
||||
"personality": "personality/",
|
||||
|
||||
279
cron/jobs.json
279
cron/jobs.json
@@ -1 +1,278 @@
|
||||
[]
|
||||
[
|
||||
{
|
||||
"name": "discord-test",
|
||||
"cron": "0 18 2 4 *",
|
||||
"channel": "echo-core",
|
||||
"model": "haiku",
|
||||
"prompt": "Răspunde doar cu textul: Test Discord cron job — funcționează!",
|
||||
"allowed_tools": [],
|
||||
"enabled": false,
|
||||
"last_run": "2026-04-02T18:09:42.851876+00:00",
|
||||
"last_status": "ok",
|
||||
"next_run": null
|
||||
},
|
||||
{
|
||||
"name": "anaf-monitor",
|
||||
"kind": "shell",
|
||||
"cron": "0 10,16 * * 1-5",
|
||||
"channel": "echo-work",
|
||||
"command": [
|
||||
"python3",
|
||||
"tools/anaf-monitor/monitor_v2.py"
|
||||
],
|
||||
"report_on": "changes",
|
||||
"timeout": 120,
|
||||
"enabled": true,
|
||||
"last_run": "2026-06-06T16:00:00.002312+00:00",
|
||||
"last_status": "ok",
|
||||
"next_run": "2026-06-09T10:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "security-audit-daily",
|
||||
"kind": "shell",
|
||||
"cron": "0 3 * * *",
|
||||
"channel": "echo-work",
|
||||
"command": [
|
||||
"python3",
|
||||
"tools/security_audit.py"
|
||||
],
|
||||
"report_on": "changes",
|
||||
"timeout": 180,
|
||||
"enabled": true,
|
||||
"last_run": "2026-06-09T03:00:00.002688+00:00",
|
||||
"last_status": "error",
|
||||
"next_run": "2026-06-10T03:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "kb-index-refresh",
|
||||
"kind": "shell",
|
||||
"cron": "30 3 * * *",
|
||||
"channel": "echo-work",
|
||||
"command": [
|
||||
"python3",
|
||||
"tools/update_notes_index.py"
|
||||
],
|
||||
"report_on": "never",
|
||||
"timeout": 120,
|
||||
"enabled": true,
|
||||
"last_run": "2026-06-09T03:30:00.002397+00:00",
|
||||
"last_status": "ok",
|
||||
"next_run": "2026-06-10T03:30:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "archive-tasks-daily",
|
||||
"kind": "shell",
|
||||
"cron": "0 3 * * *",
|
||||
"channel": "echo-work",
|
||||
"command": [
|
||||
"python3",
|
||||
"dashboard/archive_tasks.py"
|
||||
],
|
||||
"report_on": "changes",
|
||||
"timeout": 60,
|
||||
"enabled": true,
|
||||
"last_run": "2026-06-09T03:00:00.002281+00:00",
|
||||
"last_status": "ok",
|
||||
"next_run": "2026-06-10T03:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "backup-config",
|
||||
"kind": "shell",
|
||||
"cron": "0 2 * * *",
|
||||
"channel": "echo-work",
|
||||
"command": [
|
||||
"bash",
|
||||
"tools/backup_config.sh"
|
||||
],
|
||||
"report_on": "never",
|
||||
"timeout": 120,
|
||||
"enabled": true,
|
||||
"last_run": "2026-06-09T02:00:00.002899+00:00",
|
||||
"last_status": "ok",
|
||||
"next_run": "2026-06-10T02:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "insights-extract",
|
||||
"cron": "0 4 * * *",
|
||||
"channel": "echo-work",
|
||||
"model": "sonnet",
|
||||
"prompt": "PLACEHOLDER — Marius will write the full prompt. Intent: extract daily insights from chat history (Discord, Telegram, WhatsApp) and save to memory/kb/insights/YYYY-MM-DD.md. Runs after content-discovery (03:00) so insights can incorporate discovered content proposals.",
|
||||
"allowed_tools": [],
|
||||
"enabled": false,
|
||||
"last_run": null,
|
||||
"last_status": null,
|
||||
"next_run": null
|
||||
},
|
||||
{
|
||||
"name": "weekly-planning-sun",
|
||||
"cron": "0 22 * * 0",
|
||||
"channel": "echo-work",
|
||||
"model": "sonnet",
|
||||
"prompt": "WEEKLY PLANNING - duminică seara\n\n## CALENDAR SĂPTĂMÂNA URMATOARE\nVerifică calendarul:\n```bash\ncd ~/echo-core && source venv/bin/activate && python3 tools/calendar_check.py week\n```\n\nȘi verifică travel reminders:\n```bash\ncd ~/echo-core && source venv/bin/activate && python3 tools/calendar_check.py travel\n```\n\n## TRIMITE PE DISCORD #echo-work\nchannel=discord, target=1466726254312030259\n\nFormat:\n[⚡ Echo] **Săptămâna începe mâine!**\n\n📅 **CALENDAR:**\n- Luni: [evenimente]\n- Marți: [evenimente]\n- ... (toate zilele cu evenimente)\n\n🚂 **TRAVEL:**\nDacă sunt evenimente NLP/București:\n- ⚠️ [Event] pe [dată] - Ai bilete și cazare?\n- Link CFR: https://bilete.cfrcalatori.ro/\n- Link cazare: booking.com sau unde stă de obicei",
|
||||
"allowed_tools": [],
|
||||
"enabled": false,
|
||||
"last_run": null,
|
||||
"last_status": null,
|
||||
"next_run": null
|
||||
},
|
||||
{
|
||||
"name": "grup-sprijin-5feb",
|
||||
"cron": "0 18 5 2 *",
|
||||
"channel": "echo-work",
|
||||
"model": "sonnet",
|
||||
"prompt": "Reminder: Azi la 18:00 ai întâlnirea grupului de sprijin cu liderii de la cercetași.\n\nTrimite pe Discord #echo-sprijin (message tool):\nchannel=discord, target=1466739361503772864\n\nMesaj:\n[⭕ Echo] *Azi la 18:00 - Grup de sprijin!*\n\nAi pregătit ceva sau vrei idei de exerciții/întrebări?\n\nFișier: /home/moltbot/echo-core/memory/kb/projects/grup-sprijin/README.md\n\nDupă întâlnire: întreabă cum a fost și notează în fișier.",
|
||||
"allowed_tools": [],
|
||||
"enabled": false,
|
||||
"last_run": null,
|
||||
"last_status": null,
|
||||
"next_run": null
|
||||
},
|
||||
{
|
||||
"name": "grup-sprijin-pregatire",
|
||||
"cron": "0 18 3 2 *",
|
||||
"channel": "echo-work",
|
||||
"model": "sonnet",
|
||||
"prompt": "Pregătire fișă grup sprijin - joi 5 februarie.\n\nTrimite pe Discord #echo-sprijin (message tool):\nchannel=discord, target=1466739361503772864\n\nMesaj:\n[⭕ Echo] *Întâlnirea de grup e joi!*\n\nHai să pregătim fișa:\n\n1. Ce temă vrei să abordezi de data asta?\n2. Uită-te la exerciții: https://moltbot.tailf7372d.ts.net/echo/grup-sprijin.html - care 1-2 vrei să folosim?\n3. Ai ceva nou de adăugat din săptămâna asta?\n\nDupă ce îmi spui, fac fișa.",
|
||||
"allowed_tools": [],
|
||||
"enabled": false,
|
||||
"last_run": null,
|
||||
"last_status": null,
|
||||
"next_run": null
|
||||
},
|
||||
{
|
||||
"name": "content-discovery",
|
||||
"cron": "0 3 * * *",
|
||||
"channel": "echo-work",
|
||||
"model": "sonnet",
|
||||
"prompt": "JOB NOAPTE (02:00) - Content Discovery proactiv.\n\n## SCOP\nCaută video-uri/articole/bloguri relevante DE CALITATE pentru Marius și generează propuneri în format insight.\n\n## PAȘI:\n\n### 1. Citește contextul\n- read: USER.md (interese, provocări)\n- read: memory/YYYY-MM-DD.md (note recente, teme)\n\n### 2. Generează 3-4 queries de căutare\nBazat pe:\n- 60% teme recente (din note zilnice)\n- 40% interese bază (NLP, coaching, productivitate, sănătate)\n\n### 3. Caută conținut de CALITATE\n\n**YouTube (1-2 video-uri):**\n- web_search: 'site:youtube.com [query]'\n- Preferă: <20 min, autori cunoscuți/credibili\n- Evită: clickbait, shorts fără substanță\n\n**Articole/Bloguri (1-2 surse):**\n- web_search: '[query] blog article'\n- Criterii OBLIGATORII pentru a fi inclus:\n ✅ Autor cu credibilitate (expert în domeniu, publicații recunoscute)\n ✅ Conținut profund (nu listicle superficiale)\n ✅ Relevanță directă cu provocările/interesele lui Maris\n ✅ Perspective practice (nu doar teorie)\n \n- Surse de încredere (exemple):\n * Medium (autori verificați cu track record)\n * Bloguri experți NLP/coaching/productivitate\n * HBR, Psychology Today, Scientific American (când e relevant)\n * Bloguri personale ale practițienilor (cu substanță, nu marketing)\n \n- EVITĂ:\n ❌ Listicle generice (\"10 tips for...\")\n ❌ Conținut SEO fără substanță\n ❌ Articole de marketing/vânzare\n ❌ Surse necredibile sau fără autor identificabil\n\n### 4. Verifică calitatea înainte de a propune\nPentru fiecare articol/blog găsit:\n- Citește abstract/primele paragrafe cu web_fetch\n- Întreabă-te: \"Are insight-uri practice pentru Marius?\"\n- Dacă răspuns = NU → nu-l include\n\n### 5. Adaugă în insights ca propuneri\nScrie în memory/kb/insights/YYYY-MM-DD.md (data de MÂINE):\n\n```markdown\n## 🔍 Content Discovery\n\n### [ ] 🎬 **Titlu Video** (💡 nice / 📌 important)\n\n**De ce:** Explicație scurtă - cum se leagă de interesele/provocările lui Marius\n\n**Acțiune:** Procesează video și extrage note\n\n**Link:** https://youtube.com/watch?v=...\n\n---\n\n### [ ] 📄 **Titlu Articol - Autor** (💡 nice / 📌 important)\n\n**De ce:** Explicație - ce insight-uri practice oferă\n\n**Credibilitate:** [Cine e autorul + de ce e relevant]\n\n**Acțiune:** Citește și extrage în kb/articole/\n\n**Link:** https://...\n```\n\n### 6. NU trimite mesaj\nRaportul de dimineață va propune automat.\n\n## REGULI:\n- Max 3-4 propuneri per noapte (1-2 video + 1-2 articole)\n- Prioritate: **CALITATE > CANTITATE**\n- Evită duplicate (verifică memory/kb/ pentru ce e deja procesat)\n- Fii variat - nu repeta aceiași autori zilnic\n- **FILTRARE STRICTĂ:** Doar conținut cu greutate, nu orice link",
|
||||
"allowed_tools": [],
|
||||
"enabled": false,
|
||||
"last_run": null,
|
||||
"last_status": null,
|
||||
"next_run": null
|
||||
},
|
||||
{
|
||||
"name": "provocare-reminder",
|
||||
"cron": "0 13 * * 1-5",
|
||||
"channel": "echo-work",
|
||||
"model": "sonnet",
|
||||
"prompt": "REMINDER PROVOCARE - la prânz\n\n1. Citește provocarea: read memory/provocare-azi.md\n\n2. Trimite pe Discord #echo-self (target=1466739112747864175):\n\n[⭕ Echo] **Reminder: Provocarea de azi**\n\n[conținutul provocării]\n\nAi făcut progres? Sau măcar un pas mic?\n\n3. NU trimite pe email (doar Discord)",
|
||||
"allowed_tools": [],
|
||||
"enabled": false,
|
||||
"last_run": null,
|
||||
"last_status": null,
|
||||
"next_run": null
|
||||
},
|
||||
{
|
||||
"name": "morning-report",
|
||||
"cron": "30 8 * * *",
|
||||
"channel": "echo-work",
|
||||
"model": "sonnet",
|
||||
"enabled": false,
|
||||
"prompt": "RAPORT DIMINEAȚĂ - trimite pe EMAIL (Gmail: mmarius28@gmail.com)\n\n## CALENDAR\nVerifică calendarul:\n```bash\ncd ~/echo-core && source venv/bin/activate && python3 tools/calendar_check.py today\npython3 tools/calendar_check.py travel\npython3 tools/calendar_check.py week\n```\n\n## CITEȘTE CONTEXT\n- USER.md pentru programul lui Marius (luni-joi 15-16 liber)\n- memory/kb/insights/ pentru propuneri (ultimele 3 zile)\n- /home/moltbot/echo-core/approved-tasks.json pentru status proiecte/features (câmpurile: name, status, started_at, pid)\n\n## FORMAT EMAIL HTML\n- Font: 16px text, 18px titluri\n- Culori: albastru (#dbeafe) DONE, gri (#f3f4f6) PROGRAMAT, verde (#d1fae5) PROJECTS\n- Link-uri vizibile\n\n## STRUCTURA RAPORT\n\n### 1. CALENDAR\n- 📅 **AZI:** [evenimente]\n- 📅 **MÂINE:** [evenimente]\n- 📅 **PESTE 2 ZILE:** [dacă e GRUP, NLP, meeting mare]\n- 🚂 **TRAVEL:** Reminders bilete+cazare\n\n### 2. PROIECTE/FEATURES NOAPTEA 💻\n\nCitesc /home/moltbot/echo-core/approved-tasks.json și raportez ce s-a realizat:\n(statusuri: pending, approved, running, complete, failed, stopped)\nPentru stories done/total: citesc /home/moltbot/workspace/{name}/scripts/ralph/prd.json\n\n**Format pentru fiecare proiect/feature [x]:**\n\n```html\n<div style=\"background: #d1fae5; padding: 15px; margin: 10px 0; border-radius: 8px;\">\n <h3>✅ P1 - Nume Proiect</h3>\n \n <p><strong>Status:</strong> X/Y stories complete</p>\n \n <p><strong>Stories realizate:</strong></p>\n <ul>\n <li>✅ US-001: Titlu story - implementat cu succes</li>\n <li>✅ US-002: Titlu story - quality checks pass</li>\n <li>🔄 US-003: Titlu story - în progres (blocat pe dependency)</li>\n </ul>\n \n <p><strong>Link:</strong> <a href=\"https://gitea.romfast.ro/romfast/PROJECT-NAME\">gitea.romfast.ro/romfast/PROJECT-NAME</a></p>\n \n <p><strong>Learnings:</strong> [din progress.txt - ce patterns am descoperit]</p>\n \n <p><strong>Next steps:</strong> [ce rămâne de făcut]</p>\n</div>\n```\n\n**Dacă NU s-au executat proiecte/features:**\n- Sari peste această secțiune\n\n### 3. STATUS GENERAL\n- Ce s-a făcut ieri (joburi, taskuri)\n- Git status ~/clawd\n- Joburi executate (YouTube, insights, etc.)\n\n### 4. PROPUNERI CU ZI ȘI ORĂ!\n\n**OBLIGATORIU:** Fiecare propunere TU+EU sau FAC TU trebuie să aibă ZI și ORĂ concrete!\n\nCategorii:\n- 🤖 **FAC EU** (0 efort) - execut singur\n- 🤝 **TU+EU** (eu pregătesc) - cu zi/oră!\n- 👤 **FAC TU** (template gata) - cu zi/oră!\n\nExemplu:\n- **A1 - Sesiune Dizolvare Vină** 🤝 TU+EU\n 📅 **Marți 3 feb, 15:00-15:30**\n Context + link sursă\n\nReguli programare:\n- Luni-Joi 15:00-16:00 = slot liber\n- Vineri-Duminică = NLP, evită\n- Verifică calendar să nu fie ocupat\n\n### 5. INSIGHTS DISPONIBILE\n\nListează insights-uri [ ] nepropuse încă (format scurt).\n\n### 6. CUM RĂSPUNZI\n- DA = aprob toate (cu zilele/orele propuse)\n- 1 pentru A1,A2 = execut ACUM\n- 2 pentru A3 = programez noapte\n- 3 pentru A5 = skip\n- Alt orar = \"A1 miercuri nu marți\"\n\n## TRIMITERE\npython3 /home/moltbot/echo-core/tools/email_send.py \"mmarius28@gmail.com\" \"Raport Dimineata DATA\" \"HTML_CONTENT\"\n\nNU trimite pe Discord - doar email.",
|
||||
"allowed_tools": [],
|
||||
"last_run": "2026-05-14T08:30:00.001601+00:00",
|
||||
"last_status": "ok",
|
||||
"next_run": "2026-05-15T08:30:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "evening-report",
|
||||
"cron": "0 21 * * *",
|
||||
"channel": "echo-work",
|
||||
"model": "sonnet",
|
||||
"enabled": false,
|
||||
"prompt": "RAPORT SEARĂ - trimite pe EMAIL (Gmail: mmarius28@gmail.com)\n\n## CALENDAR\nVerifică ce ai mâine și săptămâna:\n```bash\ncd ~/echo-core && source venv/bin/activate && python3 tools/calendar_check.py today\npython3 tools/calendar_check.py week\n```\n\n## CITEȘTE CONTEXT\n- USER.md pentru programul lui Marius (luni-joi 15-16 liber, vineri-dum NLP)\n- memory/kb/insights/YYYY-MM-DD.md pentru propuneri insights\n- memory/kb/youtube/ și memory/kb/articole/ pentru inspirație proiecte\n- /home/moltbot/echo-core/approved-tasks.json pentru status proiecte existente (câmpurile: name, status, proposed_at)\n\n## FORMAT EMAIL HTML\n- Font: 16px text, 18px titluri\n- Culori: albastru (#dbeafe) DONE, gri (#f3f4f6) PROGRAMAT, verde (#d1fae5) PROJECTS\n- Link-uri vizibile\n\n## STRUCTURA RAPORT\n\n### 1. MÂINE\n- 📅 Evenimente calendar\n- 🚂 Travel reminders\n\n### 2. STATUS\n- Ce s-a făcut azi\n- Git status\n\n### 3. PROPUNERI CU ZI ȘI ORĂ!\n\n**OBLIGATORIU:** Fiecare propunere TU+EU sau FAC TU trebuie să aibă ZI și ORĂ concrete!\n\nReguli programare:\n- Luni-Joi 15:00-16:00 = slot liber\n- Vineri-Duminică = NLP, evită\n- Verifică calendar să nu fie ocupat\n- Sesiuni scurte: 15-30 min\n\n### 4. PROGRAME/PROIECTE PRACTICE 💻\n\n**CONTEXT OBLIGATORIU - citește înainte de a propune:**\n\n**Proiecte existente (PRIORITARE pentru features):**\n- **roa2web** (gitea.romfast.ro/romfast/roa2web) - FastAPI+Vue.js+Telegram bot\n - Are deja: balanță, facturi, trezorerie\n - Lipsesc: validări declarații ANAF, facturare valută/taxare inversă, notificări\n - Rapoarte ROA noi → FEATURE în roa2web, NU proiect separat!\n- **Chatbot Maria** (Flowise pe LXC 104, ngrok → romfast.ro/chatbot_maria.html)\n - Document store: XML, MD | Groq gratuit + Ollama embeddings + FAISS\n - Problema: răspunsuri nu sunt suficient de bune\n - Angajatul nou poate menține documentația (scrie TXT, trebuie converter)\n - Clientii îl accesează din programele ROA direct\n\n**Întrebări frecvente clienți (surse de proiecte):**\n- Erori validare declarații ANAF (D406, D394, D100 etc.)\n- Cum facturez în valută cu taxare inversă?\n- Probleme la instalări, inițializări firme noi, configurări\n\n**Reguli propuneri (80/20 STRICT):**\n- Impact mare pentru Marius → apoi pentru clienți ERP ROA\n- Inspirat din discovery (YouTube, articole, insights procesate)\n- Features roa2web > proiecte noi (integrare în existent)\n- Proiecte independente doar dacă NU se potrivesc în roa2web/Flowise\n\n**A. FEATURES PROIECTE EXISTENTE (2-3, PRIORITAR):**\n\nFormat:\n```\n### ⚡ F1 - Feature pentru [roa2web/chatbot]\n**Ce face:** Descriere scurtă\n**De ce:** Ce problemă rezolvă (ex: \"clienții întreabă X de 5 ori/săptămână\")\n**Complexitate:** S/M/L\n**Proiect:** roa2web / chatbot-maria\n```\n\n**B. PROIECTE NOI (max 1, doar dacă nu se integrează în existente):**\n\nFormat:\n```\n### 💻 P1 - Nume Proiect\n**De ce:** Cum se leagă de nevoile lui Marius/clienți\n**Impact:** Pentru Marius + pentru clienți\n**Efort:** Ore/zile realist\n**Stack:** Simplu (80/20)\n**Sursă:** [Link nota KB]\n```\n\n**NU propune:**\n- Proiecte complexe fără beneficiu clar\n- Proiecte duplicat cu ce există deja\n- Rapoarte ROA ca proiect separat (→ feature roa2web)\n\n### 5. INSIGHTS DISPONIBILE\nListează insights-uri [ ] nepropuse încă (format scurt).\n\n### 6. CUM RĂSPUNZI\n- DA = aprob toate (cu zilele/orele propuse)\n- 1 pentru A1,A2 = execut ACUM\n- 2 pentru A3 = programez noapte\n- 3 pentru A5 = skip\n- **F pentru F1,F3** = implementează features (joburi noapte)\n- **P pentru P1** = creează proiect nou (job noapte)\n- Alt orar = \"A1 miercuri nu marți\"\n\n## IMPLEMENTARE PROIECTE APROBATE\n\nCând propui features (F) sau proiecte (P), adaugă-le automat în /home/moltbot/echo-core/approved-tasks.json cu status 'pending':\n```bash\npython3 -c \"\nimport json, datetime\nf = open('/home/moltbot/echo-core/approved-tasks.json')\ndata = json.load(f); f.close()\ndata['projects'].append({'name': 'SLUG-PROIECT', 'description': 'DESCRIERE', 'status': 'pending', 'proposed_at': datetime.datetime.utcnow().isoformat(), 'approved_at': None, 'started_at': None, 'pid': None})\ndata['last_updated'] = datetime.datetime.utcnow().isoformat()\nopen('/home/moltbot/echo-core/approved-tasks.json', 'w').write(json.dumps(data, indent=2))\n\"\n```\n\nÎn email, arată lui Marius comanda de aprobare:\n`!approve SLUG-PROIECT` (trimite pe Discord/Telegram la Echo)\n\nNight-execute (23:00) va:\n - genera PRD cu ralph_prd_generator.py dacă nu există prd.json\n - lansa ralph.sh 15 iterații pentru fiecare proiect aprobat\n\n## TRIMITERE\npython3 /home/moltbot/echo-core/tools/email_send.py \"mmarius28@gmail.com\" \"Raport Seara DATA\" \"HTML_CONTENT\"\n\nNU trimite pe Discord - doar email.",
|
||||
"allowed_tools": [],
|
||||
"last_run": "2026-05-14T21:00:00.003039+00:00",
|
||||
"last_status": "ok",
|
||||
"next_run": "2026-05-15T21:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "morning-coaching",
|
||||
"cron": "0 10 * * *",
|
||||
"channel": "echo-work",
|
||||
"model": "sonnet",
|
||||
"prompt": "COACHING DIMINEAȚĂ pentru Marius.\n\n## VERIFICĂ ÎNTÂI DACĂ S-A TRIMIS DEJA\n```bash\nls ~/clawd/memory/kb/coaching/ | grep \"$(date +%Y-%m-%d)\"\n```\n\nDacă EXISTĂ fișier cu data de azi (ex: 2026-02-03-dimineata.md):\n→ Răspunde doar: \"Coaching deja trimis azi. HEARTBEAT_OK\"\n→ NU crea alt fișier, NU trimite pe Discord, NU trimite email\n\nDacă NU există:\n→ Continuă cu pașii de mai jos\n\n## PAȘI\n\n1. Verifică ce ai trimis recent:\n exec ls -la /home/moltbot/echo-core/memory/kb/coaching/ | tail -7\n\n2. Inspiră-te din memory/kb/youtube/ și memory/kb/insights/\n\n3. Crează mesajul de coaching (inspirațional + provocare)\n\n4. Trimite pe Discord #echo-self:\n channel=discord, target=1466739112747864175\n Format: [⭕ Echo] **GÂNDUL DE DIMINEAȚĂ** + conținut\n\n5. Trimite pe EMAIL (Gmail):\n python3 /home/moltbot/echo-core/tools/email_send.py \"mmarius28@gmail.com\" \"Gandul de dimineata\" \"[MESAJUL - text sau HTML simplu]\"\n\n6. Salvează în memory/kb/coaching/YYYY-MM-DD-dimineata.md\n\n7. Salvează provocarea în memory/provocare-azi.md\n\n8. ADAUGĂ PROVOCAREA În TODO'S:\n Citește dashboard/todos.json, adaugă item nou cu structura:\n {\n \"id\": \"prov-YYYY-MM-DD\",\n \"text\": \"Provocare: [TEXT SCURT - max 100 caractere]\",\n \"context\": \"[EXPLICAȚIE: de ce e important, cum să fac pas cu pas]\",\n \"example\": \"[EXEMPLU CONCRET din viața lui Marius - situație reală]\",\n \"domain\": \"self\",\n \"dueDate\": \"YYYY-MM-DD\",\n \"done\": false,\n \"doneAt\": null,\n \"source\": \"[Autor - Titlu video/articol]\",\n \"sourceUrl\": \"https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/[fisier].md\",\n \"createdAt\": \"[now ISO]\"\n }\n Salvează dashboard/todos.json\n\n9. Update index:\n python3 /home/moltbot/echo-core/tools/update_notes_index.py\n\n## IMPORTANT\n- VERIFICĂ ÎNTÂI DACĂ EXISTĂ DEJA FIȘIER CU DATA DE AZI!\n- Context-ul explică DE CE și CUM\n- Exemplul e CONCRET, din viața lui Marius (clienți, angajat, etc.)\n- Citește USER.md pentru context despre Marius\n\nFii creativ!",
|
||||
"allowed_tools": [],
|
||||
"enabled": false,
|
||||
"last_run": null,
|
||||
"last_status": null,
|
||||
"next_run": null
|
||||
},
|
||||
{
|
||||
"name": "evening-coaching",
|
||||
"cron": "0 22 * * *",
|
||||
"channel": "echo-work",
|
||||
"model": "sonnet",
|
||||
"prompt": "COACHING SEARĂ pentru Marius.\n\n## PAȘI\n\n1. Verifică ce ai trimis:\n exec ls -la /home/moltbot/echo-core/memory/kb/coaching/ | tail -7\n\n2. Verifică provocarea de azi:\n read memory/provocare-azi.md\n\n3. Verifică dacă a fost bifată în Todo's:\n read dashboard/todos.json\n Caută \"prov-YYYY-MM-DD\" (azi) și vezi dacă \"done\": true\n\n4. Inspiră-te din memory/kb/youtube/ și memory/kb/insights/\n\n5. Crează reflecție + follow-up provocare:\n - Dacă a bifat: felicită-l, întreabă cum a fost\n - Dacă nu a bifat: întreabă ce l-a blocat, fără judecată\n\n6. Trimite pe Discord #echo-self:\n channel=discord, target=1466739112747864175\n Format: [⭕ Echo] **GÂNDUL DE SEARĂ** + reflecție\n\n7. Trimite pe EMAIL (Gmail):\n python3 /home/moltbot/echo-core/tools/email_send.py \"mmarius28@gmail.com\" \"Gandul de seara\" \"[MESAJUL - text sau HTML simplu]\"\n\n8. Salvează în memory/kb/coaching/YYYY-MM-DD-seara.md\n\n9. Update index:\n python3 /home/moltbot/echo-core/tools/update_notes_index.py\n\n## IMPORTANT\n- Verifică Todo's pentru a ști dacă a făcut provocarea\n- Fii empatic, nu critic\n- Citește USER.md pentru context\n\nFii creativ!",
|
||||
"allowed_tools": [],
|
||||
"enabled": false,
|
||||
"last_run": null,
|
||||
"last_status": null,
|
||||
"next_run": null
|
||||
},
|
||||
{
|
||||
"name": "exercise-snack-1",
|
||||
"cron": "30 9 * * *",
|
||||
"channel": "echo-work",
|
||||
"model": "sonnet",
|
||||
"prompt": "Trimite exercițiul pe Discord #echo-self:\n\nmessage tool: action=send, channel=discord, target=1466739112747864175\n\n⏰ Exercise Snack #1 (3 min)\n\n• 10 squats\n• 5 push-ups\n• 30 sec plank\n\nGata? Reacționează cu ✅",
|
||||
"allowed_tools": [],
|
||||
"enabled": false,
|
||||
"last_run": null,
|
||||
"last_status": null,
|
||||
"next_run": null
|
||||
},
|
||||
{
|
||||
"name": "exercise-snack-2",
|
||||
"cron": "30 13 * * *",
|
||||
"channel": "echo-work",
|
||||
"model": "sonnet",
|
||||
"prompt": "Trimite exercițiul pe Discord #echo-self:\n\nmessage tool: action=send, channel=discord, target=1466739112747864175\n\n⏰ Exercise Snack #2 (3 min)\n\n• 20 step-ups pe scaun\n• 20 high knees\n\nGata? Reacționează cu ✅",
|
||||
"allowed_tools": [],
|
||||
"enabled": false,
|
||||
"last_run": null,
|
||||
"last_status": null,
|
||||
"next_run": null
|
||||
},
|
||||
{
|
||||
"name": "exercise-snack-3",
|
||||
"cron": "30 17 * * *",
|
||||
"channel": "echo-work",
|
||||
"model": "sonnet",
|
||||
"prompt": "Trimite exercițiul pe Discord #echo-self:\n\nmessage tool: action=send, channel=discord, target=1466739112747864175\n\n⏰ Exercise Snack #3 (3 min)\n\n• 15 squats\n• 10 lunges\n• Marș pe loc 1 min\n\nGata? Reacționează cu ✅",
|
||||
"allowed_tools": [],
|
||||
"enabled": false,
|
||||
"last_run": null,
|
||||
"last_status": null,
|
||||
"next_run": null
|
||||
},
|
||||
{
|
||||
"name": "heartbeat-2h",
|
||||
"cron": "0 6-18/2 * * *",
|
||||
"channel": "echo-work",
|
||||
"model": "sonnet",
|
||||
"prompt": "Heartbeat check. Rulează src/heartbeat.py printr-un scurt raport de status.\nDacă nu e nimic de raportat (email=0, calendar nu are evenimente <2h, kb ok), răspunde doar cu HEARTBEAT_OK și oprește-te — nu trimite mesaj.\nDacă e ceva: raport scurt pe Discord #echo-work.",
|
||||
"allowed_tools": [],
|
||||
"enabled": true,
|
||||
"last_run": "2026-06-09T08:00:00.001362+00:00",
|
||||
"last_status": "ok",
|
||||
"next_run": "2026-06-09T10:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "night-execute",
|
||||
"cron": "0 23 * * *",
|
||||
"channel": "echo-work",
|
||||
"model": "opus",
|
||||
"enabled": true,
|
||||
"prompt": "NIGHT-EXECUTE - Implementare autonoma proiecte aprobate\n\n## PASUL 1: Citeste proiectele aprobate\n\nCiteste /home/moltbot/echo-core/approved-tasks.json\nSelecteaza proiectele cu status='approved'\nDaca nu sunt proiecte aprobate: raporteaza pe Discord si opreste-te.\n\n## PASUL 2: Pentru fiecare proiect aprobat\n\nPentru un proiect cu schema extinsa (campuri optionale {repo, branch, base_branch}):\n - {name} = slug-ul proiectului (cheia 'name' din JSON)\n - {repo} = numele repo-ului Gitea (default = {name} daca nu e setat)\n - {branch} = feature branch nou (None inseamna 'lucreaza pe HEAD-ul default al repo-ului')\n - {base_branch} = branch-ul de la care porneste {branch} (default 'main')\n\n1. Verifica daca workspace-ul exista: /home/moltbot/workspace/{name}\n - Daca NU exista:\n TOKEN=$(grep GITEA_TOKEN /home/moltbot/echo-core/dashboard/.env | cut -d= -f2)\n git clone https://moltbot:${TOKEN}@gitea.romfast.ro/romfast/{repo}.git /home/moltbot/workspace/{name}\n # NOTA: cloneaza {repo}, nu {name}, ca sa suporte features pe repo-uri existente\n # (ex: slug='roa2web-bonuri', repo='roa2web')\n cd /home/moltbot/workspace/{name}\n # Daca {branch} e setat: creeaza branch nou de la {base_branch}\n if [ -n \"{branch}\" ]; then\n git fetch origin {base_branch:-main}\n git checkout {base_branch:-main}\n git checkout -b {branch} 2>/dev/null || git checkout {branch}\n fi\n - Daca EXISTA workspace-ul si {branch} e setat: asigura-te ca esti pe {branch}:\n cd /home/moltbot/workspace/{name}\n git checkout {branch} 2>/dev/null || git checkout -b {branch} {base_branch:-main}\n\n2. Verifica daca prd.json exista: /home/moltbot/workspace/{name}/scripts/ralph/prd.json\n - Daca nu: ruleaza generatorul PRD:\n source .venv/bin/activate\n python3 tools/ralph_prd_generator.py \"{name}\" \"{description}\" /home/moltbot/workspace\n\n3. Lanseaza Ralph loop:\n cd /home/moltbot/workspace/{name}\n chmod +x scripts/ralph/ralph.sh\n mkdir -p scripts/ralph/logs\n nohup ./scripts/ralph/ralph.sh 15 > scripts/ralph/logs/ralph-$(date +%Y%m%d).log 2>&1 &\n echo $! > scripts/ralph/.ralph.pid\n\n4. Actualizeaza approved-tasks.json:\n - status: 'running'\n - started_at: timestamp curent\n - pid: PID din .ralph.pid\n\n## PASUL 3: Raport Discord\n\nTrimite pe echo-work:\n- Cate proiecte au pornit\n- PID-urile lor\n- Pentru cele cu {branch} setat, mentioneaza branch-ul activ\n- 'morning-report va raporta progresul la 08:30'\n\n## REGULI IMPORTANTE\n\n- Nu modifica niciodata src/router.py, src/claude_session.py sau alte fisiere core echo-core prin Ralph\n- echo-core self-improvement NUMAI pe branch ralph/echo-improve, nu pe master\n- Daca ralph.sh esueaza: log in approved-tasks.json (status: failed, error: mesaj)\n- Daca git clone esueaza (repo inexistent): log status='failed' cu mesajul, NU continua cu PRD/ralph\n- Delay 5 secunde intre proiecte pentru a evita rate limiting\n",
|
||||
"allowed_tools": [
|
||||
"Bash",
|
||||
"Read",
|
||||
"Write"
|
||||
],
|
||||
"last_run": "2026-06-08T23:00:00.001531+00:00",
|
||||
"last_status": "ok",
|
||||
"next_run": "2026-06-09T23:00:00+00:00"
|
||||
}
|
||||
]
|
||||
|
||||
276
cron/jobs.json.bak-pre-restore
Normal file
276
cron/jobs.json.bak-pre-restore
Normal file
File diff suppressed because one or more lines are too long
5
cron/newsletter-cercetasi-state.json
Normal file
5
cron/newsletter-cercetasi-state.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"last_sent": 21,
|
||||
"year": 2026,
|
||||
"last_sent_at": "2026-06-04T19:53:04.648928+00:00"
|
||||
}
|
||||
317
dashboard/DESIGN.md
Normal file
317
dashboard/DESIGN.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# Echo Dashboard — Design System
|
||||
|
||||
This document is the source of truth for visual decisions across the Echo
|
||||
Dashboard (port 8088, served at `/echo/`). Tokens live in
|
||||
`dashboard/static/tokens.css`. Page-level CSS is in `common.css` and per-page
|
||||
`<style>` blocks. **Pages must include `tokens.css` before `common.css`.**
|
||||
|
||||
---
|
||||
|
||||
## Theme
|
||||
|
||||
- **Default**: dark. Background `--bg-base: #13131a` (near-black neutral).
|
||||
- **Light theme**: opt-in via `<html data-theme="light">`. Light tokens override
|
||||
the dark palette in the same `:root`-equivalent block.
|
||||
- **Toggle**: header `.theme-toggle` button — persisted in `localStorage`.
|
||||
|
||||
Surfaces are translucent overlays on `--bg-base`, never solid greys, so
|
||||
elevation reads consistently against future backgrounds.
|
||||
|
||||
---
|
||||
|
||||
## Color tokens
|
||||
|
||||
### Surfaces (dark)
|
||||
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `--bg-base` | `#13131a` | App background |
|
||||
| `--bg-surface` | `rgba(255,255,255,0.12)` | Cards, panels, inputs |
|
||||
| `--bg-surface-hover` | `rgba(255,255,255,0.16)` | Hover state on surfaces |
|
||||
| `--bg-surface-active` | `rgba(255,255,255,0.20)` | Pressed / active surfaces |
|
||||
| `--bg-elevated` | `rgba(255,255,255,0.14)` | Selects, popovers |
|
||||
| `--header-bg` | `rgba(19,19,26,0.95)` | Sticky header backdrop |
|
||||
|
||||
### Text
|
||||
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `--text-primary` | `#ffffff` | Headings, key labels |
|
||||
| `--text-secondary` | `#f5f5f5` | Body copy |
|
||||
| `--text-muted` | `#e5e5e5` | Meta, timestamps, captions |
|
||||
|
||||
### Accent + borders
|
||||
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `--accent` | `#3b82f6` | Primary buttons, focus, links |
|
||||
| `--accent-hover` | `#2563eb` | Hover on `--accent` |
|
||||
| `--accent-subtle` | `rgba(59,130,246,0.2)` | Active nav background |
|
||||
| `--border` | `rgba(255,255,255,0.3)` | Card / input outline |
|
||||
| `--border-focus` | `rgba(59,130,246,0.7)` | Card hover, input focus |
|
||||
|
||||
### Semantic state
|
||||
|
||||
| Token | Value | Meaning |
|
||||
|---|---|---|
|
||||
| `--success` | `#22c55e` | OK, saved, healthy |
|
||||
| `--warning` | `#eab308` | Caution, soft fail |
|
||||
| `--error` | `#ef4444` | Hard fail, destructive |
|
||||
|
||||
### Status palette (workflow states)
|
||||
|
||||
These drive the `.status-pill[data-status]` system on workspace cards.
|
||||
|
||||
| Token | Value | State name |
|
||||
|---|---|---|
|
||||
| `--status-running` | `rgb(34, 197, 94)` | `running-ralph`, `running-manual` |
|
||||
| `--status-blocked` | `rgb(245, 158, 11)` | `blocked` |
|
||||
| `--status-failed` | `rgb(239, 68, 68)` | `failed` |
|
||||
| `--status-complete` | `rgb(156, 163, 175)` | `complete` |
|
||||
| `--status-idle` | `var(--text-muted)` | `idle` |
|
||||
| `--status-planning` | `rgb(167, 139, 250)` | `planning` *(new)* |
|
||||
| `--status-pending` | `rgb(96, 165, 250)` | `pending` *(new)* |
|
||||
| `--status-approved` | `rgb(234, 179, 8)` | `approved` *(new)* |
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
- **Sans**: `'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`
|
||||
— self-hosted woff2 at `/echo/static/fonts/inter-{400,500,600,700}.woff2`.
|
||||
- **Mono**: `'JetBrains Mono', 'Fira Code', ui-monospace, monospace` — for
|
||||
logs, code blocks, slugs, IDs. Loaded by browser if present (not bundled).
|
||||
|
||||
### Size scale
|
||||
|
||||
| Token | rem | px @ 16px |
|
||||
|---|---|---|
|
||||
| `--text-xs` | 0.75 | 12 |
|
||||
| `--text-sm` | 0.875 | 14 |
|
||||
| `--text-base` | 1 | 16 |
|
||||
| `--text-lg` | 1.125 | 18 |
|
||||
| `--text-xl` | 1.25 | 20 |
|
||||
|
||||
### Weights
|
||||
|
||||
400 (body), 500 (medium emphasis), 600 (headings, button labels),
|
||||
700 (rare — page titles only). No 800/900.
|
||||
|
||||
---
|
||||
|
||||
## Spacing — 8px grid
|
||||
|
||||
All padding, margin, and gap values use these tokens. No hard-coded pixels.
|
||||
|
||||
| Token | px |
|
||||
|---|---|
|
||||
| `--space-1` | 4 |
|
||||
| `--space-2` | 8 |
|
||||
| `--space-3` | 12 |
|
||||
| `--space-4` | 16 |
|
||||
| `--space-5` | 20 |
|
||||
| `--space-6` | 24 |
|
||||
| `--space-8` | 32 |
|
||||
| `--space-10` | 40 |
|
||||
|
||||
---
|
||||
|
||||
## Border radius
|
||||
|
||||
| Token | px | Use |
|
||||
|---|---|---|
|
||||
| `--radius-sm` | 4 | Tags, micro-pills |
|
||||
| `--radius-md` | 8 | Buttons, inputs |
|
||||
| `--radius-lg` | 12 | Cards, modals, panels |
|
||||
| `--radius-full` | 9999 | Status pills, badges, avatars |
|
||||
|
||||
---
|
||||
|
||||
## Buttons
|
||||
|
||||
All buttons share `.btn` (8px radius, 14px font, 8/16 padding,
|
||||
`--transition-fast`).
|
||||
|
||||
| Variant | Class | Surface | Text | Use |
|
||||
|---|---|---|---|---|
|
||||
| Primary | `.btn-primary` | `--accent` | white | The one CTA per row |
|
||||
| Secondary | `.btn-secondary` | `--bg-surface` + `--border` | `--text-secondary` | Side actions |
|
||||
| Ghost | `.btn-ghost` | transparent | `--text-secondary` | Tertiary, destructive-soft |
|
||||
| Danger | `.btn-danger` | `--error` | white | Stop, delete, irreversible |
|
||||
|
||||
Disabled state: `opacity: 0.5; cursor: not-allowed;`. Never grey out by
|
||||
swapping colors — keep variant identity.
|
||||
|
||||
---
|
||||
|
||||
## Card component (`.project-card`)
|
||||
|
||||
- `border-radius: var(--radius-lg)` (12px)
|
||||
- `background: var(--bg-surface)`
|
||||
- `border: 1px solid var(--border)`
|
||||
- `padding: var(--space-5)`
|
||||
- `transition: border-color var(--transition-base)`
|
||||
- **Hover**: `border-color: var(--border-focus)` (blue glow). No surface
|
||||
brightening — border-only hover keeps the grid calm.
|
||||
|
||||
---
|
||||
|
||||
## Status pill system
|
||||
|
||||
A `.status-pill` is a `--radius-full` chip placed on every project card. It
|
||||
encodes the current workflow state via `data-status="<state>"`.
|
||||
|
||||
### Visual recipe
|
||||
|
||||
- **Background**: state color at **18% alpha** (`color-mix(in srgb, var(--status-X) 18%, transparent)` or precomputed `rgba(...)`).
|
||||
- **Text**: solid state color (full alpha).
|
||||
- **Border**: 1px state color at 30% alpha.
|
||||
- **Padding**: `var(--space-1) var(--space-3)` — slim.
|
||||
- **Font**: `var(--text-xs)`, weight 500.
|
||||
|
||||
### Pulse-dot
|
||||
|
||||
Active states render a 6px CSS-shape circle that pulses (no SVG, no emoji).
|
||||
|
||||
```css
|
||||
.status-pill::before {
|
||||
content: ""; width: 6px; height: 6px; border-radius: 50%;
|
||||
background: currentColor; margin-right: var(--space-2);
|
||||
}
|
||||
.status-pill[data-status="running-ralph"]::before,
|
||||
.status-pill[data-status="running-manual"]::before,
|
||||
.status-pill[data-status="planning"]::before {
|
||||
animation: pulse-dot 1.6s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse-dot { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
|
||||
```
|
||||
|
||||
### State matrix
|
||||
|
||||
| `data-status` | Color token | Pulse | Label |
|
||||
|---|---|---|---|
|
||||
| `running-ralph` | `--status-running` | yes | Ralph running |
|
||||
| `running-manual` | `--status-running` | yes | Manual run |
|
||||
| `planning` | `--status-planning` | yes | Planning |
|
||||
| `approved` | `--status-approved` | no | Approved |
|
||||
| `pending` | `--status-pending` | no | Pending |
|
||||
| `blocked` | `--status-blocked` | no | Blocked |
|
||||
| `failed` | `--status-failed` | no | Failed |
|
||||
| `complete` | `--status-complete` | no | Complete |
|
||||
| `idle` | `--status-idle` | no | Idle |
|
||||
|
||||
---
|
||||
|
||||
## BUTTONS_FOR_STATE matrix
|
||||
|
||||
Each project card surfaces ≤3 actions, ordered Primary / Secondary / Ghost.
|
||||
The renderer picks the row matching `data-status`.
|
||||
|
||||
| State | Primary | Secondary | Ghost |
|
||||
|---|---|---|---|
|
||||
| `running-ralph` | Stop Ralph (danger) | Logs | PRD |
|
||||
| `running-manual` | Stop (danger) | Open server | Logs |
|
||||
| `planning` | Continue chat | — | Cancel |
|
||||
| `approved` | — | Unapprove | Plan |
|
||||
| `pending` | Approve | Plan with Echo | Cancel |
|
||||
| `blocked` | View logs | Resume | — |
|
||||
| `failed` | View logs | Retry | Rollback |
|
||||
| `complete` | View plan | — | Run again |
|
||||
| `idle` | Run Ralph | — | Delete |
|
||||
|
||||
Rules:
|
||||
- **Stop / Delete** are always `.btn-danger`, never primary blue.
|
||||
- A dash (`—`) means render nothing (no placeholder, no greyed-out slot).
|
||||
- The Primary slot is the default action when the card is keyboard-focused
|
||||
and Enter is pressed.
|
||||
|
||||
---
|
||||
|
||||
## Toast taxonomy
|
||||
|
||||
Toasts appear top-right, stack vertically, dismiss after 4s (errors: 8s).
|
||||
**Five types**, distinguished by a 3px colored left bar — no emoji, no icon
|
||||
fill. Body uses `--text-primary` on `--bg-surface`.
|
||||
|
||||
| Type | Bar color | Use |
|
||||
|---|---|---|
|
||||
| `success` | `--success` | Saved, approved, deployed |
|
||||
| `info` | `--accent` | Neutral confirmation |
|
||||
| `warning` | `--warning` | Soft fail, retried |
|
||||
| `busy` | `--status-planning` | Long-running op started |
|
||||
| `error` | `--error` | Hard fail, action required |
|
||||
|
||||
Toast renderer is shared across pages and reads from a single global
|
||||
`window.showToast(type, msg)` helper.
|
||||
|
||||
---
|
||||
|
||||
## SSE indicator
|
||||
|
||||
Top-right of pages with a live stream (workspace, ralph). Three states
|
||||
indicated via a CSS-shape pulse-dot — never an emoji.
|
||||
|
||||
| State | Dot color | Label | Pulse |
|
||||
|---|---|---|---|
|
||||
| Live | `--success` | "Live" | yes |
|
||||
| Polling | `--warning` | "Polling" | no |
|
||||
| Offline | `--error` | "Offline" | no |
|
||||
|
||||
Uses the same `.pulse-dot` 6px CSS shape as `.status-pill::before`. The dot
|
||||
sits before the label, both inside a tiny `.sse-indicator` chip on
|
||||
`--bg-surface`.
|
||||
|
||||
---
|
||||
|
||||
## Modal pattern
|
||||
|
||||
Used for the planning chat, PRD viewer, log tail, propose-feature form.
|
||||
|
||||
- **Overlay**: full viewport, `background: rgba(0,0,0,0.6)`,
|
||||
`backdrop-filter: blur(4px)`, `display: flex` centered.
|
||||
- **Container** (`.modal`): `--radius-lg`, `--bg-base`, `--border`,
|
||||
max-width 720px, max-height 80vh, scroll on overflow.
|
||||
- **Header / Footer**: 1px border separators using `--border`.
|
||||
- **Focus trap**: first focusable element gets focus on open; Tab cycles
|
||||
inside the modal.
|
||||
- **ESC**: closes — but if the modal has unsaved input, prompt
|
||||
"Discard changes?" before closing. Click on overlay = same behavior.
|
||||
- **Mobile (≤640px)**: full-screen takeover. Header / footer stick; body
|
||||
scrolls. Implemented in `tokens.css` via the shared `@media (max-width:640px)`
|
||||
block.
|
||||
|
||||
---
|
||||
|
||||
## No-emoji rule
|
||||
|
||||
**No emoji anywhere in the dashboard.** This is a hard rule, not a
|
||||
preference.
|
||||
|
||||
- Buttons are **text-only**. No leading/trailing emoji decoration.
|
||||
- Status indicators use **CSS-shape colored dots** (`.pulse-dot`,
|
||||
`.status-pill::before`) — never `🟢 ⏱ 🛑 ✅` etc.
|
||||
- The login monogram is the **letter `E`** rendered in Inter 700 inside a
|
||||
square with `--accent` background. Not an emoji, not an SVG logo.
|
||||
- Where icons are needed (nav, action buttons), use **Lucide-style stroke
|
||||
SVGs inlined** — `stroke: currentColor`, `fill: none`, `stroke-width: 2`,
|
||||
`stroke-linecap: round`, `stroke-linejoin: round`. Never use emoji as a
|
||||
substitute for an icon.
|
||||
|
||||
This rule keeps the UI legible across themes, scales correctly at all sizes,
|
||||
and avoids OS-dependent rendering (Apple, Twemoji, Noto all draw the same
|
||||
emoji differently).
|
||||
|
||||
---
|
||||
|
||||
## Pages that include this system
|
||||
|
||||
Every dashboard page (`index.html`, `workspace.html`, `ralph.html`, `notes.html`,
|
||||
`habits.html`, `files.html`, `login.html`) **must** include in `<head>`:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="/echo/static/tokens.css">
|
||||
<link rel="stylesheet" href="/echo/common.css">
|
||||
```
|
||||
|
||||
In that order — tokens first so `common.css` and per-page styles can resolve
|
||||
the variables.
|
||||
464
dashboard/api.py
Normal file
464
dashboard/api.py
Normal file
@@ -0,0 +1,464 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Echo Task Board API — thin HTTP router.
|
||||
|
||||
All endpoint logic lives in `dashboard/handlers/*.py`. This file is
|
||||
responsible only for URL dispatch, CORS, JSON response helpers, and
|
||||
server bootstrap.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote as _urlquote, parse_qs, urlparse
|
||||
|
||||
# Make dashboard/ importable for the handler submodules (constants,
|
||||
# habits_helpers, handlers.*). Tests rely on this as well.
|
||||
_DASH = Path(__file__).parent
|
||||
if str(_DASH) not in sys.path:
|
||||
sys.path.insert(0, str(_DASH))
|
||||
|
||||
from constants import ( # noqa: E402 re-exported for tests
|
||||
ALLOWED_WORKSPACES,
|
||||
BASE_DIR,
|
||||
ECHO_CORE_DIR,
|
||||
ECHO_LOG_FILE,
|
||||
ECHO_SESSIONS_FILE,
|
||||
ECO_SERVICES,
|
||||
GIT_WORKSPACE,
|
||||
GITEA_ORG,
|
||||
GITEA_TOKEN,
|
||||
GITEA_URL,
|
||||
HABITS_FILE,
|
||||
KANBAN_DIR,
|
||||
NOTES_DIR,
|
||||
TOOLS_DIR,
|
||||
VENV_PYTHON,
|
||||
WORKSPACE_DIR,
|
||||
)
|
||||
from handlers.auth import AuthHandlers # noqa: E402
|
||||
from handlers.cron import CronHandlers # noqa: E402
|
||||
from handlers.eco import EcoHandlers # noqa: E402
|
||||
from handlers.files import FilesHandlers # noqa: E402
|
||||
from handlers.git import GitHandlers # noqa: E402
|
||||
from handlers.habits import HabitsHandlers # noqa: E402
|
||||
from handlers.pdf import PDFHandlers # noqa: E402
|
||||
from handlers.projects import ProjectsHandlers # noqa: E402
|
||||
from handlers.ralph import RalphHandlers # noqa: E402
|
||||
from handlers.workspace import WorkspaceHandlers # noqa: E402
|
||||
from handlers.youtube import YoutubeHandlers # noqa: E402
|
||||
|
||||
# Shared navigation injected into every served .html via <!--NAV--> marker.
|
||||
NAV_HTML = '''<header class="header">
|
||||
<a href="/echo/index.html" class="logo">
|
||||
<i data-lucide="circle-dot"></i>
|
||||
Echo
|
||||
</a>
|
||||
<nav class="nav">
|
||||
<a href="/echo/index.html" class="nav-item" data-page="index">
|
||||
<i data-lucide="layout-dashboard"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a href="/echo/workspace.html" class="nav-item" data-page="workspace">
|
||||
<i data-lucide="code"></i>
|
||||
<span>Workspace</span>
|
||||
</a>
|
||||
<a href="/echo/notes.html" class="nav-item" data-page="notes">
|
||||
<i data-lucide="file-text"></i>
|
||||
<span>KB</span>
|
||||
</a>
|
||||
<a href="/echo/habits.html" class="nav-item" data-page="habits">
|
||||
<i data-lucide="dumbbell"></i>
|
||||
<span>Habits</span>
|
||||
</a>
|
||||
<a href="/echo/files.html" class="nav-item" data-page="files">
|
||||
<i data-lucide="folder"></i>
|
||||
<span>Files</span>
|
||||
</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
|
||||
<i data-lucide="sun" id="themeIcon"></i>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
<script>
|
||||
(function(){
|
||||
var path = window.location.pathname;
|
||||
var m = path.match(/([^\\/]+?)(?:\\.html)?$/);
|
||||
var page = m ? m[1] : 'index';
|
||||
if (!page || page === 'echo') page = 'index';
|
||||
var item = document.querySelector('.nav-item[data-page="' + page + '"]');
|
||||
if (item) item.classList.add('active');
|
||||
})();
|
||||
</script>'''
|
||||
|
||||
|
||||
class TaskBoardHandler(
|
||||
AuthHandlers,
|
||||
ProjectsHandlers,
|
||||
GitHandlers,
|
||||
HabitsHandlers,
|
||||
EcoHandlers,
|
||||
FilesHandlers,
|
||||
PDFHandlers,
|
||||
YoutubeHandlers,
|
||||
WorkspaceHandlers,
|
||||
RalphHandlers,
|
||||
CronHandlers,
|
||||
SimpleHTTPRequestHandler,
|
||||
):
|
||||
"""HTTP request handler — dispatches to handler-mixin methods."""
|
||||
|
||||
# ── shared utilities ────────────────────────────────────────
|
||||
def _read_post_json(self):
|
||||
"""Read a JSON body from the POST request."""
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||
return json.loads(post_data)
|
||||
|
||||
def send_json(self, data, code=200):
|
||||
self.send_response(code)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
self.send_header('Pragma', 'no-cache')
|
||||
self.send_header('Expires', '0')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data).encode())
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(200)
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
||||
self.end_headers()
|
||||
|
||||
# ── dispatch ────────────────────────────────────────────────
|
||||
def do_GET(self):
|
||||
from datetime import datetime as _dt
|
||||
import os
|
||||
# Static assets — served directly from dashboard/static/. Handles the
|
||||
# case where the URL is hit with the /echo/ prefix intact (e.g. direct
|
||||
# localhost curl); when behind the reverse proxy that strips /echo/,
|
||||
# the request falls through to SimpleHTTPRequestHandler which serves
|
||||
# cwd/static/ naturally (cwd is set to KANBAN_DIR/dashboard).
|
||||
if self.path.startswith('/echo/static/'):
|
||||
rel = self.path[len('/echo/static/'):].split('?', 1)[0]
|
||||
file_path = os.path.join(os.path.dirname(__file__), 'static', rel)
|
||||
if os.path.isfile(file_path):
|
||||
ext = os.path.splitext(rel)[1].lstrip('.').lower()
|
||||
ctype = {
|
||||
'css': 'text/css',
|
||||
'woff2': 'font/woff2',
|
||||
'woff': 'font/woff',
|
||||
'js': 'application/javascript',
|
||||
'svg': 'image/svg+xml',
|
||||
'png': 'image/png',
|
||||
}.get(ext, 'application/octet-stream')
|
||||
with open(file_path, 'rb') as f:
|
||||
data = f.read()
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', ctype)
|
||||
self.send_header('Content-Length', str(len(data)))
|
||||
self.send_header('Cache-Control', 'public, max-age=86400')
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
else:
|
||||
self.send_error(404)
|
||||
return
|
||||
if self.path == '/api/status':
|
||||
self.send_json({'status': 'ok', 'time': _dt.now().isoformat()})
|
||||
elif self.path == '/api/git' or self.path.startswith('/api/git?'):
|
||||
self.handle_git_status()
|
||||
elif self.path == '/api/cron' or self.path.startswith('/api/cron?'):
|
||||
self.handle_cron_status()
|
||||
elif self.path == '/api/habits':
|
||||
self.handle_habits_get()
|
||||
elif self.path.startswith('/api/files'):
|
||||
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 == '/api/eco/status' or self.path.startswith('/api/eco/status?'):
|
||||
self.handle_eco_status()
|
||||
elif self.path == '/api/eco/sessions' or self.path.startswith('/api/eco/sessions?'):
|
||||
self.handle_eco_sessions()
|
||||
elif self.path.startswith('/api/eco/sessions/content'):
|
||||
self.handle_eco_session_content()
|
||||
elif self.path.startswith('/api/eco/logs'):
|
||||
self.handle_eco_logs()
|
||||
elif self.path == '/api/eco/doctor':
|
||||
self.handle_eco_doctor()
|
||||
elif self.path == '/api/ralph/status' or self.path.startswith('/api/ralph/status?'):
|
||||
self.handle_ralph_status()
|
||||
elif self.path == '/api/ralph/usage' or self.path.startswith('/api/ralph/usage?'):
|
||||
self.handle_ralph_usage()
|
||||
elif self.path == '/api/ralph/stream' or self.path.startswith('/api/ralph/stream?'):
|
||||
self.handle_ralph_stream()
|
||||
elif self.path.startswith('/api/ralph/'):
|
||||
# /api/ralph/<slug>/log or /api/ralph/<slug>/prd
|
||||
parts = self.path.split('?', 1)[0].split('/')
|
||||
# parts: ['', 'api', 'ralph', '<slug>', '<action>']
|
||||
if len(parts) >= 5:
|
||||
slug = parts[3]
|
||||
action = parts[4]
|
||||
if action == 'log':
|
||||
self.handle_ralph_log(slug)
|
||||
elif action == 'prd':
|
||||
self.handle_ralph_prd(slug)
|
||||
else:
|
||||
self.send_error(404)
|
||||
else:
|
||||
self.send_error(404)
|
||||
elif self.path == '/api/projects' or self.path.startswith('/api/projects?'):
|
||||
self.handle_unified_status()
|
||||
elif self.path == '/api/projects/signature' or self.path.startswith('/api/projects/signature?'):
|
||||
self.handle_unified_signature()
|
||||
elif self.path == '/api/projects/stream' or self.path.startswith('/api/projects/stream?'):
|
||||
self.handle_projects_stream()
|
||||
elif self.path.startswith('/api/projects/'):
|
||||
# /api/projects/<slug>/plan/(state|transcript)
|
||||
parts = self.path.split('?', 1)[0].split('/')
|
||||
# parts: ['', 'api', 'projects', '<slug>', 'plan', '<action>']
|
||||
if len(parts) >= 6 and parts[4] == 'plan':
|
||||
slug = parts[3]
|
||||
action = parts[5]
|
||||
if action == 'state':
|
||||
self.handle_plan_state(slug)
|
||||
elif action == 'transcript':
|
||||
self.handle_plan_transcript(slug)
|
||||
else:
|
||||
self.send_error(404)
|
||||
else:
|
||||
self.send_error(404)
|
||||
elif self.path in ('/', '/echo', '/echo/'):
|
||||
self.send_response(302)
|
||||
self.send_header('Location', '/echo/index.html')
|
||||
self.send_header('Content-Length', '0')
|
||||
self.end_headers()
|
||||
return
|
||||
elif self.path in ('/echo/login', '/login') or \
|
||||
self.path.startswith(('/echo/login?', '/login?')):
|
||||
# If already logged in, redirect to next (or workspace); otherwise serve login.html.
|
||||
if self._check_dashboard_cookie():
|
||||
qs = parse_qs(urlparse(self.path).query)
|
||||
next_vals = qs.get('next', [])
|
||||
nxt = next_vals[0] if next_vals else ''
|
||||
# Proxy strips /echo/ before Python, so nxt is e.g. /workspace.html.
|
||||
# Re-add the prefix so the browser lands on the right public URL.
|
||||
if nxt and nxt.startswith('/') and '://' not in nxt:
|
||||
dest = '/echo' + nxt
|
||||
else:
|
||||
dest = '/echo/workspace.html'
|
||||
self.send_response(302)
|
||||
self.send_header('Location', dest)
|
||||
self.send_header('Content-Length', '0')
|
||||
self.end_headers()
|
||||
return
|
||||
login_html = KANBAN_DIR / 'login.html'
|
||||
if login_html.is_file():
|
||||
body = login_html.read_text('utf-8').replace('<!--NAV-->', NAV_HTML).encode('utf-8')
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
||||
self.send_header('Content-Length', str(len(body)))
|
||||
self.send_header('Cache-Control', 'no-cache')
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
else:
|
||||
# Lane B2 hasn't shipped yet — return 503 with a hint.
|
||||
self.send_error(503, 'login.html not yet available')
|
||||
elif self.path == '/ralph.html' or self.path.startswith('/ralph.html?'):
|
||||
# Legacy redirect — Ralph dashboard merged into workspace.html (Lane D1).
|
||||
self.send_response(302)
|
||||
self.send_header('Location', '/echo/workspace.html')
|
||||
self.send_header('Content-Length', '0')
|
||||
self.end_headers()
|
||||
return
|
||||
elif self.path.startswith('/api/'):
|
||||
self.send_error(404)
|
||||
else:
|
||||
# Inject shared nav into served HTML pages via <!--NAV--> marker.
|
||||
rel = self.path.lstrip('/').split('?')[0]
|
||||
if rel.endswith('.html'):
|
||||
try:
|
||||
fpath = (KANBAN_DIR / rel).resolve()
|
||||
fpath.relative_to(KANBAN_DIR.resolve())
|
||||
except (ValueError, OSError):
|
||||
self.send_error(403)
|
||||
return
|
||||
if fpath.is_file():
|
||||
if fpath.name != 'login.html' and not self._check_dashboard_cookie():
|
||||
self.send_response(302)
|
||||
next_param = _urlquote(self.path, safe='/?=&#')
|
||||
self.send_header('Location', f'/echo/login?next={next_param}')
|
||||
self.send_header('Content-Length', '0')
|
||||
self.end_headers()
|
||||
return
|
||||
html = fpath.read_text('utf-8').replace('<!--NAV-->', NAV_HTML)
|
||||
body = html.encode('utf-8')
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
||||
self.send_header('Content-Length', str(len(body)))
|
||||
self.send_header('Cache-Control', 'no-cache')
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
return
|
||||
super().do_GET()
|
||||
|
||||
# POSTs that bypass the auth middleware (login itself can't require a cookie).
|
||||
UNPROTECTED_POSTS = frozenset({'/api/auth/login'})
|
||||
|
||||
def do_POST(self):
|
||||
# ── Auth middleware ────────────────────────────────────────
|
||||
# Only protect /api/* POSTs for now — older endpoints predate auth and
|
||||
# we want a single, well-defined gate. Static asset POSTs (none today)
|
||||
# would also fall through.
|
||||
path_only = self.path.split('?', 1)[0]
|
||||
if path_only.startswith('/api/') and path_only not in self.UNPROTECTED_POSTS:
|
||||
if not self._check_dashboard_cookie():
|
||||
body = b'{"error":"Unauthorized"}'
|
||||
self.send_response(401)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_header('Content-Length', str(len(body)))
|
||||
self.send_header('Cache-Control', 'no-store')
|
||||
self.end_headers()
|
||||
try:
|
||||
self.wfile.write(body)
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
return
|
||||
# CSRF: require Origin (or Referer) to be on the allowlist.
|
||||
origin = self.headers.get('Origin', '') or ''
|
||||
referer = self.headers.get('Referer', '') or ''
|
||||
allowed = ['http://127.0.0.1:8088', 'http://localhost:8088']
|
||||
dh = os.environ.get('DASHBOARD_HOST', '').strip()
|
||||
if dh:
|
||||
allowed.append(dh)
|
||||
check = origin or referer
|
||||
if check and not any(check.startswith(a) for a in allowed):
|
||||
body = b'{"error":"CSRF"}'
|
||||
self.send_response(403)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_header('Content-Length', str(len(body)))
|
||||
self.send_header('Cache-Control', 'no-store')
|
||||
self.end_headers()
|
||||
try:
|
||||
self.wfile.write(body)
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
return
|
||||
|
||||
if self.path == '/api/youtube':
|
||||
self.handle_youtube()
|
||||
elif self.path == '/api/files':
|
||||
self.handle_files_post()
|
||||
elif self.path == '/api/refresh-index':
|
||||
self.handle_refresh_index()
|
||||
elif self.path == '/api/pdf':
|
||||
self.handle_pdf_post()
|
||||
elif self.path == '/api/habits':
|
||||
self.handle_habits_post()
|
||||
elif self.path.startswith('/api/habits/') and self.path.endswith('/check'):
|
||||
self.handle_habits_check()
|
||||
elif self.path.startswith('/api/habits/') and self.path.endswith('/skip'):
|
||||
self.handle_habits_skip()
|
||||
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()
|
||||
elif self.path == '/api/eco/restart':
|
||||
self.handle_eco_restart()
|
||||
elif self.path == '/api/eco/stop':
|
||||
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()
|
||||
elif self.path == '/api/eco/restart-taskboard':
|
||||
self.handle_eco_restart_taskboard()
|
||||
elif self.path.startswith('/api/ralph/'):
|
||||
# /api/ralph/<slug>/{stop,rollback}
|
||||
parts = self.path.split('?', 1)[0].split('/')
|
||||
if len(parts) >= 5:
|
||||
slug = parts[3]
|
||||
action = parts[4]
|
||||
if action == 'stop':
|
||||
self.handle_ralph_stop(slug)
|
||||
elif action == 'rollback':
|
||||
self.handle_ralph_rollback(slug)
|
||||
else:
|
||||
self.send_error(404)
|
||||
else:
|
||||
self.send_error(404)
|
||||
elif self.path == '/api/auth/login':
|
||||
self.handle_login()
|
||||
elif self.path == '/api/auth/logout':
|
||||
self.handle_logout()
|
||||
elif self.path == '/api/projects/propose':
|
||||
self.handle_propose()
|
||||
elif self.path == '/api/projects/approve':
|
||||
self.handle_approve()
|
||||
elif self.path == '/api/projects/unapprove':
|
||||
self.handle_unapprove()
|
||||
elif self.path == '/api/projects/cancel':
|
||||
self.handle_cancel()
|
||||
elif self.path.startswith('/api/projects/'):
|
||||
# /api/projects/<slug>/plan/(start|respond|finalize|cancel|advance)
|
||||
parts = self.path.split('?', 1)[0].split('/')
|
||||
# parts: ['', 'api', 'projects', '<slug>', 'plan', '<action>']
|
||||
if len(parts) >= 6 and parts[4] == 'plan':
|
||||
slug = parts[3]
|
||||
action = parts[5]
|
||||
if action == 'start':
|
||||
self.handle_plan_start(slug)
|
||||
elif action == 'respond':
|
||||
self.handle_plan_respond(slug)
|
||||
elif action == 'finalize':
|
||||
self.handle_plan_finalize(slug)
|
||||
elif action == 'cancel':
|
||||
self.handle_plan_cancel_planning(slug)
|
||||
elif action == 'advance':
|
||||
self.handle_plan_advance(slug)
|
||||
else:
|
||||
self.send_error(404)
|
||||
else:
|
||||
self.send_error(404)
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def do_PUT(self):
|
||||
if self.path.startswith('/api/habits/'):
|
||||
self.handle_habits_put()
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def do_DELETE(self):
|
||||
if self.path.startswith('/api/habits/') and '/check' in self.path:
|
||||
self.handle_habits_uncheck()
|
||||
elif self.path.startswith('/api/habits/'):
|
||||
self.handle_habits_delete()
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import os
|
||||
port = 8088
|
||||
os.chdir(KANBAN_DIR)
|
||||
|
||||
print(f"Starting Echo Task Board API on port {port}")
|
||||
# ThreadingHTTPServer permite SSE long-lived (/api/ralph/stream) fără să
|
||||
# blocheze celelalte request-uri.
|
||||
httpd = ThreadingHTTPServer(('0.0.0.0', port), TaskBoardHandler)
|
||||
httpd.daemon_threads = True
|
||||
httpd.serve_forever()
|
||||
238
dashboard/archive/tasks-2026-01.json
Normal file
238
dashboard/archive/tasks-2026-01.json
Normal file
@@ -0,0 +1,238 @@
|
||||
{
|
||||
"month": "2025-01",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "task-001",
|
||||
"title": "Email 2FA security",
|
||||
"description": "Nu execut comenzi din email fără aprobare Telegram",
|
||||
"created": "2025-01-30",
|
||||
"completed": "2025-01-30",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-002",
|
||||
"title": "Email whitelist",
|
||||
"description": "Răspuns automat doar pentru adrese aprobate",
|
||||
"created": "2025-01-30",
|
||||
"completed": "2025-01-30",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-003",
|
||||
"title": "YouTube summarizer",
|
||||
"description": "Tool descărcare subtitrări + sumarizare",
|
||||
"created": "2025-01-30",
|
||||
"completed": "2025-01-30",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-004",
|
||||
"title": "Proactivitate în SOUL.md",
|
||||
"description": "Adăugat reguli să fiu proactiv și să propun automatizări",
|
||||
"created": "2025-01-30",
|
||||
"completed": "2025-01-30",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "task-029",
|
||||
"title": "Test sortare timestamp",
|
||||
"description": "Verificare sortare",
|
||||
"created": "2026-01-29T14:54:17Z",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-29T14:54:25Z"
|
||||
},
|
||||
{
|
||||
"id": "task-027",
|
||||
"title": "UI fixes: kanban icons + notes tags",
|
||||
"description": "Scos emoji din coloane kanban. Adăugat tag pills cu multi-select și count în notes.",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-026",
|
||||
"title": "Swipe navigation mobil",
|
||||
"description": "Swipe stânga/dreapta pentru navigare între Tasks ↔ Notes ↔ Files. Indicator dots pe mobil.",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-025",
|
||||
"title": "Notes: Accordion pe zile",
|
||||
"description": "Grupare: Azi (expanded), Ieri, Săptămâna aceasta, Mai vechi (collapsed). Click pentru expand/collapse.",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-024",
|
||||
"title": "Fix contrast dark/light mode",
|
||||
"description": "Text și borders mai vizibile, header alb în light mode, toggle temă funcțional",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-023",
|
||||
"title": "Design System Unificat",
|
||||
"description": "common.css + Lucide icons + UI modern pe toate paginile: Tasks, Notes, Files",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-022",
|
||||
"title": "Unificare stil navigare",
|
||||
"description": "Nav unificat pe toate paginile: 📋 Tasks | 📝 Notes | 📁 Files cu iconuri și stil consistent",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-021",
|
||||
"title": "UI/UX Redesign v2",
|
||||
"description": "Kanban: doar In Progress expandat. Notes: mobile tabs. Files: Browse/Editor tabs cu grid.",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-020",
|
||||
"title": "UI Responsive & Compact",
|
||||
"description": "Coloane colapsabile, task-uri compacte (click expand), sidebar toggle, Done minimizat by default",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-019",
|
||||
"title": "Comparare bilanț 12/2025 vs 12/2024",
|
||||
"description": "Doar S1002 modificat! Câmpuri noi: AN_CAEN, d_audit_intern. Raport: bilant_compare/2025_vs_2024/",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-018",
|
||||
"title": "Comparare bilanț ANAF 2024 vs 2023",
|
||||
"description": "Comparat XSD-uri S1002-S1005. Raport: anaf-monitor/bilant_compare/RAPORT_DIFERENTE_2024_vs_2023.md",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-017",
|
||||
"title": "Scrie un haiku",
|
||||
"description": "Biți în noaptea grea / Claude răspunde în liniște / Ecou digital",
|
||||
"created": "2026-01-29",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-005",
|
||||
"title": "Kanban board",
|
||||
"description": "Interfață web pentru vizualizare task-uri",
|
||||
"created": "2025-01-30",
|
||||
"priority": "high",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-008",
|
||||
"title": "YouTube Notes interface",
|
||||
"description": "Interfață pentru vizualizare notițe cu search",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-009",
|
||||
"title": "Search în notițe",
|
||||
"description": "Căutare în titlu, tags și conținut",
|
||||
"created": "2026-01-29",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "task-010",
|
||||
"title": "Sumarizare: Claude Code Do Work Pattern",
|
||||
"description": "https://youtu.be/I9-tdhxiH7w",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-011",
|
||||
"title": "File Explorer în Task Board",
|
||||
"description": "Interfață pentru browse/edit fișiere din workspace",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-013",
|
||||
"title": "Kanban interactiv cu drag & drop",
|
||||
"description": "Adăugat: drag-drop, add/edit/delete tasks, priorități, salvare automată",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-014",
|
||||
"title": "Sumarizare: It Got Worse (Clawdbot)...",
|
||||
"description": "https://youtu.be/rPAKq2oQVBs?si=6sJk41XsCrQQt6Lg",
|
||||
"created": "2026-01-29",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-015",
|
||||
"title": "Sumarizare: Greșeli post cu apă",
|
||||
"description": "https://youtu.be/4QjkI0sf64M",
|
||||
"created": "2026-01-29",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "task-016",
|
||||
"title": "Sumarizare: GSD Framework Claude Code",
|
||||
"description": "https://www.youtube.com/watch?v=l94A53kIUB0",
|
||||
"created": "2026-01-29",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "task-028",
|
||||
"title": "ANAF Monitor - verificare (test)",
|
||||
"description": "Testare manuală cron job",
|
||||
"created": "2026-01-29",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-29"
|
||||
},
|
||||
{
|
||||
"id": "task-030",
|
||||
"title": "Test task tracking",
|
||||
"description": "",
|
||||
"created": "2026-01-30T20:12:25Z",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-30T20:12:29Z"
|
||||
},
|
||||
{
|
||||
"id": "task-031",
|
||||
"title": "Fix notes tag coloring on expand",
|
||||
"description": "",
|
||||
"created": "2026-01-30T20:16:46Z",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-30T20:17:08Z"
|
||||
},
|
||||
{
|
||||
"id": "task-032",
|
||||
"title": "Fix cron jobs timezone Bucharest",
|
||||
"description": "",
|
||||
"created": "2026-01-30T20:21:26Z",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-30T20:21:44Z"
|
||||
},
|
||||
{
|
||||
"id": "task-033",
|
||||
"title": "Redirect coaching to @health, reports to @work",
|
||||
"description": "",
|
||||
"created": "2026-01-30T20:25:22Z",
|
||||
"priority": "medium",
|
||||
"completed": "2026-01-30T20:26:37Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
64
dashboard/archive/tasks-2026-02.json
Normal file
64
dashboard/archive/tasks-2026-02.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"month": "2026-02",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "task-034",
|
||||
"title": "Actualizare documentație canale agenți",
|
||||
"description": "",
|
||||
"created": "2026-02-01T12:15:41Z",
|
||||
"priority": "medium",
|
||||
"completed": "2026-02-01T12:15:44Z"
|
||||
},
|
||||
{
|
||||
"id": "task-035",
|
||||
"title": "Restructurare echipă: șterg work, unific health+growth→self",
|
||||
"description": "",
|
||||
"created": "2026-02-01T12:20:59Z",
|
||||
"priority": "medium",
|
||||
"completed": "2026-02-01T12:23:32Z"
|
||||
},
|
||||
{
|
||||
"id": "task-036",
|
||||
"title": "Unificare în 1 agent cu tehnici diminuare dezavantaje",
|
||||
"description": "",
|
||||
"created": "2026-02-01T13:27:51Z",
|
||||
"priority": "medium",
|
||||
"completed": "2026-02-01T13:30:01Z"
|
||||
},
|
||||
{
|
||||
"id": "task-037",
|
||||
"title": "Coaching dimineață - Asumarea eforturilor (Zoltan Vereș)",
|
||||
"description": "",
|
||||
"created": "2026-02-02T07:01:14Z",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "task-038",
|
||||
"title": "Raport dimineata trimis pe email",
|
||||
"description": "",
|
||||
"created": "2026-02-03T06:31:08Z",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "task-039",
|
||||
"title": "Raport seară 3 feb trimis pe email",
|
||||
"description": "",
|
||||
"created": "2026-02-03T18:01:12Z",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "task-040",
|
||||
"title": "Job night-execute: 2 video-uri YouTube procesate",
|
||||
"description": "",
|
||||
"created": "2026-02-03T21:02:31Z",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "task-041",
|
||||
"title": "Raport dimineață trimis pe email",
|
||||
"description": "",
|
||||
"created": "2026-02-04T06:31:05Z",
|
||||
"priority": "medium"
|
||||
}
|
||||
]
|
||||
}
|
||||
97
dashboard/archive_tasks.py
Normal file
97
dashboard/archive_tasks.py
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Archive old Done tasks to monthly archive files.
|
||||
Run periodically (heartbeat) to keep tasks.json small.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
TASKS_FILE = Path(__file__).parent / "tasks.json"
|
||||
ARCHIVE_DIR = Path(__file__).parent / "archive"
|
||||
DAYS_TO_KEEP = 7 # Keep Done tasks for 7 days before archiving
|
||||
|
||||
def archive_old_tasks():
|
||||
if not TASKS_FILE.exists():
|
||||
print("No tasks.json found")
|
||||
return
|
||||
|
||||
with open(TASKS_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Find Done column
|
||||
done_col = None
|
||||
for col in data['columns']:
|
||||
if col['id'] == 'done':
|
||||
done_col = col
|
||||
break
|
||||
|
||||
if not done_col:
|
||||
print("No Done column found")
|
||||
return
|
||||
|
||||
# Calculate cutoff date
|
||||
cutoff = (datetime.now() - timedelta(days=DAYS_TO_KEEP)).strftime('%Y-%m-%d')
|
||||
|
||||
# Separate old and recent tasks
|
||||
old_tasks = []
|
||||
recent_tasks = []
|
||||
|
||||
for task in done_col['tasks']:
|
||||
completed = task.get('completed', task.get('created', ''))
|
||||
if completed and completed < cutoff:
|
||||
old_tasks.append(task)
|
||||
else:
|
||||
recent_tasks.append(task)
|
||||
|
||||
if not old_tasks:
|
||||
print(f"No tasks older than {DAYS_TO_KEEP} days to archive")
|
||||
return
|
||||
|
||||
# Create archive directory
|
||||
ARCHIVE_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# Group old tasks by month
|
||||
by_month = {}
|
||||
for task in old_tasks:
|
||||
completed = task.get('completed', task.get('created', ''))[:7] # YYYY-MM
|
||||
if completed not in by_month:
|
||||
by_month[completed] = []
|
||||
by_month[completed].append(task)
|
||||
|
||||
# Write to monthly archive files
|
||||
for month, tasks in by_month.items():
|
||||
archive_file = ARCHIVE_DIR / f"tasks-{month}.json"
|
||||
|
||||
# Load existing archive
|
||||
if archive_file.exists():
|
||||
with open(archive_file, 'r') as f:
|
||||
archive = json.load(f)
|
||||
else:
|
||||
archive = {"month": month, "tasks": []}
|
||||
|
||||
# Add new tasks (avoid duplicates by ID)
|
||||
existing_ids = {t['id'] for t in archive['tasks']}
|
||||
for task in tasks:
|
||||
if task['id'] not in existing_ids:
|
||||
archive['tasks'].append(task)
|
||||
|
||||
# Save archive
|
||||
with open(archive_file, 'w') as f:
|
||||
json.dump(archive, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"Archived {len(tasks)} tasks to {archive_file.name}")
|
||||
|
||||
# Update tasks.json with only recent Done tasks
|
||||
done_col['tasks'] = recent_tasks
|
||||
data['lastUpdated'] = datetime.now().isoformat()
|
||||
|
||||
with open(TASKS_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"Kept {len(recent_tasks)} recent Done tasks, archived {len(old_tasks)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
archive_old_tasks()
|
||||
448
dashboard/common.css
Normal file
448
dashboard/common.css
Normal file
@@ -0,0 +1,448 @@
|
||||
/*
|
||||
* Echo Design System
|
||||
* Modern, minimalist, unified UI
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
CSS Variables - Design Tokens
|
||||
============================================ */
|
||||
:root {
|
||||
/* Colors - Dark theme (high contrast) */
|
||||
--bg-base: #13131a;
|
||||
--bg-surface: rgba(255, 255, 255, 0.12);
|
||||
--bg-surface-hover: rgba(255, 255, 255, 0.16);
|
||||
--bg-surface-active: rgba(255, 255, 255, 0.20);
|
||||
--bg-elevated: rgba(255, 255, 255, 0.14);
|
||||
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #f5f5f5;
|
||||
--text-muted: #e5e5e5;
|
||||
|
||||
--accent: #3b82f6;
|
||||
--accent-hover: #2563eb;
|
||||
--accent-subtle: rgba(59, 130, 246, 0.2);
|
||||
|
||||
--border: rgba(255, 255, 255, 0.3);
|
||||
--border-focus: rgba(59, 130, 246, 0.7);
|
||||
|
||||
/* Header specific */
|
||||
--header-bg: rgba(19, 19, 26, 0.95);
|
||||
|
||||
--success: #22c55e;
|
||||
--warning: #eab308;
|
||||
--error: #ef4444;
|
||||
|
||||
/* Spacing (8px grid) */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
|
||||
/* Typography */
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.875rem;
|
||||
--text-base: 1rem;
|
||||
--text-lg: 1.125rem;
|
||||
--text-xl: 1.25rem;
|
||||
|
||||
/* Radius */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 0.15s ease;
|
||||
--transition-base: 0.2s ease;
|
||||
}
|
||||
|
||||
/* Light theme */
|
||||
[data-theme="light"] {
|
||||
--bg-base: #f8f9fa;
|
||||
--bg-surface: rgba(0, 0, 0, 0.04);
|
||||
--bg-surface-hover: rgba(0, 0, 0, 0.08);
|
||||
--bg-surface-active: rgba(0, 0, 0, 0.12);
|
||||
--bg-elevated: rgba(0, 0, 0, 0.06);
|
||||
|
||||
--text-primary: #1a1a1a;
|
||||
--text-secondary: #444444;
|
||||
--text-muted: #666666;
|
||||
|
||||
--border: rgba(0, 0, 0, 0.12);
|
||||
--border-focus: rgba(59, 130, 246, 0.5);
|
||||
|
||||
--accent-subtle: rgba(59, 130, 246, 0.12);
|
||||
|
||||
/* Header light */
|
||||
--header-bg: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Reset & Base
|
||||
============================================ */
|
||||
*, *::before, *::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--bg-base);
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Header / Navigation
|
||||
============================================ */
|
||||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: var(--header-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: var(--space-3) var(--space-5);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-md);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-surface-hover);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--text-primary);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.nav-item svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Theme toggle */
|
||||
.theme-toggle {
|
||||
padding: var(--space-2);
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-surface-hover);
|
||||
}
|
||||
|
||||
.theme-toggle svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Cards
|
||||
============================================ */
|
||||
.card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-base);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Buttons
|
||||
============================================ */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Inputs
|
||||
============================================ */
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-sm);
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Select dropdowns - fix for dark mode visibility */
|
||||
select.input {
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
|
||||
select.input option {
|
||||
background: var(--bg-base);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Tags / Badges
|
||||
============================================ */
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-surface);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
padding: 2px 6px;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-surface-active);
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Grid Layouts
|
||||
============================================ */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.grid-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.grid-auto { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
|
||||
|
||||
/* ============================================
|
||||
Status Colors
|
||||
============================================ */
|
||||
.status-success { color: var(--success); }
|
||||
.status-warning { color: var(--warning); }
|
||||
.status-error { color: var(--error); }
|
||||
|
||||
/* ============================================
|
||||
Scrollbar
|
||||
============================================ */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-surface-active);
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Responsive
|
||||
============================================ */
|
||||
@media (max-width: 768px) {
|
||||
/* Larger base font for mobile readability */
|
||||
html {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.nav-item span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: var(--space-2);
|
||||
}
|
||||
|
||||
.grid-2, .grid-3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* Larger touch targets */
|
||||
.btn, .input, .tag {
|
||||
min-height: 44px;
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Utilities
|
||||
============================================ */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.flex { display: flex; }
|
||||
.flex-col { flex-direction: column; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.gap-2 { gap: var(--space-2); }
|
||||
.gap-4 { gap: var(--space-4); }
|
||||
39
dashboard/constants.py
Normal file
39
dashboard/constants.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Shared path constants + .env loading for the dashboard package.
|
||||
|
||||
All path constants are centralised here so handlers can import them via
|
||||
`from constants import BASE_DIR, ...` (dashboard/ is placed on sys.path by
|
||||
api.py on startup).
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).parent.parent # echo-core/
|
||||
TOOLS_DIR = BASE_DIR / 'tools'
|
||||
NOTES_DIR = BASE_DIR / 'memory' / 'kb' / 'youtube'
|
||||
KANBAN_DIR = BASE_DIR / 'dashboard'
|
||||
WORKSPACE_DIR = Path('/home/moltbot/workspace')
|
||||
HABITS_FILE = KANBAN_DIR / 'habits.json'
|
||||
|
||||
# Eco (echo-core) constants
|
||||
ECO_SERVICES = ['echo-core', 'echo-whatsapp-bridge', 'echo-taskboard']
|
||||
ECHO_CORE_DIR = BASE_DIR # same as BASE_DIR post-consolidation
|
||||
ECHO_LOG_FILE = ECHO_CORE_DIR / 'logs' / 'echo-core.log'
|
||||
ECHO_SESSIONS_FILE = ECHO_CORE_DIR / 'sessions' / 'active.json'
|
||||
|
||||
# Git + workspace sandbox
|
||||
GIT_WORKSPACE = BASE_DIR # was '/home/moltbot/clawd'
|
||||
ALLOWED_WORKSPACES = [BASE_DIR, WORKSPACE_DIR] # was [clawd, workspace] — clawd dropped
|
||||
VENV_PYTHON = BASE_DIR / '.venv' / 'bin' / 'python3'
|
||||
|
||||
# ── .env loading ───────────────────────────────────────────────────
|
||||
_env_file = KANBAN_DIR / '.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', '')
|
||||
17
dashboard/echo-taskboard.service
Normal file
17
dashboard/echo-taskboard.service
Normal file
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=Echo Task Board API (dashboard)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/moltbot/echo-core/dashboard
|
||||
ExecStart=/home/moltbot/echo-core/.venv/bin/python3 /home/moltbot/echo-core/dashboard/api.py
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
Environment=PATH=/home/moltbot/.local/bin:/usr/local/bin:/usr/bin:/bin
|
||||
StandardOutput=append:/home/moltbot/echo-core/logs/echo-taskboard.log
|
||||
StandardError=append:/home/moltbot/echo-core/logs/echo-taskboard.log
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
4
dashboard/favicon.svg
Normal file
4
dashboard/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<circle cx="16" cy="16" r="14" fill="none" stroke="#3b82f6" stroke-width="2.5"/>
|
||||
<circle cx="16" cy="16" r="3" fill="#3b82f6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 200 B |
1904
dashboard/files.html
Normal file
1904
dashboard/files.html
Normal file
File diff suppressed because it is too large
Load Diff
471
dashboard/grup-sprijin.html
Normal file
471
dashboard/grup-sprijin.html
Normal file
@@ -0,0 +1,471 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Echo · Grup Sprijin</title>
|
||||
<link rel="stylesheet" href="/echo/common.css">
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<style>
|
||||
.main {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-6);
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: var(--text-sm);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.item-card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.item-card:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.item-card.used {
|
||||
opacity: 0.7;
|
||||
border-left: 3px solid var(--success);
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.item-type {
|
||||
font-size: var(--text-xs);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.item-type.exercitiu { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
|
||||
.item-type.meditatie { background: rgba(139, 92, 246, 0.2); color: #8b5cf6; }
|
||||
.item-type.intrebare { background: rgba(20, 184, 166, 0.2); color: #14b8a6; }
|
||||
.item-type.reflectie { background: rgba(249, 115, 22, 0.2); color: #f97316; }
|
||||
|
||||
.item-tags {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
flex-wrap: wrap;
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: var(--text-xs);
|
||||
padding: 2px 6px;
|
||||
background: var(--bg-surface-hover);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.item-used {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--success);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal.open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #1a1a2e;
|
||||
border-radius: var(--radius-lg);
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-5);
|
||||
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
[data-theme="light"] .modal-content {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: var(--space-1);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
margin-top: var(--space-4);
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
margin-bottom: var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
color: #ef4444;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!--NAV-->
|
||||
|
||||
<main class="main">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Grup Sprijin - Exerciții & Întrebări</h1>
|
||||
<div class="search-bar">
|
||||
<input type="text" class="input" placeholder="Caută..." id="searchInput">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats" id="stats"></div>
|
||||
|
||||
<div class="fise-section" id="fiseSection" style="margin-bottom: var(--space-5); display: none;">
|
||||
<h2 style="font-size: var(--text-lg); margin-bottom: var(--space-3); color: var(--text-primary);">Fișe întâlniri</h2>
|
||||
<div class="fise-list" id="fiseList" style="display: flex; gap: var(--space-2); flex-wrap: wrap;"></div>
|
||||
</div>
|
||||
|
||||
<div class="filters" id="filters">
|
||||
<button class="filter-btn active" data-filter="all">Toate</button>
|
||||
<button class="filter-btn" data-filter="exercitiu">Exerciții</button>
|
||||
<button class="filter-btn" data-filter="meditatie">Meditații</button>
|
||||
<button class="filter-btn" data-filter="intrebare">Întrebări</button>
|
||||
<button class="filter-btn" data-filter="reflectie">Reflecții</button>
|
||||
<button class="filter-btn" data-filter="unused">Nefolosite</button>
|
||||
<button class="filter-btn" data-filter="used">Folosite</button>
|
||||
</div>
|
||||
|
||||
<div class="items-grid" id="itemsGrid">
|
||||
<p>Se încarcă...</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="modal" id="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h2 class="modal-title" id="modalTitle"></h2>
|
||||
<span class="item-type" id="modalType"></span>
|
||||
</div>
|
||||
<button class="modal-close" onclick="closeModal()">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modalBody"></div>
|
||||
<div class="item-tags" id="modalTags"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-primary" id="markUsedBtn" onclick="toggleUsed()">Marchează folosit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Theme
|
||||
function toggleTheme() {
|
||||
const body = document.body;
|
||||
const current = body.getAttribute('data-theme') || 'dark';
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
body.setAttribute('data-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
updateThemeIcon();
|
||||
}
|
||||
|
||||
function updateThemeIcon() {
|
||||
const theme = document.body.getAttribute('data-theme') || 'dark';
|
||||
const icon = document.getElementById('themeIcon');
|
||||
if (icon) {
|
||||
icon.setAttribute('data-lucide', theme === 'dark' ? 'sun' : 'moon');
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
// Load saved theme
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
document.body.setAttribute('data-theme', savedTheme);
|
||||
|
||||
// Data
|
||||
const API_BASE = window.location.pathname.includes('/echo/') ? '/echo' : '';
|
||||
let items = [];
|
||||
let currentFilter = 'all';
|
||||
let currentItem = null;
|
||||
|
||||
async function loadItems() {
|
||||
try {
|
||||
const response = await fetch('grup-sprijin/index.json?t=' + Date.now());
|
||||
if (!response.ok) throw new Error('Nu am găsit fișierul');
|
||||
items = await response.json();
|
||||
render();
|
||||
} catch (e) {
|
||||
console.error('Error loading items:', e);
|
||||
document.getElementById('itemsGrid').innerHTML = `
|
||||
<div class="error-msg">
|
||||
Eroare la încărcare: ${e.message}<br>
|
||||
<small>Verifică dacă fișierul grup-sprijin/index.json există</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
const search = document.getElementById('searchInput').value.toLowerCase();
|
||||
|
||||
let filtered = items.filter(item => {
|
||||
if (search && !item.title.toLowerCase().includes(search) &&
|
||||
!item.content.toLowerCase().includes(search) &&
|
||||
!item.tags.some(t => t.toLowerCase().includes(search))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentFilter === 'all') return true;
|
||||
if (currentFilter === 'used') return item.used;
|
||||
if (currentFilter === 'unused') return !item.used;
|
||||
return item.type === currentFilter;
|
||||
});
|
||||
|
||||
const total = items.length;
|
||||
const used = items.filter(i => i.used).length;
|
||||
document.getElementById('stats').innerHTML = `
|
||||
<span>Total: ${total}</span>
|
||||
<span>Folosite: ${used}</span>
|
||||
<span>Nefolosite: ${total - used}</span>
|
||||
`;
|
||||
|
||||
const grid = document.getElementById('itemsGrid');
|
||||
if (filtered.length === 0) {
|
||||
grid.innerHTML = '<p style="color: var(--text-muted);">Niciun rezultat</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = filtered.map(item => `
|
||||
<div class="item-card ${item.used ? 'used' : ''}" onclick="openModal('${item.id}')">
|
||||
<div class="item-header">
|
||||
<span class="item-title">${item.title}</span>
|
||||
<span class="item-type ${item.type}">${item.type}</span>
|
||||
</div>
|
||||
<div class="item-tags">
|
||||
${item.tags.map(t => `<span class="tag">${t}</span>`).join('')}
|
||||
</div>
|
||||
${item.used ? `<div class="item-used">✓ Folosit: ${item.used}</div>` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
function openModal(id) {
|
||||
currentItem = items.find(i => i.id === id);
|
||||
if (!currentItem) return;
|
||||
|
||||
document.getElementById('modalTitle').textContent = currentItem.title;
|
||||
document.getElementById('modalType').textContent = currentItem.type;
|
||||
document.getElementById('modalType').className = `item-type ${currentItem.type}`;
|
||||
document.getElementById('modalBody').textContent = currentItem.content;
|
||||
document.getElementById('modalTags').innerHTML = currentItem.tags.map(t => `<span class="tag">${t}</span>`).join('');
|
||||
document.getElementById('markUsedBtn').textContent = currentItem.used ? 'Marchează nefolosit' : 'Marchează folosit';
|
||||
|
||||
document.getElementById('modal').classList.add('open');
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modal').classList.remove('open');
|
||||
currentItem = null;
|
||||
}
|
||||
|
||||
async function toggleUsed() {
|
||||
if (!currentItem) return;
|
||||
|
||||
const idx = items.findIndex(i => i.id === currentItem.id);
|
||||
if (idx === -1) return;
|
||||
|
||||
if (items[idx].used) {
|
||||
items[idx].used = null;
|
||||
} else {
|
||||
items[idx].used = new Date().toLocaleDateString('ro-RO');
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch(`${API_BASE}/api/files`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
path: 'grup-sprijin/index.json',
|
||||
content: JSON.stringify(items, null, 2)
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error saving:', e);
|
||||
}
|
||||
|
||||
closeModal();
|
||||
render();
|
||||
}
|
||||
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentFilter = btn.dataset.filter;
|
||||
render();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('searchInput').addEventListener('input', render);
|
||||
|
||||
document.getElementById('modal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'modal') closeModal();
|
||||
});
|
||||
|
||||
// Load fise
|
||||
async function loadFise() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/files?path=kanban/grup-sprijin&action=list`);
|
||||
const data = await response.json();
|
||||
if (data.items) {
|
||||
const fise = data.items.filter(f => f.name.startsWith('fisa-') && f.name.endsWith('.md'));
|
||||
if (fise.length > 0) {
|
||||
document.getElementById('fiseSection').style.display = 'block';
|
||||
document.getElementById('fiseList').innerHTML = fise.map(f => `
|
||||
<a href="/echo/files.html#kanban/grup-sprijin/${f.name}" class="filter-btn" style="text-decoration: none;">
|
||||
${f.name.replace('fisa-', '').replace('.md', '')}
|
||||
</a>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('No fise yet');
|
||||
}
|
||||
}
|
||||
|
||||
// Init
|
||||
loadItems();
|
||||
loadFise();
|
||||
lucide.createIcons();
|
||||
updateThemeIcon();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
3460
dashboard/habits.html
Normal file
3460
dashboard/habits.html
Normal file
File diff suppressed because it is too large
Load Diff
131
dashboard/habits.json
Normal file
131
dashboard/habits.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"lastUpdated": "2026-05-27T15:16:49.070154",
|
||||
"habits": [
|
||||
{
|
||||
"id": "95c15eef-3a14-4985-a61e-0b64b72851b0",
|
||||
"name": "Bazin \u0219i Saun\u0103",
|
||||
"category": "health",
|
||||
"color": "#EF4444",
|
||||
"icon": "target",
|
||||
"priority": 50,
|
||||
"notes": "",
|
||||
"reminderTime": "19:00",
|
||||
"frequency": {
|
||||
"type": "x_per_week",
|
||||
"count": 5
|
||||
},
|
||||
"streak": {
|
||||
"current": 1,
|
||||
"best": 6,
|
||||
"lastCheckIn": "2026-05-27"
|
||||
},
|
||||
"lives": 2,
|
||||
"completions": [
|
||||
{
|
||||
"date": "2026-02-11",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-13",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-14",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-15",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-16",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-17",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-18",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-23",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-03-31",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-27",
|
||||
"type": "check"
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-02-11T00:54:03.447063",
|
||||
"updatedAt": "2026-05-27T15:16:49.070154",
|
||||
"lastLivesAward": "2026-02-23"
|
||||
},
|
||||
{
|
||||
"id": "ceddaa7e-caf9-4038-94bb-da486c586bf8",
|
||||
"name": "Fotocitire",
|
||||
"category": "growth",
|
||||
"color": "#10B981",
|
||||
"icon": "camera",
|
||||
"priority": 30,
|
||||
"notes": "",
|
||||
"reminderTime": "",
|
||||
"frequency": {
|
||||
"type": "x_per_week",
|
||||
"count": 3
|
||||
},
|
||||
"streak": {
|
||||
"current": 1,
|
||||
"best": 6,
|
||||
"lastCheckIn": "2026-04-29"
|
||||
},
|
||||
"lives": 4,
|
||||
"completions": [
|
||||
{
|
||||
"date": "2026-02-11",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-13",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-14",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-15",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-16",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-17",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-18",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-23",
|
||||
"type": "check"
|
||||
},
|
||||
{
|
||||
"date": "2026-04-29",
|
||||
"type": "check"
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-02-11T01:58:44.779904",
|
||||
"updatedAt": "2026-04-29T05:30:59.129949",
|
||||
"lastLivesAward": "2026-02-23"
|
||||
}
|
||||
]
|
||||
}
|
||||
387
dashboard/habits_helpers.py
Normal file
387
dashboard/habits_helpers.py
Normal file
@@ -0,0 +1,387 @@
|
||||
"""
|
||||
Habit Tracker Helper Functions
|
||||
|
||||
This module provides core helper functions for calculating streaks,
|
||||
checking relevance, and computing stats for habits.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
|
||||
def calculate_streak(habit: Dict[str, Any]) -> int:
|
||||
"""
|
||||
Calculate the current streak for a habit based on its frequency type.
|
||||
Skips maintain the streak (don't break it) but don't count toward the total.
|
||||
|
||||
Args:
|
||||
habit: Dict containing habit data with frequency, completions, etc.
|
||||
|
||||
Returns:
|
||||
int: Current streak count (days, weeks, or months depending on frequency)
|
||||
"""
|
||||
frequency_type = habit.get("frequency", {}).get("type", "daily")
|
||||
completions = habit.get("completions", [])
|
||||
|
||||
if not completions:
|
||||
return 0
|
||||
|
||||
# Sort completions by date (newest first)
|
||||
sorted_completions = sorted(
|
||||
[c for c in completions if c.get("date")],
|
||||
key=lambda x: x["date"],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
if not sorted_completions:
|
||||
return 0
|
||||
|
||||
if frequency_type == "daily":
|
||||
return _calculate_daily_streak(sorted_completions)
|
||||
elif frequency_type == "specific_days":
|
||||
return _calculate_specific_days_streak(habit, sorted_completions)
|
||||
elif frequency_type == "x_per_week":
|
||||
return _calculate_x_per_week_streak(habit, sorted_completions)
|
||||
elif frequency_type == "weekly":
|
||||
return _calculate_weekly_streak(sorted_completions)
|
||||
elif frequency_type == "monthly":
|
||||
return _calculate_monthly_streak(sorted_completions)
|
||||
elif frequency_type == "custom":
|
||||
return _calculate_custom_streak(habit, sorted_completions)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _calculate_daily_streak(completions: List[Dict[str, Any]]) -> int:
|
||||
"""
|
||||
Calculate streak for daily habits (consecutive days).
|
||||
Skips maintain the streak (don't break it) but don't count toward the total.
|
||||
"""
|
||||
streak = 0
|
||||
today = datetime.now().date()
|
||||
expected_date = today
|
||||
|
||||
for completion in completions:
|
||||
completion_date = datetime.fromisoformat(completion["date"]).date()
|
||||
completion_type = completion.get("type", "check")
|
||||
|
||||
if completion_date == expected_date:
|
||||
# Only count 'check' completions toward streak total
|
||||
# 'skip' completions maintain the streak but don't extend it
|
||||
if completion_type == "check":
|
||||
streak += 1
|
||||
expected_date = completion_date - timedelta(days=1)
|
||||
elif completion_date < expected_date:
|
||||
# Gap found, streak breaks
|
||||
break
|
||||
|
||||
return streak
|
||||
|
||||
|
||||
def _calculate_specific_days_streak(habit: Dict[str, Any], completions: List[Dict[str, Any]]) -> int:
|
||||
"""Calculate streak for specific days habits (only count relevant days)."""
|
||||
relevant_days = set(habit.get("frequency", {}).get("days", []))
|
||||
if not relevant_days:
|
||||
return 0
|
||||
|
||||
streak = 0
|
||||
today = datetime.now().date()
|
||||
current_date = today
|
||||
|
||||
# Find the most recent relevant day
|
||||
while current_date.weekday() not in relevant_days:
|
||||
current_date -= timedelta(days=1)
|
||||
|
||||
for completion in completions:
|
||||
completion_date = datetime.fromisoformat(completion["date"]).date()
|
||||
|
||||
if completion_date == current_date:
|
||||
streak += 1
|
||||
# Move to previous relevant day
|
||||
current_date -= timedelta(days=1)
|
||||
while current_date.weekday() not in relevant_days:
|
||||
current_date -= timedelta(days=1)
|
||||
elif completion_date < current_date:
|
||||
# Check if we missed a relevant day
|
||||
temp_date = current_date
|
||||
found_gap = False
|
||||
while temp_date > completion_date:
|
||||
if temp_date.weekday() in relevant_days:
|
||||
found_gap = True
|
||||
break
|
||||
temp_date -= timedelta(days=1)
|
||||
if found_gap:
|
||||
break
|
||||
|
||||
return streak
|
||||
|
||||
|
||||
def _calculate_x_per_week_streak(habit: Dict[str, Any], completions: List[Dict[str, Any]]) -> int:
|
||||
"""Calculate streak for x_per_week habits (consecutive days with check-ins).
|
||||
|
||||
For x_per_week habits, streak counts consecutive DAYS with check-ins,
|
||||
not consecutive weeks meeting the target. The weekly target (e.g., 4/week)
|
||||
is a goal, but streak measures the chain of check-in days.
|
||||
"""
|
||||
# Use the same logic as daily habits - count consecutive check-in days
|
||||
return _calculate_daily_streak(completions)
|
||||
|
||||
|
||||
def _calculate_weekly_streak(completions: List[Dict[str, Any]]) -> int:
|
||||
"""Calculate streak for weekly habits (consecutive days with check-ins).
|
||||
|
||||
For weekly habits, streak counts consecutive DAYS with check-ins,
|
||||
just like daily habits. The weekly frequency just means you should
|
||||
check in at least once per week.
|
||||
"""
|
||||
return _calculate_daily_streak(completions)
|
||||
|
||||
|
||||
def _calculate_monthly_streak(completions: List[Dict[str, Any]]) -> int:
|
||||
"""Calculate streak for monthly habits (consecutive days with check-ins).
|
||||
|
||||
For monthly habits, streak counts consecutive DAYS with check-ins,
|
||||
just like daily habits. The monthly frequency just means you should
|
||||
check in at least once per month.
|
||||
"""
|
||||
return _calculate_daily_streak(completions)
|
||||
|
||||
|
||||
def _calculate_custom_streak(habit: Dict[str, Any], completions: List[Dict[str, Any]]) -> int:
|
||||
"""Calculate streak for custom interval habits (every X days)."""
|
||||
interval = habit.get("frequency", {}).get("interval", 1)
|
||||
if interval <= 0:
|
||||
return 0
|
||||
|
||||
streak = 0
|
||||
expected_date = datetime.now().date()
|
||||
|
||||
for completion in completions:
|
||||
completion_date = datetime.fromisoformat(completion["date"]).date()
|
||||
|
||||
# Allow completion within the interval window
|
||||
days_diff = (expected_date - completion_date).days
|
||||
if 0 <= days_diff <= interval - 1:
|
||||
streak += 1
|
||||
expected_date = completion_date - timedelta(days=interval)
|
||||
else:
|
||||
break
|
||||
|
||||
return streak
|
||||
|
||||
|
||||
def should_check_today(habit: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Check if a habit is relevant for today based on its frequency type.
|
||||
|
||||
Args:
|
||||
habit: Dict containing habit data with frequency settings
|
||||
|
||||
Returns:
|
||||
bool: True if the habit should be checked today
|
||||
"""
|
||||
frequency_type = habit.get("frequency", {}).get("type", "daily")
|
||||
today = datetime.now().date()
|
||||
weekday = today.weekday() # 0=Monday, 6=Sunday
|
||||
|
||||
if frequency_type == "daily":
|
||||
return True
|
||||
|
||||
elif frequency_type == "specific_days":
|
||||
relevant_days = set(habit.get("frequency", {}).get("days", []))
|
||||
return weekday in relevant_days
|
||||
|
||||
elif frequency_type == "x_per_week":
|
||||
# Always relevant for x_per_week (can check any day)
|
||||
return True
|
||||
|
||||
elif frequency_type == "weekly":
|
||||
# Always relevant (can check any day of the week)
|
||||
return True
|
||||
|
||||
elif frequency_type == "monthly":
|
||||
# Always relevant (can check any day of the month)
|
||||
return True
|
||||
|
||||
elif frequency_type == "custom":
|
||||
# Check if enough days have passed since last completion
|
||||
completions = habit.get("completions", [])
|
||||
if not completions:
|
||||
return True
|
||||
|
||||
interval = habit.get("frequency", {}).get("interval", 1)
|
||||
last_completion = max(completions, key=lambda x: x.get("date", ""))
|
||||
last_date = datetime.fromisoformat(last_completion["date"]).date()
|
||||
days_since = (today - last_date).days
|
||||
|
||||
return days_since >= interval
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_completion_rate(habit: Dict[str, Any], days: int = 30) -> float:
|
||||
"""
|
||||
Calculate the completion rate as a percentage over the last N days.
|
||||
|
||||
Args:
|
||||
habit: Dict containing habit data
|
||||
days: Number of days to look back (default 30)
|
||||
|
||||
Returns:
|
||||
float: Completion rate as percentage (0-100)
|
||||
"""
|
||||
frequency_type = habit.get("frequency", {}).get("type", "daily")
|
||||
completions = habit.get("completions", [])
|
||||
|
||||
today = datetime.now().date()
|
||||
start_date = today - timedelta(days=days - 1)
|
||||
|
||||
# Count relevant days and checked days
|
||||
relevant_days = 0
|
||||
checked_dates = set()
|
||||
|
||||
for completion in completions:
|
||||
completion_date = datetime.fromisoformat(completion["date"]).date()
|
||||
if start_date <= completion_date <= today:
|
||||
checked_dates.add(completion_date)
|
||||
|
||||
# Calculate relevant days based on frequency type
|
||||
if frequency_type == "daily":
|
||||
relevant_days = days
|
||||
|
||||
elif frequency_type == "specific_days":
|
||||
relevant_day_set = set(habit.get("frequency", {}).get("days", []))
|
||||
current = start_date
|
||||
while current <= today:
|
||||
if current.weekday() in relevant_day_set:
|
||||
relevant_days += 1
|
||||
current += timedelta(days=1)
|
||||
|
||||
elif frequency_type == "x_per_week":
|
||||
target_per_week = habit.get("frequency", {}).get("count", 1)
|
||||
num_weeks = days // 7
|
||||
relevant_days = num_weeks * target_per_week
|
||||
|
||||
elif frequency_type == "weekly":
|
||||
num_weeks = days // 7
|
||||
relevant_days = num_weeks
|
||||
|
||||
elif frequency_type == "monthly":
|
||||
num_months = days // 30
|
||||
relevant_days = num_months
|
||||
|
||||
elif frequency_type == "custom":
|
||||
interval = habit.get("frequency", {}).get("interval", 1)
|
||||
relevant_days = days // interval if interval > 0 else 0
|
||||
|
||||
if relevant_days == 0:
|
||||
return 0.0
|
||||
|
||||
checked_days = len(checked_dates)
|
||||
return (checked_days / relevant_days) * 100
|
||||
|
||||
|
||||
def get_weekly_summary(habit: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""
|
||||
Get a summary of the current week showing status for each day.
|
||||
|
||||
Args:
|
||||
habit: Dict containing habit data
|
||||
|
||||
Returns:
|
||||
Dict mapping day names to status: "checked", "skipped", "missed", or "upcoming"
|
||||
"""
|
||||
frequency_type = habit.get("frequency", {}).get("type", "daily")
|
||||
completions = habit.get("completions", [])
|
||||
|
||||
today = datetime.now().date()
|
||||
|
||||
# Start of current week (Monday)
|
||||
start_of_week = today - timedelta(days=today.weekday())
|
||||
|
||||
# Create completion map
|
||||
completion_map = {}
|
||||
for completion in completions:
|
||||
completion_date = datetime.fromisoformat(completion["date"]).date()
|
||||
if completion_date >= start_of_week:
|
||||
completion_type = completion.get("type", "check")
|
||||
completion_map[completion_date] = completion_type
|
||||
|
||||
# Build summary for each day of the week
|
||||
summary = {}
|
||||
day_names = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||
|
||||
for i, day_name in enumerate(day_names):
|
||||
day_date = start_of_week + timedelta(days=i)
|
||||
|
||||
if day_date > today:
|
||||
summary[day_name] = "upcoming"
|
||||
elif day_date in completion_map:
|
||||
if completion_map[day_date] == "skip":
|
||||
summary[day_name] = "skipped"
|
||||
else:
|
||||
summary[day_name] = "checked"
|
||||
else:
|
||||
# Check if this day was relevant
|
||||
if frequency_type == "specific_days":
|
||||
relevant_days = set(habit.get("frequency", {}).get("days", []))
|
||||
if day_date.weekday() not in relevant_days:
|
||||
summary[day_name] = "not_relevant"
|
||||
else:
|
||||
summary[day_name] = "missed"
|
||||
else:
|
||||
summary[day_name] = "missed"
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
def check_and_award_weekly_lives(habit: Dict[str, Any]) -> tuple[int, bool]:
|
||||
"""
|
||||
Check if habit qualifies for weekly lives recovery and award +1 life if eligible.
|
||||
|
||||
Awards +1 life if:
|
||||
- At least one check-in in the previous week (Monday-Sunday)
|
||||
- Not already awarded this week
|
||||
|
||||
Args:
|
||||
habit: Dict containing habit data with completions and lastLivesAward
|
||||
|
||||
Returns:
|
||||
tuple[int, bool]: (new_lives_count, was_awarded)
|
||||
"""
|
||||
completions = habit.get("completions", [])
|
||||
current_lives = habit.get("lives", 3)
|
||||
|
||||
today = datetime.now().date()
|
||||
|
||||
# Calculate current week start (Monday 00:00)
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
|
||||
# Check if already awarded this week
|
||||
last_lives_award = habit.get("lastLivesAward")
|
||||
if last_lives_award:
|
||||
last_award_date = datetime.fromisoformat(last_lives_award).date()
|
||||
if last_award_date >= current_week_start:
|
||||
# Already awarded this week
|
||||
return (current_lives, False)
|
||||
|
||||
# Calculate previous week boundaries
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
previous_week_end = current_week_start - timedelta(days=1)
|
||||
|
||||
# Count check-ins in previous week
|
||||
checkins_in_previous_week = 0
|
||||
for completion in completions:
|
||||
completion_date = datetime.fromisoformat(completion["date"]).date()
|
||||
completion_type = completion.get("type", "check")
|
||||
|
||||
if previous_week_start <= completion_date <= previous_week_end:
|
||||
if completion_type == "check":
|
||||
checkins_in_previous_week += 1
|
||||
|
||||
# Award life if at least 1 check-in found
|
||||
if checkins_in_previous_week >= 1:
|
||||
new_lives = current_lives + 1
|
||||
return (new_lives, True)
|
||||
|
||||
return (current_lives, False)
|
||||
7
dashboard/handlers/__init__.py
Normal file
7
dashboard/handlers/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Handler mixin modules for the Echo Task Board API.
|
||||
|
||||
Each module exposes a mixin class whose methods plug into
|
||||
`TaskBoardHandler` (defined in dashboard/api.py). This keeps
|
||||
api.py as a thin HTTP router while each concern lives in its
|
||||
own small module.
|
||||
"""
|
||||
54
dashboard/handlers/_validators.py
Normal file
54
dashboard/handlers/_validators.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""Shared validation helpers for dashboard handlers."""
|
||||
import json
|
||||
import re
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
|
||||
_SLUG_RE = re.compile(r'^[a-z0-9][a-z0-9\-_]{1,38}[a-z0-9]$')
|
||||
|
||||
|
||||
def validate_slug(slug: str) -> str | None:
|
||||
"""Returns error message or None if valid."""
|
||||
if not slug:
|
||||
return "slug required"
|
||||
if not _SLUG_RE.match(slug):
|
||||
return "slug must be 3-40 chars, lowercase alphanumeric + hyphens/underscores"
|
||||
return None
|
||||
|
||||
|
||||
def validate_description(desc: str) -> str | None:
|
||||
"""Returns error message or None if valid. Min 10 chars, max 500."""
|
||||
if not desc or len(desc.strip()) < 10:
|
||||
return "description must be at least 10 characters"
|
||||
if len(desc) > 500:
|
||||
return "description must be at most 500 characters"
|
||||
return None
|
||||
|
||||
|
||||
def parse_json_body(handler: BaseHTTPRequestHandler) -> dict | None:
|
||||
"""Parse JSON body from request. Returns None on failure (sends 400)."""
|
||||
try:
|
||||
length = int(handler.headers.get('Content-Length', '0') or '0')
|
||||
except (TypeError, ValueError):
|
||||
length = 0
|
||||
|
||||
def _send_error(msg: str) -> None:
|
||||
sender = getattr(handler, 'send_json', None)
|
||||
if callable(sender):
|
||||
sender({'error': msg}, 400)
|
||||
return
|
||||
body = json.dumps({'error': msg}).encode()
|
||||
handler.send_response(400)
|
||||
handler.send_header('Content-Type', 'application/json')
|
||||
handler.send_header('Content-Length', str(len(body)))
|
||||
handler.end_headers()
|
||||
handler.wfile.write(body)
|
||||
|
||||
if length <= 0:
|
||||
_send_error('empty body')
|
||||
return None
|
||||
try:
|
||||
raw = handler.rfile.read(length)
|
||||
return json.loads(raw.decode('utf-8'))
|
||||
except (ValueError, json.JSONDecodeError, UnicodeDecodeError):
|
||||
_send_error('invalid JSON body')
|
||||
return None
|
||||
174
dashboard/handlers/auth.py
Normal file
174
dashboard/handlers/auth.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""Cookie-based authentication for the unified dashboard.
|
||||
|
||||
This mixin provides:
|
||||
|
||||
- POST /api/auth/login — exchanges a token (form body) for a cookie.
|
||||
- POST /api/auth/logout — clears the cookie.
|
||||
- _check_dashboard_cookie — used by the global POST middleware (and the
|
||||
SSE GET endpoint) to gate access.
|
||||
|
||||
`DASHBOARD_TOKEN` is read once from `dashboard/.env` (loaded into
|
||||
`os.environ` by `dashboard/constants.py` at import time). When the token is
|
||||
not configured we generate a random one at startup, stash it in-process,
|
||||
and warn loudly to stderr — this means the dashboard is reachable from
|
||||
localhost only with a freshly-printed token (printed once at boot).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# 30 days
|
||||
_COOKIE_MAX_AGE = 60 * 60 * 24 * 30
|
||||
_COOKIE_NAME = "dashboard"
|
||||
_COOKIE_PATH = "/echo/"
|
||||
|
||||
# Module-level cache for the resolved token. Set lazily on first call so
|
||||
# importing this module doesn't have a side effect at process boot.
|
||||
_DASHBOARD_TOKEN: str | None = None
|
||||
|
||||
|
||||
def _get_dashboard_token() -> str:
|
||||
"""Return the dashboard token (cached). Generates a random one if absent.
|
||||
|
||||
`dashboard/constants.py` already loads `dashboard/.env` into os.environ at
|
||||
import time, so by the time this is called the value (if present) is in
|
||||
`os.environ['DASHBOARD_TOKEN']`. If missing, we mint a 32-byte URL-safe
|
||||
token and warn — operators must read it from the log to log in.
|
||||
"""
|
||||
global _DASHBOARD_TOKEN
|
||||
if _DASHBOARD_TOKEN is not None:
|
||||
return _DASHBOARD_TOKEN
|
||||
token = os.environ.get("DASHBOARD_TOKEN", "").strip()
|
||||
if not token:
|
||||
token = secrets.token_urlsafe(32)
|
||||
msg = (
|
||||
"[auth] DASHBOARD_TOKEN not set in dashboard/.env — generated a "
|
||||
f"random token for this process: {token}\n"
|
||||
" Add `DASHBOARD_TOKEN=<value>` to dashboard/.env to make it "
|
||||
"stable across restarts.\n"
|
||||
)
|
||||
print(msg, file=sys.stderr, flush=True)
|
||||
log.warning("DASHBOARD_TOKEN not configured — using ephemeral token")
|
||||
_DASHBOARD_TOKEN = token
|
||||
return token
|
||||
|
||||
|
||||
def _parse_cookie_header(raw: str) -> dict[str, str]:
|
||||
"""Tiny RFC 6265 cookie-pair parser. Last-write-wins on duplicates."""
|
||||
out: dict[str, str] = {}
|
||||
if not raw:
|
||||
return out
|
||||
for chunk in raw.split(";"):
|
||||
chunk = chunk.strip()
|
||||
if not chunk or "=" not in chunk:
|
||||
continue
|
||||
k, v = chunk.split("=", 1)
|
||||
out[k.strip()] = v.strip()
|
||||
return out
|
||||
|
||||
|
||||
class AuthHandlers:
|
||||
"""Mixin: /api/auth/login, /api/auth/logout, plus _check_dashboard_cookie."""
|
||||
|
||||
# ── helpers ────────────────────────────────────────────────────────
|
||||
def _check_dashboard_cookie(self) -> bool:
|
||||
"""Return True if the request carries a valid `dashboard` cookie."""
|
||||
raw = self.headers.get("Cookie", "") or ""
|
||||
cookies = _parse_cookie_header(raw)
|
||||
provided = cookies.get(_COOKIE_NAME, "")
|
||||
if not provided:
|
||||
return False
|
||||
expected = _get_dashboard_token()
|
||||
# Constant-time compare — token guess attacks aren't realistic here
|
||||
# (cookie path is /echo/, HttpOnly), but cheap defense in depth.
|
||||
return secrets.compare_digest(provided, expected)
|
||||
|
||||
def _read_form_body(self) -> dict[str, str]:
|
||||
"""Parse `application/x-www-form-urlencoded` POST body."""
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length", "0") or "0")
|
||||
except (TypeError, ValueError):
|
||||
length = 0
|
||||
if length <= 0:
|
||||
return {}
|
||||
try:
|
||||
raw = self.rfile.read(length).decode("utf-8")
|
||||
except (UnicodeDecodeError, OSError):
|
||||
return {}
|
||||
parsed = parse_qs(raw, keep_blank_values=True)
|
||||
# Flatten — single-value form fields only
|
||||
return {k: v[0] for k, v in parsed.items() if v}
|
||||
|
||||
# ── POST /api/auth/login ───────────────────────────────────────────
|
||||
def handle_login(self):
|
||||
"""Validate token from form body; on success, set cookie + 302 to workspace.
|
||||
|
||||
On failure, return 401 JSON. The cookie is set with HttpOnly +
|
||||
SameSite=Strict; Path=/echo/ so it scopes to the dashboard reverse
|
||||
proxy mount.
|
||||
"""
|
||||
# Accept JSON body too (login.html might POST JSON in Lane B2)
|
||||
ctype = (self.headers.get("Content-Type", "") or "").lower()
|
||||
if "application/json" in ctype:
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length", "0") or "0")
|
||||
raw = self.rfile.read(length).decode("utf-8") if length > 0 else ""
|
||||
form = json.loads(raw) if raw else {}
|
||||
if not isinstance(form, dict):
|
||||
form = {}
|
||||
except (ValueError, json.JSONDecodeError, UnicodeDecodeError, OSError):
|
||||
form = {}
|
||||
else:
|
||||
form = self._read_form_body()
|
||||
|
||||
provided = (form.get("token") or "").strip()
|
||||
expected = _get_dashboard_token()
|
||||
if not provided or not secrets.compare_digest(provided, expected):
|
||||
body = json.dumps({"error": "Invalid token"}).encode("utf-8")
|
||||
self.send_response(401)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
try:
|
||||
self.wfile.write(body)
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
return
|
||||
|
||||
cookie = (
|
||||
f"{_COOKIE_NAME}={expected}; HttpOnly; SameSite=Strict; "
|
||||
f"Path={_COOKIE_PATH}; Max-Age={_COOKIE_MAX_AGE}"
|
||||
)
|
||||
self.send_response(302)
|
||||
self.send_header("Set-Cookie", cookie)
|
||||
self.send_header("Location", "/echo/workspace.html")
|
||||
self.send_header("Content-Length", "0")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
|
||||
# ── POST /api/auth/logout ──────────────────────────────────────────
|
||||
def handle_logout(self):
|
||||
"""Clear the dashboard cookie. Returns 200 JSON `{"ok": true}`."""
|
||||
cookie = (
|
||||
f"{_COOKIE_NAME}=; HttpOnly; SameSite=Strict; "
|
||||
f"Path={_COOKIE_PATH}; Max-Age=0"
|
||||
)
|
||||
body = json.dumps({"ok": True}).encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Set-Cookie", cookie)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
try:
|
||||
self.wfile.write(body)
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
95
dashboard/handlers/cron.py
Normal file
95
dashboard/handlers/cron.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""/api/cron — reads echo-core/cron/jobs.json (flat schema)."""
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
import constants
|
||||
|
||||
|
||||
def _parse_cron_time(expr):
|
||||
"""Extract a display-time string from a cron expression.
|
||||
|
||||
Echo-core cron strings are already Bucharest local time (Lane B
|
||||
scheduler sets tz=Europe/Bucharest), so NO UTC→local conversion.
|
||||
"""
|
||||
parts = expr.split()
|
||||
if len(parts) < 2:
|
||||
return expr[:15]
|
||||
minute, hour = parts[0], parts[1]
|
||||
if minute.isdigit() and (hour.isdigit() or '-' in hour):
|
||||
if '-' in hour:
|
||||
hour = hour.split('-')[0]
|
||||
try:
|
||||
return f"{int(hour):02d}:{int(minute):02d}"
|
||||
except ValueError:
|
||||
return expr[:15]
|
||||
return expr[:15]
|
||||
|
||||
|
||||
def _iso_to_epoch_ms(iso_str):
|
||||
"""Convert an ISO 8601 datetime string to epoch ms. Returns 0 on failure."""
|
||||
if not iso_str:
|
||||
return 0
|
||||
try:
|
||||
dt = datetime.fromisoformat(iso_str.replace('Z', '+00:00'))
|
||||
return int(dt.timestamp() * 1000)
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
|
||||
class CronHandlers:
|
||||
"""Mixin for /api/cron."""
|
||||
|
||||
def handle_cron_status(self):
|
||||
"""Get enabled cron jobs from echo-core/cron/jobs.json (flat schema).
|
||||
|
||||
Output shape preserved for the frontend: id, name, time, schedule,
|
||||
ranToday, lastStatus, lastRunAtMs, nextRunAtMs.
|
||||
"""
|
||||
try:
|
||||
jobs_file = constants.BASE_DIR / 'cron' / 'jobs.json'
|
||||
if not jobs_file.exists():
|
||||
self.send_json({'jobs': [], 'error': 'No jobs file found'})
|
||||
return
|
||||
|
||||
all_jobs = json.loads(jobs_file.read_text())
|
||||
if not isinstance(all_jobs, list):
|
||||
self.send_json({'jobs': [], 'error': 'Unexpected jobs.json shape'})
|
||||
return
|
||||
|
||||
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_start_ms = today_start.timestamp() * 1000
|
||||
|
||||
jobs = []
|
||||
for job in all_jobs:
|
||||
if not job.get('enabled', False):
|
||||
continue
|
||||
|
||||
name = job.get('name', '')
|
||||
expr = job.get('cron', '')
|
||||
last_run_iso = job.get('last_run')
|
||||
next_run_iso = job.get('next_run')
|
||||
last_status = job.get('last_status', 'unknown')
|
||||
|
||||
last_run_ms = _iso_to_epoch_ms(last_run_iso)
|
||||
next_run_ms = _iso_to_epoch_ms(next_run_iso) or None
|
||||
ran_today = last_run_ms >= today_start_ms
|
||||
|
||||
jobs.append({
|
||||
'id': name, # echo-core has no separate id; use name
|
||||
'name': name,
|
||||
'time': _parse_cron_time(expr),
|
||||
'schedule': expr,
|
||||
'ranToday': ran_today,
|
||||
'lastStatus': last_status if ran_today else None,
|
||||
'lastRunAtMs': last_run_ms,
|
||||
'nextRunAtMs': next_run_ms,
|
||||
})
|
||||
|
||||
jobs.sort(key=lambda j: j['time'])
|
||||
self.send_json({
|
||||
'jobs': jobs,
|
||||
'total': len(jobs),
|
||||
'ranToday': sum(1 for j in jobs if j['ranToday']),
|
||||
})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
378
dashboard/handlers/eco.py
Normal file
378
dashboard/handlers/eco.py
Normal file
@@ -0,0 +1,378 @@
|
||||
"""Echo Core (eco) service + session + doctor endpoints."""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import constants
|
||||
|
||||
|
||||
class EcoHandlers:
|
||||
"""Mixin for /api/eco/* endpoints."""
|
||||
|
||||
# ── /api/eco/status ─────────────────────────────────────────
|
||||
def handle_eco_status(self):
|
||||
"""Get status of echo-core services + active sessions."""
|
||||
try:
|
||||
services = []
|
||||
for svc in constants.ECO_SERVICES:
|
||||
info = {'name': svc, 'active': False, 'pid': None, 'uptime': None, 'memory': None}
|
||||
|
||||
result = subprocess.run(
|
||||
['systemctl', '--user', 'is-active', svc],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
info['active'] = result.stdout.strip() == 'active'
|
||||
|
||||
if info['active']:
|
||||
result = subprocess.run(
|
||||
['systemctl', '--user', 'show', '-p', 'MainPID', '--value', svc],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
pid = result.stdout.strip()
|
||||
if pid and pid != '0':
|
||||
info['pid'] = int(pid)
|
||||
|
||||
try:
|
||||
r = subprocess.run(
|
||||
['systemctl', '--user', 'show', '-p', 'ActiveEnterTimestamp', '--value', svc],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
ts = r.stdout.strip()
|
||||
if ts:
|
||||
start = datetime.strptime(ts, '%a %Y-%m-%d %H:%M:%S %Z')
|
||||
info['uptime'] = int((datetime.utcnow() - start).total_seconds())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
for line in Path(f'/proc/{pid}/status').read_text().splitlines():
|
||||
if line.startswith('VmRSS:'):
|
||||
info['memory'] = line.split(':')[1].strip()
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
services.append(info)
|
||||
|
||||
self.send_json({'services': services})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
# ── sessions ────────────────────────────────────────────────
|
||||
def _eco_channel_map(self):
|
||||
"""Build channel_id -> {name, platform, is_group} from config.json."""
|
||||
config_file = constants.ECHO_CORE_DIR / 'config.json'
|
||||
m = {}
|
||||
try:
|
||||
cfg = json.loads(config_file.read_text())
|
||||
for name, ch in cfg.get('channels', {}).items():
|
||||
m[str(ch['id'])] = {'name': name, 'platform': 'discord'}
|
||||
for name, ch in cfg.get('telegram_channels', {}).items():
|
||||
m[str(ch['id'])] = {'name': name, 'platform': 'telegram'}
|
||||
for name, ch in cfg.get('whatsapp_channels', {}).items():
|
||||
m[str(ch['id'])] = {'name': name, 'platform': 'whatsapp', 'is_group': True}
|
||||
for admin_id in cfg.get('bot', {}).get('admins', []):
|
||||
m.setdefault(str(admin_id), {'name': 'TG DM', 'platform': 'telegram'})
|
||||
wa_owner = cfg.get('whatsapp', {}).get('owner', '')
|
||||
if wa_owner:
|
||||
m.setdefault(f'wa-{wa_owner}', {'name': 'WA Owner', 'platform': 'whatsapp'})
|
||||
except Exception:
|
||||
pass
|
||||
return m
|
||||
|
||||
def _eco_enrich_sessions(self):
|
||||
"""Return enriched sessions list sorted by last_message_at desc."""
|
||||
raw = {}
|
||||
if constants.ECHO_SESSIONS_FILE.exists():
|
||||
try:
|
||||
raw = json.loads(constants.ECHO_SESSIONS_FILE.read_text())
|
||||
except Exception:
|
||||
pass
|
||||
cmap = self._eco_channel_map()
|
||||
sessions = []
|
||||
if isinstance(raw, dict):
|
||||
for ch_id, sdata in raw.items():
|
||||
if 'MagicMock' in ch_id:
|
||||
continue
|
||||
entry = dict(sdata) if isinstance(sdata, dict) else {}
|
||||
entry['channel_id'] = ch_id
|
||||
if ch_id in cmap:
|
||||
entry['platform'] = cmap[ch_id]['platform']
|
||||
entry['channel_name'] = cmap[ch_id]['name']
|
||||
entry['is_group'] = cmap[ch_id].get('is_group', False)
|
||||
elif ch_id.startswith('wa-') or '@g.us' in ch_id or '@s.whatsapp.net' in ch_id:
|
||||
entry['platform'] = 'whatsapp'
|
||||
entry['is_group'] = '@g.us' in ch_id
|
||||
entry['channel_name'] = ('WA Grup' if entry['is_group'] else 'WA DM')
|
||||
elif ch_id.isdigit() and len(ch_id) >= 17:
|
||||
entry['platform'] = 'discord'
|
||||
entry['channel_name'] = 'Discord #' + ch_id[-6:]
|
||||
elif ch_id.isdigit():
|
||||
entry['platform'] = 'telegram'
|
||||
entry['channel_name'] = 'TG ' + ch_id
|
||||
else:
|
||||
entry['platform'] = 'unknown'
|
||||
entry['channel_name'] = ch_id[:20]
|
||||
sessions.append(entry)
|
||||
sessions.sort(key=lambda s: s.get('last_message_at', ''), reverse=True)
|
||||
return sessions
|
||||
|
||||
def handle_eco_sessions(self):
|
||||
"""Return enriched sessions list."""
|
||||
try:
|
||||
self.send_json({'sessions': self._eco_enrich_sessions()})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_eco_session_content(self):
|
||||
"""Return conversation messages from a Claude session transcript."""
|
||||
try:
|
||||
params = parse_qs(urlparse(self.path).query)
|
||||
session_id = params.get('id', [''])[0]
|
||||
if not session_id or '/' in session_id or '..' in session_id:
|
||||
self.send_json({'error': 'Invalid session id'}, 400)
|
||||
return
|
||||
|
||||
transcript = Path.home() / '.claude' / 'projects' / '-home-moltbot-echo-core' / f'{session_id}.jsonl'
|
||||
if not transcript.exists():
|
||||
self.send_json({'messages': [], 'error': 'Transcript not found'})
|
||||
return
|
||||
|
||||
messages = []
|
||||
for line in transcript.read_text().splitlines():
|
||||
try:
|
||||
d = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
t = d.get('type', '')
|
||||
if t == 'user':
|
||||
msg = d.get('message', {})
|
||||
content = msg.get('content', '')
|
||||
if isinstance(content, str):
|
||||
text = content.replace('[EXTERNAL CONTENT]\n', '').replace('\n[END EXTERNAL CONTENT]', '').strip()
|
||||
if text:
|
||||
messages.append({'role': 'user', 'text': text[:20000]})
|
||||
elif t == 'assistant':
|
||||
msg = d.get('message', {})
|
||||
content = msg.get('content', '')
|
||||
if isinstance(content, list):
|
||||
parts = [block['text'] for block in content if block.get('type') == 'text']
|
||||
text = '\n'.join(parts).strip()
|
||||
if text:
|
||||
messages.append({'role': 'assistant', 'text': text[:20000]})
|
||||
elif isinstance(content, str) and content.strip():
|
||||
messages.append({'role': 'assistant', 'text': content[:20000]})
|
||||
|
||||
self.send_json({'messages': messages})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_eco_sessions_clear(self):
|
||||
"""Clear active sessions (all or specific channel)."""
|
||||
try:
|
||||
data = self._read_post_json()
|
||||
channel = data.get('channel', None)
|
||||
|
||||
if not constants.ECHO_SESSIONS_FILE.exists():
|
||||
self.send_json({'success': True, 'message': 'No sessions file'})
|
||||
return
|
||||
|
||||
if channel:
|
||||
sessions = json.loads(constants.ECHO_SESSIONS_FILE.read_text())
|
||||
if isinstance(sessions, list):
|
||||
sessions = [s for s in sessions if s.get('channel') != channel]
|
||||
elif isinstance(sessions, dict):
|
||||
sessions.pop(channel, None)
|
||||
constants.ECHO_SESSIONS_FILE.write_text(json.dumps(sessions, indent=2))
|
||||
self.send_json({'success': True, 'message': f'Cleared session: {channel}'})
|
||||
else:
|
||||
if isinstance(json.loads(constants.ECHO_SESSIONS_FILE.read_text()), list):
|
||||
constants.ECHO_SESSIONS_FILE.write_text('[]')
|
||||
else:
|
||||
constants.ECHO_SESSIONS_FILE.write_text('{}')
|
||||
self.send_json({'success': True, 'message': 'All sessions cleared'})
|
||||
except Exception as e:
|
||||
self.send_json({'success': False, 'error': str(e)}, 500)
|
||||
|
||||
# ── logs + doctor ───────────────────────────────────────────
|
||||
def handle_eco_logs(self):
|
||||
"""Return last N lines from echo-core.log."""
|
||||
try:
|
||||
params = parse_qs(urlparse(self.path).query)
|
||||
lines = min(int(params.get('lines', ['100'])[0]), 500)
|
||||
|
||||
if not constants.ECHO_LOG_FILE.exists():
|
||||
self.send_json({'lines': ['(log file not found)']})
|
||||
return
|
||||
|
||||
result = subprocess.run(
|
||||
['tail', '-n', str(lines), str(constants.ECHO_LOG_FILE)],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
self.send_json({'lines': result.stdout.splitlines()})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_eco_doctor(self):
|
||||
"""Run health checks on the echo-core ecosystem."""
|
||||
checks = []
|
||||
|
||||
# 1. Services
|
||||
for svc in constants.ECO_SERVICES:
|
||||
try:
|
||||
r = subprocess.run(
|
||||
['systemctl', '--user', 'is-active', svc],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
active = r.stdout.strip() == 'active'
|
||||
checks.append({
|
||||
'name': f'Service: {svc}',
|
||||
'pass': active,
|
||||
'detail': 'active' if active else r.stdout.strip(),
|
||||
})
|
||||
except Exception as e:
|
||||
checks.append({'name': f'Service: {svc}', 'pass': False, 'detail': str(e)})
|
||||
|
||||
# 2. Disk space
|
||||
try:
|
||||
st = shutil.disk_usage('/')
|
||||
pct_free = (st.free / st.total) * 100
|
||||
checks.append({
|
||||
'name': 'Disk space',
|
||||
'pass': pct_free > 5,
|
||||
'detail': f'{pct_free:.1f}% free ({st.free // (1024**3)} GB)',
|
||||
})
|
||||
except Exception as e:
|
||||
checks.append({'name': 'Disk space', 'pass': False, 'detail': str(e)})
|
||||
|
||||
# 3. Log file
|
||||
try:
|
||||
if constants.ECHO_LOG_FILE.exists():
|
||||
size_mb = constants.ECHO_LOG_FILE.stat().st_size / (1024 * 1024)
|
||||
checks.append({
|
||||
'name': 'Log file',
|
||||
'pass': size_mb < 100,
|
||||
'detail': f'{size_mb:.1f} MB',
|
||||
})
|
||||
else:
|
||||
checks.append({'name': 'Log file', 'pass': False, 'detail': 'Not found'})
|
||||
except Exception as e:
|
||||
checks.append({'name': 'Log file', 'pass': False, 'detail': str(e)})
|
||||
|
||||
# 4. Sessions file
|
||||
try:
|
||||
if constants.ECHO_SESSIONS_FILE.exists():
|
||||
data = json.loads(constants.ECHO_SESSIONS_FILE.read_text())
|
||||
count = len(data) if isinstance(data, list) else len(data.keys()) if isinstance(data, dict) else 0
|
||||
checks.append({'name': 'Sessions file', 'pass': True, 'detail': f'{count} active'})
|
||||
else:
|
||||
checks.append({'name': 'Sessions file', 'pass': False, 'detail': 'Not found'})
|
||||
except Exception as e:
|
||||
checks.append({'name': 'Sessions file', 'pass': False, 'detail': str(e)})
|
||||
|
||||
# 5. Config
|
||||
config_file = constants.ECHO_CORE_DIR / 'config.json'
|
||||
try:
|
||||
if config_file.exists():
|
||||
json.loads(config_file.read_text())
|
||||
checks.append({'name': 'Config', 'pass': True, 'detail': 'Valid JSON'})
|
||||
else:
|
||||
checks.append({'name': 'Config', 'pass': False, 'detail': 'Not found'})
|
||||
except Exception as e:
|
||||
checks.append({'name': 'Config', 'pass': False, 'detail': str(e)})
|
||||
|
||||
# 6. WhatsApp bridge log
|
||||
wa_log = constants.ECHO_CORE_DIR / 'logs' / 'whatsapp-bridge.log'
|
||||
try:
|
||||
if wa_log.exists():
|
||||
r = subprocess.run(['tail', '-1', str(wa_log)], capture_output=True, text=True, timeout=5)
|
||||
last = r.stdout.strip()
|
||||
has_error = 'error' in last.lower() or 'fatal' in last.lower()
|
||||
checks.append({
|
||||
'name': 'WhatsApp bridge log',
|
||||
'pass': not has_error,
|
||||
'detail': last[:80] if last else 'Empty',
|
||||
})
|
||||
else:
|
||||
checks.append({'name': 'WhatsApp bridge log', 'pass': False, 'detail': 'Not found'})
|
||||
except Exception as e:
|
||||
checks.append({'name': 'WhatsApp bridge log', 'pass': False, 'detail': str(e)})
|
||||
|
||||
# 7. Claude CLI
|
||||
try:
|
||||
r = subprocess.run(['which', 'claude'], capture_output=True, text=True, timeout=5)
|
||||
found = r.returncode == 0
|
||||
checks.append({
|
||||
'name': 'Claude CLI',
|
||||
'pass': found,
|
||||
'detail': r.stdout.strip() if found else 'Not in PATH',
|
||||
})
|
||||
except Exception as e:
|
||||
checks.append({'name': 'Claude CLI', 'pass': False, 'detail': str(e)})
|
||||
|
||||
self.send_json({'checks': checks})
|
||||
|
||||
# ── service control ─────────────────────────────────────────
|
||||
def handle_eco_restart(self):
|
||||
"""Restart an echo-core service (not the taskboard itself)."""
|
||||
try:
|
||||
data = self._read_post_json()
|
||||
svc = data.get('service', '')
|
||||
|
||||
if svc not in constants.ECO_SERVICES:
|
||||
self.send_json({'success': False, 'error': f'Unknown service: {svc}'}, 400)
|
||||
return
|
||||
if svc == 'echo-taskboard':
|
||||
self.send_json({'success': False, 'error': 'Cannot restart taskboard from itself'}, 400)
|
||||
return
|
||||
|
||||
result = subprocess.run(
|
||||
['systemctl', '--user', 'restart', svc],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
self.send_json({'success': True, 'message': f'{svc} restarted'})
|
||||
else:
|
||||
self.send_json({'success': False, 'error': result.stderr.strip()}, 500)
|
||||
except Exception as e:
|
||||
self.send_json({'success': False, 'error': str(e)}, 500)
|
||||
|
||||
def handle_eco_stop(self):
|
||||
"""Stop an echo-core service (not the taskboard itself)."""
|
||||
try:
|
||||
data = self._read_post_json()
|
||||
svc = data.get('service', '')
|
||||
|
||||
if svc not in constants.ECO_SERVICES:
|
||||
self.send_json({'success': False, 'error': f'Unknown service: {svc}'}, 400)
|
||||
return
|
||||
if svc == 'echo-taskboard':
|
||||
self.send_json({'success': False, 'error': 'Cannot stop taskboard from itself'}, 400)
|
||||
return
|
||||
|
||||
result = subprocess.run(
|
||||
['systemctl', '--user', 'stop', svc],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
self.send_json({'success': True, 'message': f'{svc} stopped'})
|
||||
else:
|
||||
self.send_json({'success': False, 'error': result.stderr.strip()}, 500)
|
||||
except Exception as e:
|
||||
self.send_json({'success': False, 'error': str(e)}, 500)
|
||||
|
||||
def handle_eco_restart_taskboard(self):
|
||||
"""Restart the taskboard itself. Sends response then exits; systemd restarts."""
|
||||
import threading
|
||||
self.send_json({'success': True, 'message': 'Restarting taskboard in 1s...'})
|
||||
|
||||
def _exit():
|
||||
import time
|
||||
time.sleep(1)
|
||||
os._exit(0)
|
||||
|
||||
threading.Thread(target=_exit, daemon=True).start()
|
||||
120
dashboard/handlers/files.py
Normal file
120
dashboard/handlers/files.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""File-browser + note-index endpoints (sandbox-enforced)."""
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import constants
|
||||
|
||||
|
||||
class FilesHandlers:
|
||||
"""Mixin for /api/files, /api/refresh-index."""
|
||||
|
||||
def _resolve_sandboxed(self, path):
|
||||
"""Resolve `path` against ALLOWED_WORKSPACES. Returns (target, workspace) or (None, None)."""
|
||||
allowed_dirs = constants.ALLOWED_WORKSPACES
|
||||
for base in allowed_dirs:
|
||||
try:
|
||||
candidate = (base / path).resolve()
|
||||
if any(str(candidate).startswith(str(d)) for d in allowed_dirs):
|
||||
return candidate, base
|
||||
except Exception:
|
||||
continue
|
||||
return None, None
|
||||
|
||||
def handle_files_get(self):
|
||||
"""List files or get file content."""
|
||||
params = parse_qs(urlparse(self.path).query)
|
||||
path = params.get('path', [''])[0]
|
||||
action = params.get('action', ['list'])[0]
|
||||
|
||||
target, workspace = self._resolve_sandboxed(path)
|
||||
if target is None:
|
||||
self.send_json({'error': 'Access denied'}, 403)
|
||||
return
|
||||
|
||||
if action != 'list':
|
||||
self.send_json({'error': 'Unknown action'}, 400)
|
||||
return
|
||||
|
||||
if not target.exists():
|
||||
self.send_json({'error': 'Path not found'}, 404)
|
||||
return
|
||||
|
||||
if target.is_file():
|
||||
try:
|
||||
content = target.read_text(encoding='utf-8', errors='replace')
|
||||
self.send_json({
|
||||
'type': 'file',
|
||||
'path': path,
|
||||
'name': target.name,
|
||||
'content': content[:100000],
|
||||
'size': target.stat().st_size,
|
||||
'truncated': target.stat().st_size > 100000,
|
||||
})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
else:
|
||||
items = []
|
||||
try:
|
||||
for item in sorted(target.iterdir()):
|
||||
stat = item.stat()
|
||||
item_path = f"{path}/{item.name}" if path else item.name
|
||||
items.append({
|
||||
'name': item.name,
|
||||
'type': 'dir' if item.is_dir() else 'file',
|
||||
'size': stat.st_size if item.is_file() else None,
|
||||
'mtime': stat.st_mtime,
|
||||
'path': item_path,
|
||||
})
|
||||
self.send_json({'type': 'dir', 'path': path, 'items': items})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_files_post(self):
|
||||
"""Save file content."""
|
||||
try:
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||
data = json.loads(post_data)
|
||||
|
||||
path = data.get('path', '')
|
||||
content = data.get('content', '')
|
||||
|
||||
target, workspace = self._resolve_sandboxed(path)
|
||||
if target is None:
|
||||
self.send_json({'error': 'Access denied'}, 403)
|
||||
return
|
||||
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(content, encoding='utf-8')
|
||||
|
||||
self.send_json({'status': 'saved', 'path': path, 'size': len(content)})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_refresh_index(self):
|
||||
"""Regenerate memory/kb/index.json by running tools/update_notes_index.py."""
|
||||
try:
|
||||
script = constants.TOOLS_DIR / 'update_notes_index.py'
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script)],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
output = result.stdout
|
||||
total_match = re.search(r'with (\d+) notes', output)
|
||||
total = int(total_match.group(1)) if total_match else 0
|
||||
self.send_json({
|
||||
'success': True,
|
||||
'message': f'Index regenerat cu {total} notițe',
|
||||
'total': total,
|
||||
'output': output,
|
||||
})
|
||||
else:
|
||||
self.send_json({'success': False, 'error': result.stderr or 'Unknown error'}, 500)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.send_json({'success': False, 'error': 'Timeout'}, 500)
|
||||
except Exception as e:
|
||||
self.send_json({'success': False, 'error': str(e)}, 500)
|
||||
279
dashboard/handlers/git.py
Normal file
279
dashboard/handlers/git.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""Git status / diff / commit handlers for dashboard + workspace projects."""
|
||||
import json
|
||||
import subprocess
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import constants
|
||||
|
||||
|
||||
class GitHandlers:
|
||||
"""Mixin providing git status/diff/commit endpoints."""
|
||||
|
||||
# ── shared helper ────────────────────────────────────────────
|
||||
def _run_git(self, workspace, args, timeout=5):
|
||||
"""Run a git command in workspace. Returns CompletedProcess."""
|
||||
return subprocess.run(
|
||||
['git', *args],
|
||||
cwd=str(workspace),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
# ── /api/git (dashboard repo) ───────────────────────────────
|
||||
def handle_git_status(self):
|
||||
"""Get git status for the echo-core repo."""
|
||||
try:
|
||||
workspace = constants.GIT_WORKSPACE
|
||||
|
||||
branch = self._run_git(workspace, ['branch', '--show-current']).stdout.strip()
|
||||
last_commit = self._run_git(workspace, ['log', '-1', '--format=%h|%s|%cr']).stdout.strip()
|
||||
commit_parts = last_commit.split('|') if last_commit else ['', '', '']
|
||||
|
||||
status_output = self._run_git(workspace, ['status', '--short']).stdout.strip()
|
||||
uncommitted = [f for f in status_output.split('\n') if f.strip()] if status_output else []
|
||||
|
||||
diff_stat = ''
|
||||
if uncommitted:
|
||||
diff_stat = self._run_git(workspace, ['diff', '--stat', '--cached']).stdout.strip()
|
||||
if not diff_stat:
|
||||
diff_stat = self._run_git(workspace, ['diff', '--stat']).stdout.strip()
|
||||
|
||||
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,
|
||||
'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 '',
|
||||
},
|
||||
'uncommitted': uncommitted,
|
||||
'uncommittedParsed': uncommitted_parsed,
|
||||
'uncommittedCount': len(uncommitted),
|
||||
'diffStat': diff_stat,
|
||||
'clean': len(uncommitted) == 0,
|
||||
})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
# ── /api/diff ────────────────────────────────────────────────
|
||||
def handle_git_diff(self):
|
||||
"""Get git diff for a specific file."""
|
||||
params = parse_qs(urlparse(self.path).query)
|
||||
filepath = params.get('path', [''])[0]
|
||||
|
||||
if not filepath:
|
||||
self.send_json({'error': 'path required'}, 400)
|
||||
return
|
||||
|
||||
try:
|
||||
workspace = constants.GIT_WORKSPACE
|
||||
|
||||
target = (workspace / filepath).resolve()
|
||||
if not str(target).startswith(str(workspace)):
|
||||
self.send_json({'error': 'Access denied'}, 403)
|
||||
return
|
||||
|
||||
diff = self._run_git(workspace, ['diff', '--cached', '--', filepath], timeout=10).stdout
|
||||
if not diff:
|
||||
diff = self._run_git(workspace, ['diff', '--', filepath], timeout=10).stdout
|
||||
|
||||
if not diff:
|
||||
status = self._run_git(workspace, ['status', '--short', '--', filepath]).stdout.strip()
|
||||
if status.startswith('??') and target.exists():
|
||||
content = target.read_text(encoding='utf-8', errors='replace')[:50000]
|
||||
diff = f"+++ b/{filepath}\n" + '\n'.join(f'+{line}' for line in content.split('\n'))
|
||||
|
||||
self.send_json({
|
||||
'path': filepath,
|
||||
'diff': diff or 'No changes',
|
||||
'hasDiff': bool(diff),
|
||||
})
|
||||
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 = constants.ECHO_CORE_DIR
|
||||
|
||||
self._run_git(workspace, ['add', '-A'], timeout=10)
|
||||
|
||||
status = self._run_git(workspace, ['status', '--porcelain']).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_result = self._run_git(workspace, ['commit', '-m', 'chore: auto-commit from dashboard'], timeout=30)
|
||||
push_result = self._run_git(workspace, ['push'], 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)
|
||||
|
||||
# ── /api/workspace/git/* (per-project) ───────────────────────
|
||||
def handle_workspace_git_diff(self):
|
||||
"""Get git diff for a workspace project."""
|
||||
try:
|
||||
params = parse_qs(urlparse(self.path).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 = self._run_git(project_dir, ['status', '--short'], timeout=10).stdout.strip()
|
||||
diff = self._run_git(project_dir, ['diff'], timeout=10).stdout
|
||||
diff_cached = self._run_git(project_dir, ['diff', '--cached'], 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
|
||||
|
||||
porcelain = self._run_git(project_dir, ['status', '--porcelain'], 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()])
|
||||
|
||||
if not message:
|
||||
now = datetime.now().strftime('%Y-%m-%d %H:%M')
|
||||
message = f'Update: {now} ({files_changed} files)'
|
||||
|
||||
self._run_git(project_dir, ['add', '-A'], timeout=10)
|
||||
|
||||
result = self._run_git(project_dir, ['commit', '-m', message], 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)."""
|
||||
if not constants.GITEA_TOKEN:
|
||||
return False, 'GITEA_TOKEN not set'
|
||||
|
||||
api_url = f'{constants.GITEA_URL}/api/v1/orgs/{constants.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 {constants.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}'
|
||||
|
||||
remote_url = f'{constants.GITEA_URL}/{constants.GITEA_ORG}/{project_name}.git'
|
||||
auth_url = remote_url.replace('https://', f'https://gitea:{constants.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 {constants.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 = ''
|
||||
remote_check = self._run_git(project_dir, ['remote', 'get-url', 'origin'], 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'
|
||||
|
||||
result = self._run_git(project_dir, ['push', '-u', 'origin', 'HEAD'], 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)
|
||||
391
dashboard/handlers/habits.py
Normal file
391
dashboard/handlers/habits.py
Normal file
@@ -0,0 +1,391 @@
|
||||
"""Habit tracking endpoints (CRUD + check / skip / uncheck)."""
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import constants
|
||||
import habits_helpers
|
||||
|
||||
|
||||
def _enrich(habit):
|
||||
"""Return habit with calculated stats added."""
|
||||
enriched = habit.copy()
|
||||
enriched['current_streak'] = habits_helpers.calculate_streak(habit)
|
||||
enriched['best_streak'] = habit.get('streak', {}).get('best', 0)
|
||||
enriched['completion_rate_30d'] = habits_helpers.get_completion_rate(habit, days=30)
|
||||
enriched['weekly_summary'] = habits_helpers.get_weekly_summary(habit)
|
||||
enriched['should_check_today'] = habits_helpers.should_check_today(habit)
|
||||
return enriched
|
||||
|
||||
|
||||
class HabitsHandlers:
|
||||
"""Mixin providing /api/habits endpoints."""
|
||||
|
||||
def handle_habits_get(self):
|
||||
"""Return all habits with enriched stats."""
|
||||
try:
|
||||
if not constants.HABITS_FILE.exists():
|
||||
self.send_json([])
|
||||
return
|
||||
|
||||
with open(constants.HABITS_FILE, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
enriched = [_enrich(h) for h in data.get('habits', [])]
|
||||
enriched.sort(key=lambda h: h.get('priority', 999))
|
||||
self.send_json(enriched)
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_habits_post(self):
|
||||
"""Create a new habit."""
|
||||
try:
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||
data = json.loads(post_data)
|
||||
|
||||
name = data.get('name', '').strip()
|
||||
if not name:
|
||||
self.send_json({'error': 'name is required'}, 400)
|
||||
return
|
||||
if len(name) > 100:
|
||||
self.send_json({'error': 'name must be max 100 characters'}, 400)
|
||||
return
|
||||
|
||||
color = data.get('color', '#3b82f6')
|
||||
if color and not re.match(r'^#[0-9A-Fa-f]{6}$', color):
|
||||
self.send_json({'error': 'color must be valid hex format (#RRGGBB)'}, 400)
|
||||
return
|
||||
|
||||
frequency_type = data.get('frequency', {}).get('type', 'daily')
|
||||
valid_types = ['daily', 'specific_days', 'x_per_week', 'weekly', 'monthly', 'custom']
|
||||
if frequency_type not in valid_types:
|
||||
self.send_json({'error': f'frequency.type must be one of: {", ".join(valid_types)}'}, 400)
|
||||
return
|
||||
|
||||
habit_id = str(uuid.uuid4())
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
new_habit = {
|
||||
'id': habit_id,
|
||||
'name': name,
|
||||
'category': data.get('category', 'other'),
|
||||
'color': color,
|
||||
'icon': data.get('icon', 'check-circle'),
|
||||
'priority': data.get('priority', 5),
|
||||
'notes': data.get('notes', ''),
|
||||
'reminderTime': data.get('reminderTime', ''),
|
||||
'frequency': data.get('frequency', {'type': 'daily'}),
|
||||
'streak': {'current': 0, 'best': 0, 'lastCheckIn': None},
|
||||
'lives': 3,
|
||||
'completions': [],
|
||||
'createdAt': now,
|
||||
'updatedAt': now,
|
||||
}
|
||||
|
||||
if constants.HABITS_FILE.exists():
|
||||
with open(constants.HABITS_FILE, 'r', encoding='utf-8') as f:
|
||||
habits_data = json.load(f)
|
||||
else:
|
||||
habits_data = {'lastUpdated': '', 'habits': []}
|
||||
|
||||
habits_data['habits'].append(new_habit)
|
||||
habits_data['lastUpdated'] = now
|
||||
|
||||
with open(constants.HABITS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(habits_data, f, indent=2)
|
||||
|
||||
self.send_json(new_habit, 201)
|
||||
except json.JSONDecodeError:
|
||||
self.send_json({'error': 'Invalid JSON'}, 400)
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_habits_put(self):
|
||||
"""Update an existing habit."""
|
||||
try:
|
||||
path_parts = self.path.split('/')
|
||||
if len(path_parts) < 4:
|
||||
self.send_json({'error': 'Invalid path'}, 400)
|
||||
return
|
||||
habit_id = path_parts[3]
|
||||
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||
data = json.loads(post_data)
|
||||
|
||||
if not constants.HABITS_FILE.exists():
|
||||
self.send_json({'error': 'Habit not found'}, 404)
|
||||
return
|
||||
|
||||
with open(constants.HABITS_FILE, 'r', encoding='utf-8') as f:
|
||||
habits_data = json.load(f)
|
||||
|
||||
habits = habits_data.get('habits', [])
|
||||
habit_index = next((i for i, h in enumerate(habits) if h['id'] == habit_id), None)
|
||||
if habit_index is None:
|
||||
self.send_json({'error': 'Habit not found'}, 404)
|
||||
return
|
||||
|
||||
if 'name' in data:
|
||||
name = data['name'].strip()
|
||||
if not name:
|
||||
self.send_json({'error': 'name cannot be empty'}, 400)
|
||||
return
|
||||
if len(name) > 100:
|
||||
self.send_json({'error': 'name must be max 100 characters'}, 400)
|
||||
return
|
||||
if 'color' in data:
|
||||
color = data['color']
|
||||
if color and not re.match(r'^#[0-9A-Fa-f]{6}$', color):
|
||||
self.send_json({'error': 'color must be valid hex format (#RRGGBB)'}, 400)
|
||||
return
|
||||
if 'frequency' in data:
|
||||
frequency_type = data.get('frequency', {}).get('type', 'daily')
|
||||
valid_types = ['daily', 'specific_days', 'x_per_week', 'weekly', 'monthly', 'custom']
|
||||
if frequency_type not in valid_types:
|
||||
self.send_json({'error': f'frequency.type must be one of: {", ".join(valid_types)}'}, 400)
|
||||
return
|
||||
|
||||
allowed_fields = ['name', 'category', 'color', 'icon', 'priority', 'notes', 'frequency', 'reminderTime']
|
||||
habit = habits[habit_index]
|
||||
for field in allowed_fields:
|
||||
if field in data:
|
||||
habit[field] = data[field]
|
||||
|
||||
habit['updatedAt'] = datetime.now().isoformat()
|
||||
habits_data['lastUpdated'] = habit['updatedAt']
|
||||
with open(constants.HABITS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(habits_data, f, indent=2)
|
||||
|
||||
self.send_json(habit)
|
||||
except json.JSONDecodeError:
|
||||
self.send_json({'error': 'Invalid JSON'}, 400)
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_habits_delete(self):
|
||||
"""Delete a habit."""
|
||||
try:
|
||||
path_parts = self.path.split('/')
|
||||
if len(path_parts) < 4:
|
||||
self.send_json({'error': 'Invalid path'}, 400)
|
||||
return
|
||||
habit_id = path_parts[3]
|
||||
|
||||
if not constants.HABITS_FILE.exists():
|
||||
self.send_json({'error': 'Habit not found'}, 404)
|
||||
return
|
||||
|
||||
with open(constants.HABITS_FILE, 'r', encoding='utf-8') as f:
|
||||
habits_data = json.load(f)
|
||||
|
||||
habits = habits_data.get('habits', [])
|
||||
habit_found = False
|
||||
for i, habit in enumerate(habits):
|
||||
if habit['id'] == habit_id:
|
||||
habits.pop(i)
|
||||
habit_found = True
|
||||
break
|
||||
|
||||
if not habit_found:
|
||||
self.send_json({'error': 'Habit not found'}, 404)
|
||||
return
|
||||
|
||||
habits_data['lastUpdated'] = datetime.now().isoformat()
|
||||
with open(constants.HABITS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(habits_data, f, indent=2)
|
||||
|
||||
self.send_response(204)
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_habits_check(self):
|
||||
"""Check in on a habit for today."""
|
||||
try:
|
||||
path_parts = self.path.split('/')
|
||||
if len(path_parts) < 5:
|
||||
self.send_json({'error': 'Invalid path'}, 400)
|
||||
return
|
||||
habit_id = path_parts[3]
|
||||
|
||||
body_data = {}
|
||||
content_length = self.headers.get('Content-Length')
|
||||
if content_length:
|
||||
post_data = self.rfile.read(int(content_length)).decode('utf-8')
|
||||
if post_data.strip():
|
||||
try:
|
||||
body_data = json.loads(post_data)
|
||||
except json.JSONDecodeError:
|
||||
self.send_json({'error': 'Invalid JSON'}, 400)
|
||||
return
|
||||
|
||||
if not constants.HABITS_FILE.exists():
|
||||
self.send_json({'error': 'Habit not found'}, 404)
|
||||
return
|
||||
|
||||
with open(constants.HABITS_FILE, 'r', encoding='utf-8') as f:
|
||||
habits_data = json.load(f)
|
||||
|
||||
habit = next((h for h in habits_data.get('habits', []) if h['id'] == habit_id), None)
|
||||
if not habit:
|
||||
self.send_json({'error': 'Habit not found'}, 404)
|
||||
return
|
||||
|
||||
if not habits_helpers.should_check_today(habit):
|
||||
self.send_json({'error': 'Habit is not relevant for today based on its frequency'}, 400)
|
||||
return
|
||||
|
||||
today = datetime.now().date().isoformat()
|
||||
for completion in habit.get('completions', []):
|
||||
if completion.get('date') == today:
|
||||
self.send_json({'error': 'Habit already checked in today'}, 409)
|
||||
return
|
||||
|
||||
completion_entry = {'date': today, 'type': 'check'}
|
||||
if 'note' in body_data:
|
||||
completion_entry['note'] = body_data['note']
|
||||
if 'rating' in body_data:
|
||||
rating = body_data['rating']
|
||||
if not isinstance(rating, int) or rating < 1 or rating > 5:
|
||||
self.send_json({'error': 'rating must be an integer between 1 and 5'}, 400)
|
||||
return
|
||||
completion_entry['rating'] = rating
|
||||
if 'mood' in body_data:
|
||||
mood = body_data['mood']
|
||||
if mood not in ['happy', 'neutral', 'sad']:
|
||||
self.send_json({'error': 'mood must be one of: happy, neutral, sad'}, 400)
|
||||
return
|
||||
completion_entry['mood'] = mood
|
||||
|
||||
habit['completions'].append(completion_entry)
|
||||
|
||||
current_streak = habits_helpers.calculate_streak(habit)
|
||||
habit['streak']['current'] = current_streak
|
||||
if current_streak > habit['streak']['best']:
|
||||
habit['streak']['best'] = current_streak
|
||||
habit['streak']['lastCheckIn'] = today
|
||||
|
||||
new_lives, was_awarded = habits_helpers.check_and_award_weekly_lives(habit)
|
||||
lives_awarded_this_checkin = False
|
||||
if was_awarded:
|
||||
habit['lives'] = new_lives
|
||||
habit['lastLivesAward'] = today
|
||||
lives_awarded_this_checkin = True
|
||||
|
||||
habit['updatedAt'] = datetime.now().isoformat()
|
||||
habits_data['lastUpdated'] = habit['updatedAt']
|
||||
|
||||
with open(constants.HABITS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(habits_data, f, indent=2)
|
||||
|
||||
enriched = _enrich(habit)
|
||||
enriched['livesAwarded'] = lives_awarded_this_checkin
|
||||
self.send_json(enriched, 200)
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_habits_uncheck(self):
|
||||
"""Remove a habit completion for a specific date."""
|
||||
try:
|
||||
path_parts = self.path.split('?')[0].split('/')
|
||||
if len(path_parts) < 5:
|
||||
self.send_json({'error': 'Invalid path'}, 400)
|
||||
return
|
||||
habit_id = path_parts[3]
|
||||
|
||||
query_params = parse_qs(urlparse(self.path).query)
|
||||
if 'date' not in query_params:
|
||||
self.send_json({'error': 'date parameter is required (format: YYYY-MM-DD)'}, 400)
|
||||
return
|
||||
|
||||
target_date = query_params['date'][0]
|
||||
try:
|
||||
datetime.fromisoformat(target_date)
|
||||
except ValueError:
|
||||
self.send_json({'error': 'Invalid date format. Use YYYY-MM-DD'}, 400)
|
||||
return
|
||||
|
||||
if not constants.HABITS_FILE.exists():
|
||||
self.send_json({'error': 'Habit not found'}, 404)
|
||||
return
|
||||
|
||||
with open(constants.HABITS_FILE, 'r', encoding='utf-8') as f:
|
||||
habits_data = json.load(f)
|
||||
|
||||
habit = next((h for h in habits_data.get('habits', []) if h['id'] == habit_id), None)
|
||||
if not habit:
|
||||
self.send_json({'error': 'Habit not found'}, 404)
|
||||
return
|
||||
|
||||
completions = habit.get('completions', [])
|
||||
completion_found = False
|
||||
for i, completion in enumerate(completions):
|
||||
if completion.get('date') == target_date:
|
||||
completions.pop(i)
|
||||
completion_found = True
|
||||
break
|
||||
|
||||
if not completion_found:
|
||||
self.send_json({'error': 'No completion found for the specified date'}, 404)
|
||||
return
|
||||
|
||||
current_streak = habits_helpers.calculate_streak(habit)
|
||||
habit['streak']['current'] = current_streak
|
||||
if current_streak > habit['streak']['best']:
|
||||
habit['streak']['best'] = current_streak
|
||||
|
||||
habit['updatedAt'] = datetime.now().isoformat()
|
||||
habits_data['lastUpdated'] = habit['updatedAt']
|
||||
|
||||
with open(constants.HABITS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(habits_data, f, indent=2)
|
||||
|
||||
self.send_json(_enrich(habit), 200)
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_habits_skip(self):
|
||||
"""Skip a day using a life to preserve streak."""
|
||||
try:
|
||||
path_parts = self.path.split('/')
|
||||
if len(path_parts) < 5:
|
||||
self.send_json({'error': 'Invalid path'}, 400)
|
||||
return
|
||||
habit_id = path_parts[3]
|
||||
|
||||
if not constants.HABITS_FILE.exists():
|
||||
self.send_json({'error': 'Habit not found'}, 404)
|
||||
return
|
||||
|
||||
with open(constants.HABITS_FILE, 'r', encoding='utf-8') as f:
|
||||
habits_data = json.load(f)
|
||||
|
||||
habit = next((h for h in habits_data.get('habits', []) if h['id'] == habit_id), None)
|
||||
if not habit:
|
||||
self.send_json({'error': 'Habit not found'}, 404)
|
||||
return
|
||||
|
||||
current_lives = habit.get('lives', 3)
|
||||
if current_lives <= 0:
|
||||
self.send_json({'error': 'No lives remaining'}, 400)
|
||||
return
|
||||
|
||||
habit['lives'] = current_lives - 1
|
||||
|
||||
today = datetime.now().date().isoformat()
|
||||
habit['completions'].append({'date': today, 'type': 'skip'})
|
||||
|
||||
habit['updatedAt'] = datetime.now().isoformat()
|
||||
habits_data['lastUpdated'] = habit['updatedAt']
|
||||
|
||||
with open(constants.HABITS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(habits_data, f, indent=2)
|
||||
|
||||
self.send_json(_enrich(habit), 200)
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
62
dashboard/handlers/pdf.py
Normal file
62
dashboard/handlers/pdf.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Markdown → PDF conversion endpoint (delegates to tools/generate_pdf.py)."""
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
import constants
|
||||
|
||||
|
||||
class PDFHandlers:
|
||||
"""Mixin for /api/pdf."""
|
||||
|
||||
def handle_pdf_post(self):
|
||||
"""Convert markdown to PDF (text-based) by spawning the venv python."""
|
||||
try:
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||
data = json.loads(post_data)
|
||||
|
||||
markdown_content = data.get('markdown', '')
|
||||
filename = data.get('filename', 'document.pdf')
|
||||
|
||||
if not markdown_content:
|
||||
self.send_json({'error': 'No markdown content'}, 400)
|
||||
return
|
||||
|
||||
venv_python = constants.VENV_PYTHON
|
||||
pdf_script = constants.TOOLS_DIR / 'generate_pdf.py'
|
||||
|
||||
if not venv_python.exists():
|
||||
self.send_json({'error': 'Venv Python not found'}, 500)
|
||||
return
|
||||
if not pdf_script.exists():
|
||||
self.send_json({'error': 'PDF generator script not found'}, 500)
|
||||
return
|
||||
|
||||
input_data = json.dumps({'markdown': markdown_content, 'filename': filename})
|
||||
result = subprocess.run(
|
||||
[str(venv_python), str(pdf_script)],
|
||||
input=input_data.encode('utf-8'),
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
error_msg = result.stderr.decode('utf-8', errors='replace')
|
||||
try:
|
||||
error_json = json.loads(error_msg)
|
||||
self.send_json(error_json, 500)
|
||||
except Exception:
|
||||
self.send_json({'error': error_msg}, 500)
|
||||
return
|
||||
|
||||
pdf_bytes = result.stdout
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/pdf')
|
||||
self.send_header('Content-Disposition', f'attachment; filename="{filename}"')
|
||||
self.send_header('Content-Length', str(len(pdf_bytes)))
|
||||
self.end_headers()
|
||||
self.wfile.write(pdf_bytes)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.send_json({'error': 'PDF generation timeout'}, 500)
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
1168
dashboard/handlers/projects.py
Normal file
1168
dashboard/handlers/projects.py
Normal file
File diff suppressed because it is too large
Load Diff
615
dashboard/handlers/ralph.py
Normal file
615
dashboard/handlers/ralph.py
Normal file
@@ -0,0 +1,615 @@
|
||||
"""Ralph live dashboard endpoints (W3 + instrumentation + realtime).
|
||||
|
||||
Endpoints:
|
||||
GET /api/ralph/status — toate proiectele Ralph (cards data)
|
||||
GET /api/ralph/stream — Server-Sent Events stream (realtime)
|
||||
GET /api/ralph/<slug>/log — tail progress.txt (default 100 lines)
|
||||
GET /api/ralph/<slug>/prd — full prd.json content
|
||||
GET /api/ralph/usage[?days=N] — rate limit budget summary (cross-project)
|
||||
POST /api/ralph/<slug>/stop — SIGTERM la Ralph PID
|
||||
POST /api/ralph/<slug>/rollback — git revert HEAD + decrement last passing story
|
||||
|
||||
SSE detail: stream emite `event: status\\ndata: <json>\\n\\n` la schimbări (poll
|
||||
fişiere la 2s); heartbeat la 30s pentru ca clientul să nu reseze conexiunea.
|
||||
Necesită ThreadingHTTPServer în api.py — altfel un singur stream blochează tot.
|
||||
|
||||
Citește status din `~/workspace/<slug>/scripts/ralph/`:
|
||||
- prd.json → stories (passes/failed/blocked/retries)
|
||||
- progress.txt → log human-readable
|
||||
- logs/iteration-*.log → mtime ultimului iter
|
||||
- .ralph.pid → PID activ (verificat cu os.kill 0)
|
||||
- usage.jsonl → token/cost log per iter (instrumentation MVP)
|
||||
|
||||
Reuse path constants din `dashboard/constants.py` (WORKSPACE_DIR).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import constants
|
||||
|
||||
from handlers._validators import _SLUG_RE, validate_slug
|
||||
|
||||
# Best-effort import of pure functions for /api/ralph/usage (instrumentation MVP).
|
||||
# Helper lives at <repo>/tools/ralph_usage.py — sibling of `dashboard/`.
|
||||
_TOOLS_DIR = Path(__file__).resolve().parents[2] / "tools"
|
||||
if str(_TOOLS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_TOOLS_DIR))
|
||||
try:
|
||||
import ralph_usage # type: ignore
|
||||
except ImportError: # pragma: no cover — diagnostic only
|
||||
ralph_usage = None # type: ignore
|
||||
|
||||
|
||||
# Path Ralph per proiect (mereu în scripts/ralph/)
|
||||
def _ralph_dir(project_dir: Path) -> Path:
|
||||
return project_dir / "scripts" / "ralph"
|
||||
|
||||
|
||||
# Estimare ETA simplistă: avg iter time × stories rămase
|
||||
DEFAULT_ITER_MINUTES = 12 # midpoint din intervalul 8-15min menționat în plan
|
||||
|
||||
|
||||
class RalphHandlers:
|
||||
"""Mixin pentru /api/ralph/* — Ralph live status + control."""
|
||||
|
||||
# ── helpers ────────────────────────────────────────────────
|
||||
def _ralph_validate_slug(self, slug: str):
|
||||
"""Validează slug-ul + returnează project_dir sau None.
|
||||
|
||||
Delegates the slug-shape check to the shared `validate_slug` helper
|
||||
in `dashboard/handlers/_validators.py`; only filesystem checks remain
|
||||
here (existence + path-confinement under WORKSPACE_DIR).
|
||||
"""
|
||||
if validate_slug(slug) is not None:
|
||||
return None
|
||||
project_dir = constants.WORKSPACE_DIR / slug
|
||||
try:
|
||||
resolved = project_dir.resolve()
|
||||
workspace_resolved = constants.WORKSPACE_DIR.resolve()
|
||||
resolved.relative_to(workspace_resolved)
|
||||
except (ValueError, OSError):
|
||||
return None
|
||||
if not project_dir.exists() or not project_dir.is_dir():
|
||||
return None
|
||||
return project_dir
|
||||
|
||||
def _ralph_pid_alive(self, ralph_dir: Path):
|
||||
"""Întoarce (running: bool, pid: int|None)."""
|
||||
pid_file = ralph_dir / ".ralph.pid"
|
||||
if not pid_file.exists():
|
||||
return False, None
|
||||
try:
|
||||
pid = int(pid_file.read_text().strip())
|
||||
os.kill(pid, 0) # signal 0 = check existence
|
||||
return True, pid
|
||||
except (ValueError, ProcessLookupError, PermissionError, OSError):
|
||||
return False, None
|
||||
|
||||
def _ralph_eta_minutes(self, stories_remaining: int, last_iter_mtime: float | None) -> int | None:
|
||||
"""Estimează minute rămase — None dacă nu avem date."""
|
||||
if stories_remaining <= 0:
|
||||
return 0
|
||||
return stories_remaining * DEFAULT_ITER_MINUTES
|
||||
|
||||
def _ralph_summarize_project(self, project_dir: Path) -> dict | None:
|
||||
"""Construiește dict de status per proiect — None dacă nu e Ralph project."""
|
||||
ralph_dir = _ralph_dir(project_dir)
|
||||
prd_json = ralph_dir / "prd.json"
|
||||
if not prd_json.exists():
|
||||
return None
|
||||
|
||||
# Defensive parse — corupt prd.json nu trebuie să dărâme dashboard
|
||||
try:
|
||||
prd = json.loads(prd_json.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return {
|
||||
"slug": project_dir.name,
|
||||
"status": "error",
|
||||
"error": "prd.json invalid sau ilizibil",
|
||||
"running": False,
|
||||
"pid": None,
|
||||
"stories": [],
|
||||
"storiesTotal": 0,
|
||||
"storiesComplete": 0,
|
||||
"storiesFailed": 0,
|
||||
"storiesBlocked": 0,
|
||||
}
|
||||
|
||||
stories = prd.get("userStories", []) or []
|
||||
total = len(stories)
|
||||
complete = sum(1 for s in stories if s.get("passes"))
|
||||
failed = sum(1 for s in stories if s.get("failed"))
|
||||
blocked = sum(1 for s in stories if s.get("blocked"))
|
||||
remaining = total - complete - failed - blocked
|
||||
|
||||
running, pid = self._ralph_pid_alive(ralph_dir)
|
||||
|
||||
# Last iteration mtime (pentru "acum X")
|
||||
logs_dir = ralph_dir / "logs"
|
||||
last_iter_mtime = None
|
||||
last_iter_iso = None
|
||||
if logs_dir.exists():
|
||||
iter_logs = sorted(logs_dir.glob("iteration-*.log"), key=lambda f: f.stat().st_mtime, reverse=True)
|
||||
if iter_logs:
|
||||
last_iter_mtime = iter_logs[0].stat().st_mtime
|
||||
last_iter_iso = datetime.fromtimestamp(last_iter_mtime).isoformat()
|
||||
|
||||
# Status compus pentru UI cards
|
||||
if running:
|
||||
top_status = "running"
|
||||
elif failed > 0 and remaining == 0:
|
||||
top_status = "failed"
|
||||
elif complete == total and total > 0:
|
||||
top_status = "complete"
|
||||
elif blocked > 0 and running is False:
|
||||
top_status = "blocked"
|
||||
else:
|
||||
top_status = "idle"
|
||||
|
||||
# Current story (DAG-eligible cel mai mic priority)
|
||||
current_story = None
|
||||
if running:
|
||||
eligible = [
|
||||
s for s in stories
|
||||
if not s.get("passes") and not s.get("failed") and not s.get("blocked")
|
||||
]
|
||||
eligible.sort(key=lambda s: (s.get("priority", 999), s.get("id", "")))
|
||||
if eligible:
|
||||
current_story = {
|
||||
"id": eligible[0].get("id"),
|
||||
"title": eligible[0].get("title"),
|
||||
"tags": eligible[0].get("tags", []),
|
||||
"retries": eligible[0].get("retries", 0),
|
||||
}
|
||||
|
||||
return {
|
||||
"slug": project_dir.name,
|
||||
"status": top_status,
|
||||
"running": running,
|
||||
"pid": pid,
|
||||
"branchName": prd.get("branchName", ""),
|
||||
"storiesTotal": total,
|
||||
"storiesComplete": complete,
|
||||
"storiesFailed": failed,
|
||||
"storiesBlocked": blocked,
|
||||
"storiesRemaining": remaining,
|
||||
"currentStory": current_story,
|
||||
"lastIterAt": last_iter_iso,
|
||||
"etaMinutes": self._ralph_eta_minutes(remaining, last_iter_mtime),
|
||||
"stories": [
|
||||
{
|
||||
"id": s.get("id"),
|
||||
"title": s.get("title"),
|
||||
"passes": bool(s.get("passes")),
|
||||
"failed": bool(s.get("failed")),
|
||||
"blocked": bool(s.get("blocked")),
|
||||
"retries": int(s.get("retries", 0)),
|
||||
"tags": s.get("tags", []),
|
||||
"failureReason": s.get("failureReason", ""),
|
||||
}
|
||||
for s in stories
|
||||
],
|
||||
}
|
||||
|
||||
def _ralph_collect_status(self) -> dict:
|
||||
"""Construieşte payload-ul de status pentru toate proiectele.
|
||||
|
||||
Folosit de `/api/ralph/status` (GET single-shot) şi de `/api/ralph/stream`
|
||||
(SSE — emis la schimbări).
|
||||
"""
|
||||
projects: list[dict] = []
|
||||
if constants.WORKSPACE_DIR.exists():
|
||||
for entry in sorted(constants.WORKSPACE_DIR.iterdir()):
|
||||
if not entry.is_dir() or entry.name.startswith("."):
|
||||
continue
|
||||
summary = self._ralph_summarize_project(entry)
|
||||
if summary is not None:
|
||||
projects.append(summary)
|
||||
return {
|
||||
"projects": projects,
|
||||
"fetchedAt": datetime.now().isoformat(),
|
||||
"count": len(projects),
|
||||
}
|
||||
|
||||
def _ralph_signature(self, snapshot: dict) -> tuple:
|
||||
"""Compactă semnătură pentru change-detection în SSE — doar fields care
|
||||
contează pentru UI (status, counts, current story). Timestamps de iter
|
||||
au granularitate de second pentru a evita flicker pe nanosecond drift.
|
||||
"""
|
||||
sig: list[tuple] = []
|
||||
for p in snapshot.get("projects", []) or []:
|
||||
cs = p.get("currentStory") or {}
|
||||
sig.append((
|
||||
p.get("slug"),
|
||||
p.get("status"),
|
||||
bool(p.get("running")),
|
||||
p.get("storiesTotal"),
|
||||
p.get("storiesComplete"),
|
||||
p.get("storiesFailed"),
|
||||
p.get("storiesBlocked"),
|
||||
p.get("lastIterAt"),
|
||||
cs.get("id"),
|
||||
cs.get("retries"),
|
||||
))
|
||||
return tuple(sorted(sig, key=lambda t: t[0] or ""))
|
||||
|
||||
# ── /api/ralph/status (GET) ────────────────────────────────
|
||||
def handle_ralph_status(self):
|
||||
"""Întoarce status pentru toate proiectele Ralph din workspace."""
|
||||
try:
|
||||
self.send_json(self._ralph_collect_status())
|
||||
except Exception as exc:
|
||||
self.send_json({"error": str(exc)}, 500)
|
||||
|
||||
# ── /api/ralph/stream (GET, SSE) ───────────────────────────
|
||||
def handle_ralph_stream(self):
|
||||
"""Server-Sent Events: emite snapshot la schimbări (poll fişiere 2s).
|
||||
|
||||
Heartbeat la 30s pentru a evita timeout pe proxy-uri. Loop-ul iese
|
||||
curat la BrokenPipe (clientul închis tab-ul). Necesită
|
||||
ThreadingHTTPServer în api.py — altfel blochează toate request-urile.
|
||||
"""
|
||||
try:
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
self.send_header("Cache-Control", "no-cache")
|
||||
self.send_header("Connection", "keep-alive")
|
||||
# Disable proxy buffering (nginx/cloudflare) — flush imediat
|
||||
self.send_header("X-Accel-Buffering", "no")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
return
|
||||
|
||||
last_signature: tuple | None = None
|
||||
last_heartbeat = time.monotonic()
|
||||
|
||||
# Initial snapshot — clientul nu aşteaptă primul change
|
||||
try:
|
||||
snapshot = self._ralph_collect_status()
|
||||
last_signature = self._ralph_signature(snapshot)
|
||||
payload = json.dumps(snapshot).encode("utf-8")
|
||||
self.wfile.write(b"event: status\ndata: " + payload + b"\n\n")
|
||||
self.wfile.flush()
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
return
|
||||
except Exception as exc:
|
||||
try:
|
||||
err = json.dumps({"error": str(exc)}).encode("utf-8")
|
||||
self.wfile.write(b"event: error\ndata: " + err + b"\n\n")
|
||||
self.wfile.flush()
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
# Stream loop
|
||||
while True:
|
||||
try:
|
||||
time.sleep(2)
|
||||
snapshot = self._ralph_collect_status()
|
||||
signature = self._ralph_signature(snapshot)
|
||||
now = time.monotonic()
|
||||
if signature != last_signature:
|
||||
payload = json.dumps(snapshot).encode("utf-8")
|
||||
self.wfile.write(b"event: status\ndata: " + payload + b"\n\n")
|
||||
self.wfile.flush()
|
||||
last_signature = signature
|
||||
last_heartbeat = now
|
||||
elif now - last_heartbeat >= 30:
|
||||
self.wfile.write(b"event: heartbeat\ndata: {}\n\n")
|
||||
self.wfile.flush()
|
||||
last_heartbeat = now
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
return
|
||||
except Exception:
|
||||
# Best-effort: o iteraţie eşuată nu trebuie să termine stream-ul,
|
||||
# dar dacă socketul e mort BrokenPipe va prinde next loop.
|
||||
continue
|
||||
|
||||
# ── /api/ralph/<slug>/log (GET) ────────────────────────────
|
||||
def handle_ralph_log(self, slug: str):
|
||||
"""Tail progress.txt pentru un slug. Default last 100 lines."""
|
||||
try:
|
||||
project_dir = self._ralph_validate_slug(slug)
|
||||
if not project_dir:
|
||||
self.send_json({"error": "Invalid project slug"}, 400)
|
||||
return
|
||||
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
qs = parse_qs(urlparse(self.path).query)
|
||||
try:
|
||||
lines_n = min(int(qs.get("lines", ["100"])[0]), 1000)
|
||||
except ValueError:
|
||||
lines_n = 100
|
||||
|
||||
progress = _ralph_dir(project_dir) / "progress.txt"
|
||||
if not progress.exists():
|
||||
self.send_json({"slug": slug, "lines": [], "total": 0})
|
||||
return
|
||||
|
||||
try:
|
||||
content = progress.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError as exc:
|
||||
self.send_json({"error": f"read failed: {exc}"}, 500)
|
||||
return
|
||||
|
||||
all_lines = content.splitlines()
|
||||
tail = all_lines[-lines_n:] if len(all_lines) > lines_n else all_lines
|
||||
self.send_json({
|
||||
"slug": slug,
|
||||
"lines": tail,
|
||||
"total": len(all_lines),
|
||||
})
|
||||
except Exception as exc:
|
||||
self.send_json({"error": str(exc)}, 500)
|
||||
|
||||
# ── /api/ralph/<slug>/prd (GET) ────────────────────────────
|
||||
def handle_ralph_prd(self, slug: str):
|
||||
"""Returnează full prd.json pentru un slug."""
|
||||
try:
|
||||
project_dir = self._ralph_validate_slug(slug)
|
||||
if not project_dir:
|
||||
self.send_json({"error": "Invalid project slug"}, 400)
|
||||
return
|
||||
|
||||
prd_json = _ralph_dir(project_dir) / "prd.json"
|
||||
if not prd_json.exists():
|
||||
self.send_json({"error": "prd.json not found"}, 404)
|
||||
return
|
||||
|
||||
try:
|
||||
data = json.loads(prd_json.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
self.send_json({"error": f"prd.json invalid: {exc}"}, 500)
|
||||
return
|
||||
|
||||
self.send_json(data)
|
||||
except Exception as exc:
|
||||
self.send_json({"error": str(exc)}, 500)
|
||||
|
||||
# ── /api/ralph/usage (GET) ─────────────────────────────────
|
||||
def handle_ralph_usage(self):
|
||||
"""Returnează rate limit budget summary cross-project.
|
||||
|
||||
Citește toate `~/workspace/<slug>/scripts/ralph/usage.jsonl`, le concatenează,
|
||||
rulează `ralph_usage.summarize` cu `?days=N` (default 7).
|
||||
|
||||
Răspuns:
|
||||
{
|
||||
"today": "YYYY-MM-DD",
|
||||
"today_cost": float,
|
||||
"today_runs": int,
|
||||
"window_days": N,
|
||||
"window_cost": float,
|
||||
"window_runs": int,
|
||||
"by_project": {...},
|
||||
"by_day": {...},
|
||||
"total_cost": float,
|
||||
"total_runs": int
|
||||
}
|
||||
"""
|
||||
try:
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
qs = parse_qs(urlparse(self.path).query)
|
||||
try:
|
||||
days = int(qs.get("days", ["7"])[0])
|
||||
if days <= 0:
|
||||
days = 7
|
||||
if days > 365:
|
||||
days = 365
|
||||
except ValueError:
|
||||
days = 7
|
||||
|
||||
if ralph_usage is None:
|
||||
self.send_json({"error": "ralph_usage helper unavailable"}, 500)
|
||||
return
|
||||
|
||||
entries: list[dict] = []
|
||||
if constants.WORKSPACE_DIR.exists():
|
||||
for entry in sorted(constants.WORKSPACE_DIR.iterdir()):
|
||||
if not entry.is_dir() or entry.name.startswith("."):
|
||||
continue
|
||||
usage_path = _ralph_dir(entry) / "usage.jsonl"
|
||||
if usage_path.exists():
|
||||
entries.extend(ralph_usage.parse_usage_jsonl(usage_path))
|
||||
|
||||
summary = ralph_usage.summarize(entries, days=days)
|
||||
summary["fetchedAt"] = datetime.now().isoformat()
|
||||
self.send_json(summary)
|
||||
except Exception as exc:
|
||||
self.send_json({"error": str(exc)}, 500)
|
||||
|
||||
# ── /api/ralph/<slug>/stop (POST) ──────────────────────────
|
||||
def handle_ralph_stop(self, slug: str):
|
||||
"""Trimite SIGTERM la Ralph PID. Verifică că PID-ul e în WORKSPACE_DIR."""
|
||||
try:
|
||||
project_dir = self._ralph_validate_slug(slug)
|
||||
if not project_dir:
|
||||
self.send_json({"success": False, "error": "Invalid project slug"}, 400)
|
||||
return
|
||||
|
||||
ralph_dir = _ralph_dir(project_dir)
|
||||
pid_file = ralph_dir / ".ralph.pid"
|
||||
if not pid_file.exists():
|
||||
self.send_json({"success": False, "error": "No PID file"}, 404)
|
||||
return
|
||||
|
||||
try:
|
||||
pid = int(pid_file.read_text().strip())
|
||||
except (ValueError, OSError) as exc:
|
||||
self.send_json({"success": False, "error": f"Invalid PID file: {exc}"}, 500)
|
||||
return
|
||||
|
||||
# Sandbox: verifică că procesul e în workspace (nu omoară random PID)
|
||||
try:
|
||||
proc_cwd = Path(f"/proc/{pid}/cwd").resolve()
|
||||
if not str(proc_cwd).startswith(str(constants.WORKSPACE_DIR)):
|
||||
self.send_json({"success": False, "error": "PID not in workspace"}, 403)
|
||||
return
|
||||
except (FileNotFoundError, PermissionError):
|
||||
# Procesul nu mai există — best-effort cleanup
|
||||
self.send_json({"success": True, "message": "Process already stopped"})
|
||||
return
|
||||
|
||||
try:
|
||||
os.killpg(os.getpgid(pid), signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
self.send_json({"success": True, "message": "Process already stopped"})
|
||||
return
|
||||
except PermissionError:
|
||||
self.send_json({"success": False, "error": "Permission denied"}, 403)
|
||||
return
|
||||
|
||||
self.send_json({"success": True, "message": f"Ralph stopped (PID {pid})"})
|
||||
except Exception as exc:
|
||||
self.send_json({"success": False, "error": str(exc)}, 500)
|
||||
|
||||
# ── /api/ralph/<slug>/rollback (POST) ──────────────────────
|
||||
def _ralph_decrement_last_pass(self, project_dir: Path) -> str | None:
|
||||
"""Marchează ultima story `passes=True` (din ordinea din prd.json) ca
|
||||
incompletă (`passes=False`, şterge `failed`/`blocked`/`failureReason`,
|
||||
retries=0). Atomic write (temp + rename). Întoarce id-ul story-ului
|
||||
sau None dacă nu există nimic de decrementat / prd.json invalid.
|
||||
"""
|
||||
prd_path = _ralph_dir(project_dir) / "prd.json"
|
||||
if not prd_path.exists():
|
||||
return None
|
||||
try:
|
||||
prd = json.loads(prd_path.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return None
|
||||
stories = prd.get("userStories", []) or []
|
||||
target_idx: int | None = None
|
||||
# ultima poziţională cu passes=True (DAG-order = ordine de finalizare)
|
||||
for i in range(len(stories) - 1, -1, -1):
|
||||
if stories[i].get("passes"):
|
||||
target_idx = i
|
||||
break
|
||||
if target_idx is None:
|
||||
return None
|
||||
story_id = stories[target_idx].get("id")
|
||||
stories[target_idx]["passes"] = False
|
||||
# Reset stare derivată — story-ul e disponibil pentru re-run
|
||||
stories[target_idx].pop("failed", None)
|
||||
stories[target_idx].pop("blocked", None)
|
||||
stories[target_idx].pop("failureReason", None)
|
||||
stories[target_idx]["retries"] = 0
|
||||
# Atomic write (acelaşi pattern ca W3 ralph_dag.py)
|
||||
tmp = prd_path.with_suffix(".json.tmp")
|
||||
try:
|
||||
tmp.write_text(json.dumps(prd, indent=2), encoding="utf-8")
|
||||
tmp.replace(prd_path)
|
||||
except OSError:
|
||||
tmp.unlink(missing_ok=True)
|
||||
return None
|
||||
return story_id
|
||||
|
||||
def handle_ralph_rollback(self, slug: str):
|
||||
"""Rollback ultimul commit într-un proiect Ralph.
|
||||
|
||||
Strategy: `git revert --no-edit HEAD` (history-preserving). Fallback la
|
||||
`git reset --hard HEAD~1` doar dacă revert eşuează (conflict, binary
|
||||
file). După succes, decrementează `passes` pe ultima story marcată
|
||||
complete în prd.json (atomic write).
|
||||
|
||||
Returns: `{success, message, reverted_commit, story_reverted, method}`.
|
||||
"""
|
||||
try:
|
||||
project_dir = self._ralph_validate_slug(slug)
|
||||
if not project_dir:
|
||||
self.send_json({
|
||||
"success": False,
|
||||
"message": "Invalid project slug",
|
||||
"reverted_commit": None,
|
||||
"story_reverted": None,
|
||||
}, 400)
|
||||
return
|
||||
|
||||
git_dir = project_dir / ".git"
|
||||
if not git_dir.exists():
|
||||
self.send_json({
|
||||
"success": False,
|
||||
"message": "Not a git repository",
|
||||
"reverted_commit": None,
|
||||
"story_reverted": None,
|
||||
}, 400)
|
||||
return
|
||||
|
||||
# Read HEAD before any operation (raportăm SHA-ul afectat)
|
||||
head_proc = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=str(project_dir), capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if head_proc.returncode != 0:
|
||||
self.send_json({
|
||||
"success": False,
|
||||
"message": f"git rev-parse HEAD failed: {head_proc.stderr.strip()}",
|
||||
"reverted_commit": None,
|
||||
"story_reverted": None,
|
||||
}, 500)
|
||||
return
|
||||
commit_to_revert = head_proc.stdout.strip()
|
||||
|
||||
# Try revert (preserves history, recommended)
|
||||
method = "revert"
|
||||
revert = subprocess.run(
|
||||
["git", "revert", "--no-edit", "HEAD"],
|
||||
cwd=str(project_dir), capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if revert.returncode != 0:
|
||||
# Conflict / binary file — abort & fall back to reset --hard
|
||||
subprocess.run(
|
||||
["git", "revert", "--abort"],
|
||||
cwd=str(project_dir), capture_output=True, timeout=10,
|
||||
)
|
||||
reset = subprocess.run(
|
||||
["git", "reset", "--hard", "HEAD~1"],
|
||||
cwd=str(project_dir), capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if reset.returncode != 0:
|
||||
self.send_json({
|
||||
"success": False,
|
||||
"message": (
|
||||
f"revert failed ({revert.stderr.strip()[:200]}), "
|
||||
f"reset failed ({reset.stderr.strip()[:200]})"
|
||||
),
|
||||
"reverted_commit": commit_to_revert,
|
||||
"story_reverted": None,
|
||||
}, 500)
|
||||
return
|
||||
method = "reset"
|
||||
|
||||
# Best-effort: decrement story passes (nu fail dacă lipseşte prd.json)
|
||||
story_reverted = self._ralph_decrement_last_pass(project_dir)
|
||||
|
||||
short_sha = commit_to_revert[:8]
|
||||
msg_bits = [f"Rolled back {short_sha} via git {method}"]
|
||||
if story_reverted:
|
||||
msg_bits.append(f"story {story_reverted} marked incomplete")
|
||||
self.send_json({
|
||||
"success": True,
|
||||
"message": "; ".join(msg_bits),
|
||||
"reverted_commit": commit_to_revert,
|
||||
"story_reverted": story_reverted,
|
||||
"method": method,
|
||||
})
|
||||
except subprocess.TimeoutExpired:
|
||||
self.send_json({
|
||||
"success": False,
|
||||
"message": "git operation timed out",
|
||||
"reverted_commit": None,
|
||||
"story_reverted": None,
|
||||
}, 500)
|
||||
except Exception as exc:
|
||||
self.send_json({
|
||||
"success": False,
|
||||
"message": str(exc),
|
||||
"reverted_commit": None,
|
||||
"story_reverted": None,
|
||||
}, 500)
|
||||
375
dashboard/handlers/workspace.py
Normal file
375
dashboard/handlers/workspace.py
Normal file
@@ -0,0 +1,375 @@
|
||||
"""~/workspace/ project control: list, run, stop, delete, logs."""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import constants
|
||||
|
||||
from handlers._validators import validate_slug
|
||||
|
||||
|
||||
class WorkspaceHandlers:
|
||||
"""Mixin for /api/workspace and /api/workspace/*."""
|
||||
|
||||
def _validate_project(self, name):
|
||||
"""Validate project name and return its path, or None."""
|
||||
if validate_slug(name) is not None:
|
||||
return None
|
||||
project_dir = constants.WORKSPACE_DIR / name
|
||||
if not project_dir.exists() or not project_dir.is_dir():
|
||||
return None
|
||||
if not str(project_dir.resolve()).startswith(str(constants.WORKSPACE_DIR)):
|
||||
return None
|
||||
return project_dir
|
||||
|
||||
# ── /api/workspace list ─────────────────────────────────────
|
||||
def handle_workspace_list(self):
|
||||
"""List projects in ~/workspace/ with Ralph status, git info, etc."""
|
||||
try:
|
||||
projects = []
|
||||
if not constants.WORKSPACE_DIR.exists():
|
||||
self.send_json({'projects': []})
|
||||
return
|
||||
|
||||
for project_dir in sorted(constants.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'))
|
||||
|
||||
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)
|
||||
ralph_running = True
|
||||
ralph_pid = pid
|
||||
except (ValueError, ProcessLookupError, PermissionError):
|
||||
pass
|
||||
|
||||
last_iter = None
|
||||
tech = {}
|
||||
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 (using _run_git from GitHandlers mixin)
|
||||
if (project_dir / '.git').exists():
|
||||
try:
|
||||
branch = self._run_git(project_dir, ['branch', '--show-current']).stdout.strip()
|
||||
last_commit = self._run_git(project_dir, ['log', '-1', '--format=%h - %s']).stdout.strip()
|
||||
status_out = self._run_git(project_dir, ['status', '--short']).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)
|
||||
|
||||
# ── /api/workspace/run (main | ralph | test) ───────────────
|
||||
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
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
(ralph_dir / '.ralph.pid').write_text(str(proc.pid))
|
||||
self.send_json({'success': True, 'pid': proc.pid, 'log': str(log_path)})
|
||||
|
||||
elif command == 'test':
|
||||
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())
|
||||
proc_cwd = Path(f'/proc/{pid}/cwd').resolve()
|
||||
if str(proc_cwd).startswith(str(constants.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':
|
||||
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(constants.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_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
|
||||
|
||||
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:
|
||||
params = parse_qs(urlparse(self.path).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'
|
||||
|
||||
if log_type == 'ralph':
|
||||
log_file = ralph_dir / 'logs' / 'ralph.log'
|
||||
if not log_file.exists():
|
||||
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'
|
||||
elif 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
|
||||
|
||||
if not str(log_file.resolve()).startswith(str(constants.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)
|
||||
257
dashboard/handlers/youtube.py
Normal file
257
dashboard/handlers/youtube.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""YouTube subtitle-download + note-creation endpoint."""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import constants
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _clean_vtt(content):
|
||||
"""Convert VTT captions to plain text."""
|
||||
lines = []
|
||||
seen = set()
|
||||
for line in content.split('\n'):
|
||||
if any([
|
||||
line.startswith('WEBVTT'),
|
||||
line.startswith('Kind:'),
|
||||
line.startswith('Language:'),
|
||||
'-->' in line,
|
||||
line.strip().startswith('<'),
|
||||
not line.strip(),
|
||||
re.match(r'^\d+$', line.strip()),
|
||||
]):
|
||||
continue
|
||||
clean = re.sub(r'<[^>]+>', '', line).strip()
|
||||
if clean and clean not in seen:
|
||||
seen.add(clean)
|
||||
lines.append(clean)
|
||||
return ' '.join(lines)
|
||||
|
||||
|
||||
def _is_description_about_video(description):
|
||||
"""Return True if description contains info about the video (chapters/topics)."""
|
||||
if not description or len(description.strip()) < 50:
|
||||
return False
|
||||
timestamp_pattern = re.compile(r'\b\d{1,2}:\d{2}(:\d{2})?\b')
|
||||
if len(timestamp_pattern.findall(description)) >= 3:
|
||||
return True
|
||||
lines = description.strip().split('\n')
|
||||
bullet_lines = [l for l in lines if re.match(r'^\s*[◼•\-\*▶►]\s+\S', l)]
|
||||
if len(bullet_lines) >= 3:
|
||||
return True
|
||||
numbered_lines = [l for l in lines if re.match(r'^\s*\d+[\.\)]\s+\S', l)]
|
||||
if len(numbered_lines) >= 3:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _extract_relevant_description(description):
|
||||
"""Strip promotional tails (links, social media) from description."""
|
||||
if not description:
|
||||
return ""
|
||||
promo_patterns = [
|
||||
re.compile(r'https?://\S+'),
|
||||
re.compile(r'instagram|twitter|facebook|tiktok|linkedin|patreon|spotify', re.I),
|
||||
re.compile(r'follow|subscribe|newsletter|merch|sponsor|affiliate', re.I),
|
||||
re.compile(r'purchase|buy|order|shop|store', re.I),
|
||||
]
|
||||
result_lines = []
|
||||
promo_streak = 0
|
||||
for line in description.strip().split('\n'):
|
||||
stripped = line.strip()
|
||||
is_promo = any(p.search(stripped) for p in promo_patterns)
|
||||
if is_promo:
|
||||
promo_streak += 1
|
||||
if promo_streak >= 2:
|
||||
break
|
||||
else:
|
||||
promo_streak = 0
|
||||
result_lines.append(line)
|
||||
while result_lines and not result_lines[-1].strip():
|
||||
result_lines.pop()
|
||||
return '\n'.join(result_lines)
|
||||
|
||||
|
||||
ANALYSIS_PROMPT = """\
|
||||
Ai primit transcriptul unui video YouTube și descrierea lui. Scrie o notiță KB în română, format Markdown.
|
||||
|
||||
Structura notei (în ordine):
|
||||
1. ## TL;DR — un paragraf de 3-5 rânduri care surprinde esența
|
||||
2. ## Puncte cheie — 6-10 puncte concise (pot fi bullets, dar scurte și dense)
|
||||
3. ## Quote-uri memorabile — 4-6 citate directe din transcript, în limba originală, între ghilimele
|
||||
4. ## Idei acționabile — 4-8 lucruri concrete pe care cititorul le poate face
|
||||
5. Secțiuni tematice cu ## heading — câte teme apar natural, în proze curgătoare (NU bullets), fiecare cu conținut real din transcript: cifre, exemple, mecanisme, argumente
|
||||
|
||||
Nu scrie metadate (titlu, url, tags, dată) — vor fi adăugate separat.
|
||||
Nu scrie fraze introductive despre tine sau despre video. Începe direct cu ## TL;DR.
|
||||
Scrie în română. Citatele rămân în engleză dacă sursa e engleză.
|
||||
"""
|
||||
|
||||
|
||||
def _analyze_with_claude(title, description, transcript):
|
||||
"""Call claude -p to generate rich analysis of the video."""
|
||||
claude_bin = os.path.expanduser('~/.local/bin/claude')
|
||||
if not os.path.exists(claude_bin):
|
||||
claude_bin = 'claude'
|
||||
|
||||
desc_section = ""
|
||||
if description:
|
||||
desc_section = f"DESCRIERE VIDEO:\n{description[:3000]}\n\n"
|
||||
|
||||
prompt = (
|
||||
f"{ANALYSIS_PROMPT}\n\n"
|
||||
f"TITLU: {title}\n\n"
|
||||
f"{desc_section}"
|
||||
f"TRANSCRIPT (primele 40000 caractere):\n{transcript[:40000]}"
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
[claude_bin, '-p', prompt],
|
||||
capture_output=True, text=True, timeout=300,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return result.stdout.strip()
|
||||
log.warning("Claude analysis failed: %s", result.stderr[:300])
|
||||
return None
|
||||
|
||||
|
||||
def _process_youtube(url):
|
||||
"""Download subtitles, save note."""
|
||||
yt_dlp = os.path.expanduser('~/.local/bin/yt-dlp')
|
||||
|
||||
result = subprocess.run(
|
||||
[yt_dlp, '--dump-json', '--no-download', url],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"Failed to get video info: {result.stderr}")
|
||||
return
|
||||
|
||||
info = json.loads(result.stdout)
|
||||
title = info.get('title', 'Unknown')
|
||||
duration = info.get('duration', 0)
|
||||
description = info.get('description', '')
|
||||
|
||||
temp_dir = Path('/tmp/yt_subs')
|
||||
temp_dir.mkdir(exist_ok=True)
|
||||
for f in temp_dir.glob('*'):
|
||||
f.unlink()
|
||||
|
||||
subprocess.run([
|
||||
yt_dlp, '--write-auto-subs', '--sub-langs', 'en',
|
||||
'--skip-download', '--sub-format', 'vtt',
|
||||
'-o', str(temp_dir / '%(id)s'),
|
||||
url,
|
||||
], capture_output=True, timeout=120)
|
||||
|
||||
transcript = None
|
||||
for sub_file in temp_dir.glob('*.vtt'):
|
||||
content = sub_file.read_text(encoding='utf-8', errors='replace')
|
||||
transcript = _clean_vtt(content)
|
||||
break
|
||||
|
||||
if not transcript:
|
||||
print("No subtitles found")
|
||||
return
|
||||
|
||||
date_str = datetime.now().strftime('%Y-%m-%d')
|
||||
slug = re.sub(r'[^\w\s-]', '', title.lower())[:50].strip().replace(' ', '-')
|
||||
filename = f"{date_str}_{slug}.md"
|
||||
|
||||
# Description block
|
||||
desc_block = ""
|
||||
if _is_description_about_video(description):
|
||||
relevant_desc = _extract_relevant_description(description)
|
||||
if relevant_desc:
|
||||
desc_block = f"\n## Descriere / Index\n\n{relevant_desc}\n\n---\n"
|
||||
|
||||
# Claude analysis: TL;DR + puncte cheie + citate + teme în proze
|
||||
print("Running Claude analysis...")
|
||||
analysis = _analyze_with_claude(title, description, transcript)
|
||||
|
||||
if analysis:
|
||||
note_content = f"""# {title}
|
||||
|
||||
**Video:** {url}
|
||||
**Duration:** {duration // 60}:{duration % 60:02d}
|
||||
**Saved:** {date_str}
|
||||
**Tags:** #youtube
|
||||
|
||||
---
|
||||
{desc_block}
|
||||
{analysis}
|
||||
"""
|
||||
else:
|
||||
# Fallback: save raw transcript if Claude fails
|
||||
note_content = f"""# {title}
|
||||
|
||||
**Video:** {url}
|
||||
**Duration:** {duration // 60}:{duration % 60:02d}
|
||||
**Saved:** {date_str}
|
||||
**Tags:** #youtube #to-summarize
|
||||
|
||||
---
|
||||
{desc_block}
|
||||
## Transcript
|
||||
|
||||
{transcript[:15000]}
|
||||
"""
|
||||
|
||||
constants.NOTES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
note_path = constants.NOTES_DIR / filename
|
||||
note_path.write_text(note_content, encoding='utf-8')
|
||||
|
||||
subprocess.run(
|
||||
[sys.executable, str(constants.TOOLS_DIR / 'update_notes_index.py')],
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
# Index new note with Ollama semantic embeddings
|
||||
try:
|
||||
sys.path.insert(0, str(constants.BASE_DIR))
|
||||
from src.memory_search import index_file, MEMORY_DIR
|
||||
n = index_file(note_path)
|
||||
log.info("Ollama indexed %s (%d chunks)", filename, n)
|
||||
except Exception as e:
|
||||
log.warning("Ollama indexing failed for %s: %s", filename, e)
|
||||
|
||||
print(f"Created note: {filename}")
|
||||
return filename
|
||||
|
||||
|
||||
class YoutubeHandlers:
|
||||
"""Mixin for /api/youtube."""
|
||||
|
||||
def handle_youtube(self):
|
||||
"""Process a YouTube URL: download subs, save note."""
|
||||
try:
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||
data = json.loads(post_data)
|
||||
url = data.get('url', '').strip()
|
||||
|
||||
if not url or ('youtube.com' not in url and 'youtu.be' not in url):
|
||||
self.send_json({'error': 'URL YouTube invalid'}, 400)
|
||||
return
|
||||
|
||||
try:
|
||||
print(f"Processing YouTube URL: {url}")
|
||||
_process_youtube(url)
|
||||
self.send_json({
|
||||
'status': 'done',
|
||||
'message': 'Notița a fost creată! Refresh pagina Notes.',
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"YouTube processing error: {e}")
|
||||
traceback.print_exc()
|
||||
self.send_json({'status': 'error', 'message': f'Eroare: {str(e)}'}, 500)
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
3237
dashboard/index.html
Normal file
3237
dashboard/index.html
Normal file
File diff suppressed because it is too large
Load Diff
69
dashboard/issues.json
Normal file
69
dashboard/issues.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"lastUpdated": "2026-03-31T20:02:48.501Z",
|
||||
"programs": [
|
||||
"ROACONT",
|
||||
"ROAGEST",
|
||||
"ROAIMOB",
|
||||
"ROAFACTURARE",
|
||||
"ROADEF",
|
||||
"ROASTART",
|
||||
"ROAPRINT",
|
||||
"ROAWEB",
|
||||
"Clawdbot",
|
||||
"Personal",
|
||||
"Altele"
|
||||
],
|
||||
"issues": [
|
||||
{
|
||||
"id": "ROA-004",
|
||||
"title": "Banca-Plati-Plata comision bancar 627- ar aparea si campul de Lucrare/Comanda",
|
||||
"description": "Banca-Plati-Plata comision bancar 627- ar aparea si campul de Lucrare/Comanda",
|
||||
"program": "ROACONT",
|
||||
"owner": "robert",
|
||||
"priority": "important",
|
||||
"status": "done",
|
||||
"created": "2026-02-12T13:19:01.786Z",
|
||||
"deadline": null,
|
||||
"completed": "2026-02-13T23:06:16.567Z"
|
||||
},
|
||||
{
|
||||
"id": "ROA-002",
|
||||
"title": "D406 - verificare SAFT account Id gol",
|
||||
"description": "",
|
||||
"program": "ROACONT",
|
||||
"owner": "robert",
|
||||
"priority": "urgent-important",
|
||||
"status": "done",
|
||||
"created": "2026-02-02T11:25:18.115Z",
|
||||
"deadline": "2026-02-02",
|
||||
"updated": "2026-02-02T22:27:06.428Z",
|
||||
"completed": "2026-02-03T17:20:07.195Z"
|
||||
},
|
||||
{
|
||||
"id": "ROA-001",
|
||||
"title": "D101: Mutare impozit precedent RD49→RD50",
|
||||
"description": "RD 49 = în urma inspecției fiscale\nRD 50 = impozit precedent\nFormularul nu recalculează impozitul de 16%\nRD 40 se modifică și la 4.1",
|
||||
"program": "ROACONT",
|
||||
"owner": "marius",
|
||||
"priority": "important",
|
||||
"status": "done",
|
||||
"created": "2026-01-30T15:10:00Z",
|
||||
"deadline": "2026-02-06",
|
||||
"updated": "2026-02-02T22:26:59.690Z",
|
||||
"completed": "2026-02-05T21:53:55.392Z"
|
||||
},
|
||||
{
|
||||
"id": "ROA-003",
|
||||
"title": "Auto-copiere manoperă din devize stimative în devize reale",
|
||||
"description": "",
|
||||
"program": "ROAGEST",
|
||||
"owner": "robert",
|
||||
"priority": "backlog",
|
||||
"status": "done",
|
||||
"created": "2026-02-12T10:03:13.378157+00:00",
|
||||
"deadline": null,
|
||||
"updated": "2026-02-13T13:03:45.355Z",
|
||||
"completed": "2026-03-31T20:02:48.489Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
291
dashboard/login.html
Normal file
291
dashboard/login.html
Normal file
@@ -0,0 +1,291 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/svg+xml" href="/echo/favicon.svg">
|
||||
<title>Echo — Autentificare</title>
|
||||
<link rel="stylesheet" href="/echo/static/tokens.css">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-base, #13131a);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-base);
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: var(--space-6) var(--space-4);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.login-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-5);
|
||||
width: min(380px, 100% - 48px);
|
||||
}
|
||||
|
||||
.monogram {
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 700;
|
||||
font-size: 56px;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--accent);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-6);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.login-title {
|
||||
margin: 0 0 var(--space-1) 0;
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
margin: 0 0 var(--space-5) 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-base);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: 0 0 0 3px var(--accent-subtle);
|
||||
}
|
||||
|
||||
.form-input.is-invalid {
|
||||
border-color: var(--error);
|
||||
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.18);
|
||||
}
|
||||
|
||||
.form-error {
|
||||
min-height: 1.25em;
|
||||
margin-top: var(--space-2);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--error);
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.form-error.is-visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
margin-top: var(--space-5);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: var(--accent);
|
||||
color: #ffffff;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-base);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-fast), border-color var(--transition-fast), opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
.submit-btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--accent-subtle);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-card { padding: var(--space-5); }
|
||||
.monogram { font-size: 48px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="login-shell">
|
||||
<div class="monogram" aria-hidden="true">E</div>
|
||||
<section class="login-card">
|
||||
<h1 class="login-title">Echo Dashboard</h1>
|
||||
<p class="login-subtitle">Autentificare</p>
|
||||
<form id="login-form" method="post" action="/echo/api/auth/login" novalidate>
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="token-input">Token de acces</label>
|
||||
<input
|
||||
id="token-input"
|
||||
name="token"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
aria-label="Token de acces"
|
||||
aria-describedby="form-error"
|
||||
required>
|
||||
<div id="form-error" class="form-error" role="alert" aria-live="polite"></div>
|
||||
</div>
|
||||
<button id="submit-btn" type="submit" class="submit-btn">Intră</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var form = document.getElementById('login-form');
|
||||
var input = document.getElementById('token-input');
|
||||
var btn = document.getElementById('submit-btn');
|
||||
var errorEl = document.getElementById('form-error');
|
||||
|
||||
var DEFAULT_LABEL = 'Intră';
|
||||
var SUBMITTING_LABEL = 'Se autentifică...';
|
||||
var RETRY_LABEL = 'Reîncearcă';
|
||||
|
||||
// Auto-focus input on load (skip on touch devices to avoid keyboard pop)
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
if (!('ontouchstart' in window)) {
|
||||
try { input.focus(); } catch (e) { /* ignore */ }
|
||||
}
|
||||
});
|
||||
|
||||
// Clear error styling as soon as the user edits the field
|
||||
input.addEventListener('input', function () {
|
||||
if (input.classList.contains('is-invalid')) {
|
||||
input.classList.remove('is-invalid');
|
||||
errorEl.textContent = '';
|
||||
errorEl.classList.remove('is-visible');
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener('submit', function (ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
var token = input.value.trim();
|
||||
if (!token) {
|
||||
input.classList.add('is-invalid');
|
||||
errorEl.textContent = 'Token invalid';
|
||||
errorEl.classList.add('is-visible');
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Submitting state
|
||||
btn.disabled = true;
|
||||
btn.textContent = SUBMITTING_LABEL;
|
||||
input.classList.remove('is-invalid');
|
||||
errorEl.textContent = '';
|
||||
errorEl.classList.remove('is-visible');
|
||||
|
||||
var body = 'token=' + encodeURIComponent(token);
|
||||
|
||||
fetch('/echo/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json, text/html'
|
||||
},
|
||||
body: body,
|
||||
credentials: 'same-origin',
|
||||
redirect: 'follow'
|
||||
}).then(function (res) {
|
||||
// Browsers auto-follow 302, so a successful login surfaces
|
||||
// here as a 2xx (workspace.html) or an opaqueredirect.
|
||||
if (res.ok || res.type === 'opaqueredirect' || res.redirected) {
|
||||
// Redirect back to the page the user originally wanted,
|
||||
// passed as ?next= by the server. Validate it's a safe
|
||||
// relative /echo/ path to prevent open-redirect attacks.
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var next = params.get('next') || '';
|
||||
// The proxy strips /echo/ before Python, so `next` is
|
||||
// e.g. "/workspace.html". Re-add the /echo prefix for
|
||||
// the browser. Guard against open-redirect (no ://).
|
||||
var dest = (next && /^\/[^/]/.test(next) && next.indexOf('://') === -1)
|
||||
? '/echo' + next
|
||||
: '/echo/workspace.html';
|
||||
window.location.assign(dest);
|
||||
return;
|
||||
}
|
||||
if (res.status === 401) {
|
||||
showInvalid();
|
||||
return;
|
||||
}
|
||||
// Any other status — treat as a generic failure
|
||||
showInvalid();
|
||||
}).catch(function () {
|
||||
showInvalid();
|
||||
});
|
||||
});
|
||||
|
||||
function showInvalid() {
|
||||
input.classList.add('is-invalid');
|
||||
errorEl.textContent = 'Token invalid';
|
||||
errorEl.classList.add('is-visible');
|
||||
btn.disabled = false;
|
||||
btn.textContent = RETRY_LABEL;
|
||||
try { input.focus(); input.select(); } catch (e) { /* ignore */ }
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
dashboard/memory
Symbolic link
1
dashboard/memory
Symbolic link
@@ -0,0 +1 @@
|
||||
/home/moltbot/echo-core/memory
|
||||
1
dashboard/notes-data
Symbolic link
1
dashboard/notes-data
Symbolic link
@@ -0,0 +1 @@
|
||||
/home/moltbot/echo-core/memory/kb
|
||||
1295
dashboard/notes.html
Normal file
1295
dashboard/notes.html
Normal file
File diff suppressed because it is too large
Load Diff
123
dashboard/swipe-nav.js
Normal file
123
dashboard/swipe-nav.js
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Swipe Navigation for Echo
|
||||
* Swipe left/right to navigate between pages
|
||||
*/
|
||||
(function() {
|
||||
const pages = ['index.html', 'workspace.html', 'notes.html', 'habits.html', 'files.html'];
|
||||
|
||||
// Get current page index
|
||||
function getCurrentIndex() {
|
||||
const path = window.location.pathname;
|
||||
let filename = path.split('/').pop() || 'index.html';
|
||||
// Handle /echo/ without filename
|
||||
if (filename === '' || filename === 'echo') filename = 'index.html';
|
||||
const idx = pages.indexOf(filename);
|
||||
return idx >= 0 ? idx : 0;
|
||||
}
|
||||
|
||||
// Navigate to page
|
||||
function navigateTo(index) {
|
||||
if (index >= 0 && index < pages.length) {
|
||||
window.location.href = pages[index];
|
||||
}
|
||||
}
|
||||
|
||||
// Swipe detection
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let touchEndX = 0;
|
||||
let touchEndY = 0;
|
||||
|
||||
const minSwipeDistance = 80;
|
||||
const maxVerticalDistance = 100;
|
||||
|
||||
document.addEventListener('touchstart', function(e) {
|
||||
touchStartX = e.changedTouches[0].screenX;
|
||||
touchStartY = e.changedTouches[0].screenY;
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchend', function(e) {
|
||||
touchEndX = e.changedTouches[0].screenX;
|
||||
touchEndY = e.changedTouches[0].screenY;
|
||||
handleSwipe();
|
||||
}, { passive: true });
|
||||
|
||||
function handleSwipe() {
|
||||
const deltaX = touchEndX - touchStartX;
|
||||
const deltaY = Math.abs(touchEndY - touchStartY);
|
||||
|
||||
// Ignore if vertical swipe or too short
|
||||
if (deltaY > maxVerticalDistance) return;
|
||||
if (Math.abs(deltaX) < minSwipeDistance) return;
|
||||
|
||||
const currentIndex = getCurrentIndex();
|
||||
|
||||
if (deltaX > 0) {
|
||||
// Swipe right → previous page
|
||||
navigateTo(currentIndex - 1);
|
||||
} else {
|
||||
// Swipe left → next page
|
||||
navigateTo(currentIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Visual indicator (optional dots)
|
||||
function createIndicator() {
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'swipe-indicator';
|
||||
indicator.innerHTML = pages.map((_, i) =>
|
||||
`<span class="swipe-dot ${i === getCurrentIndex() ? 'active' : ''}"></span>`
|
||||
).join('');
|
||||
document.body.appendChild(indicator);
|
||||
}
|
||||
|
||||
// Add indicator styles
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.swipe-indicator {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
z-index: 9999;
|
||||
padding: 10px 16px;
|
||||
background: rgba(50, 50, 60, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 24px;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.swipe-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.swipe-dot.active {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
transform: scale(1.3);
|
||||
box-shadow: 0 0 8px rgba(59, 130, 246, 0.6);
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
.swipe-indicator { display: none; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Init after DOM ready
|
||||
function init() {
|
||||
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
|
||||
createIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
1129
dashboard/tests/test_habits_api.py
Normal file
1129
dashboard/tests/test_habits_api.py
Normal file
File diff suppressed because it is too large
Load Diff
2868
dashboard/tests/test_habits_frontend.py
Normal file
2868
dashboard/tests/test_habits_frontend.py
Normal file
File diff suppressed because it is too large
Load Diff
573
dashboard/tests/test_habits_helpers.py
Normal file
573
dashboard/tests/test_habits_helpers.py
Normal file
@@ -0,0 +1,573 @@
|
||||
"""
|
||||
Tests for habits_helpers.py
|
||||
|
||||
Tests cover all helper functions for habit tracking including:
|
||||
- calculate_streak for all 6 frequency types
|
||||
- should_check_today for all frequency types
|
||||
- get_completion_rate
|
||||
- get_weekly_summary
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Add parent directory to path to import habits_helpers
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from habits_helpers import (
|
||||
calculate_streak,
|
||||
should_check_today,
|
||||
get_completion_rate,
|
||||
get_weekly_summary,
|
||||
check_and_award_weekly_lives
|
||||
)
|
||||
|
||||
|
||||
def test_calculate_streak_daily_consecutive():
|
||||
"""Test daily streak with consecutive days."""
|
||||
today = datetime.now().date()
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": [
|
||||
{"date": today.isoformat()},
|
||||
{"date": (today - timedelta(days=1)).isoformat()},
|
||||
{"date": (today - timedelta(days=2)).isoformat()},
|
||||
]
|
||||
}
|
||||
assert calculate_streak(habit) == 3
|
||||
|
||||
|
||||
def test_calculate_streak_daily_with_gap():
|
||||
"""Test daily streak breaks on gap."""
|
||||
today = datetime.now().date()
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": [
|
||||
{"date": today.isoformat()},
|
||||
{"date": (today - timedelta(days=1)).isoformat()},
|
||||
# Gap here (day 2 missing)
|
||||
{"date": (today - timedelta(days=3)).isoformat()},
|
||||
]
|
||||
}
|
||||
assert calculate_streak(habit) == 2
|
||||
|
||||
|
||||
def test_calculate_streak_daily_empty():
|
||||
"""Test daily streak with no completions."""
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": []
|
||||
}
|
||||
assert calculate_streak(habit) == 0
|
||||
|
||||
|
||||
def test_calculate_streak_specific_days():
|
||||
"""Test specific_days streak (Mon, Wed, Fri)."""
|
||||
today = datetime.now().date()
|
||||
|
||||
# Find the most recent Monday
|
||||
days_since_monday = today.weekday()
|
||||
last_monday = today - timedelta(days=days_since_monday)
|
||||
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "specific_days",
|
||||
"days": [0, 2, 4] # Mon, Wed, Fri (0=Mon in Python weekday)
|
||||
},
|
||||
"completions": [
|
||||
{"date": last_monday.isoformat()}, # Mon
|
||||
{"date": (last_monday - timedelta(days=2)).isoformat()}, # Fri previous week
|
||||
{"date": (last_monday - timedelta(days=4)).isoformat()}, # Wed previous week
|
||||
]
|
||||
}
|
||||
|
||||
# Should count 3 consecutive relevant days
|
||||
streak = calculate_streak(habit)
|
||||
assert streak >= 1 # At least the most recent relevant day
|
||||
|
||||
|
||||
def test_calculate_streak_x_per_week():
|
||||
"""Test x_per_week streak (3 times per week)."""
|
||||
today = datetime.now().date()
|
||||
|
||||
# Find Monday of current week
|
||||
days_since_monday = today.weekday()
|
||||
monday = today - timedelta(days=days_since_monday)
|
||||
|
||||
# Current week: 3 completions (Mon, Tue, Wed)
|
||||
# Previous week: 3 completions (Mon, Tue, Wed)
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "x_per_week",
|
||||
"count": 3
|
||||
},
|
||||
"completions": [
|
||||
{"date": monday.isoformat()}, # This week Mon
|
||||
{"date": (monday + timedelta(days=1)).isoformat()}, # This week Tue
|
||||
{"date": (monday + timedelta(days=2)).isoformat()}, # This week Wed
|
||||
# Previous week
|
||||
{"date": (monday - timedelta(days=7)).isoformat()}, # Last week Mon
|
||||
{"date": (monday - timedelta(days=6)).isoformat()}, # Last week Tue
|
||||
{"date": (monday - timedelta(days=5)).isoformat()}, # Last week Wed
|
||||
]
|
||||
}
|
||||
|
||||
streak = calculate_streak(habit)
|
||||
assert streak >= 2 # Both weeks meet the target
|
||||
|
||||
|
||||
def test_calculate_streak_weekly():
|
||||
"""Test weekly streak (at least 1 per week)."""
|
||||
today = datetime.now().date()
|
||||
|
||||
habit = {
|
||||
"frequency": {"type": "weekly"},
|
||||
"completions": [
|
||||
{"date": today.isoformat()}, # This week
|
||||
{"date": (today - timedelta(days=7)).isoformat()}, # Last week
|
||||
{"date": (today - timedelta(days=14)).isoformat()}, # 2 weeks ago
|
||||
]
|
||||
}
|
||||
|
||||
streak = calculate_streak(habit)
|
||||
assert streak >= 1
|
||||
|
||||
|
||||
def test_calculate_streak_monthly():
|
||||
"""Test monthly streak (at least 1 per month)."""
|
||||
today = datetime.now().date()
|
||||
|
||||
# This month
|
||||
habit = {
|
||||
"frequency": {"type": "monthly"},
|
||||
"completions": [
|
||||
{"date": today.isoformat()},
|
||||
]
|
||||
}
|
||||
|
||||
streak = calculate_streak(habit)
|
||||
assert streak >= 1
|
||||
|
||||
|
||||
def test_calculate_streak_custom_interval():
|
||||
"""Test custom interval streak (every 3 days)."""
|
||||
today = datetime.now().date()
|
||||
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "custom",
|
||||
"interval": 3
|
||||
},
|
||||
"completions": [
|
||||
{"date": today.isoformat()},
|
||||
{"date": (today - timedelta(days=3)).isoformat()},
|
||||
{"date": (today - timedelta(days=6)).isoformat()},
|
||||
]
|
||||
}
|
||||
|
||||
streak = calculate_streak(habit)
|
||||
assert streak == 3
|
||||
|
||||
|
||||
def test_should_check_today_daily():
|
||||
"""Test should_check_today for daily habit."""
|
||||
habit = {"frequency": {"type": "daily"}}
|
||||
assert should_check_today(habit) is True
|
||||
|
||||
|
||||
def test_should_check_today_specific_days():
|
||||
"""Test should_check_today for specific_days habit."""
|
||||
today_weekday = datetime.now().date().weekday()
|
||||
|
||||
# Habit relevant today
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "specific_days",
|
||||
"days": [today_weekday]
|
||||
}
|
||||
}
|
||||
assert should_check_today(habit) is True
|
||||
|
||||
# Habit not relevant today
|
||||
other_day = (today_weekday + 1) % 7
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "specific_days",
|
||||
"days": [other_day]
|
||||
}
|
||||
}
|
||||
assert should_check_today(habit) is False
|
||||
|
||||
|
||||
def test_should_check_today_x_per_week():
|
||||
"""Test should_check_today for x_per_week habit."""
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "x_per_week",
|
||||
"count": 3
|
||||
}
|
||||
}
|
||||
assert should_check_today(habit) is True
|
||||
|
||||
|
||||
def test_should_check_today_weekly():
|
||||
"""Test should_check_today for weekly habit."""
|
||||
habit = {"frequency": {"type": "weekly"}}
|
||||
assert should_check_today(habit) is True
|
||||
|
||||
|
||||
def test_should_check_today_monthly():
|
||||
"""Test should_check_today for monthly habit."""
|
||||
habit = {"frequency": {"type": "monthly"}}
|
||||
assert should_check_today(habit) is True
|
||||
|
||||
|
||||
def test_should_check_today_custom_ready():
|
||||
"""Test should_check_today for custom interval when ready."""
|
||||
today = datetime.now().date()
|
||||
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "custom",
|
||||
"interval": 3
|
||||
},
|
||||
"completions": [
|
||||
{"date": (today - timedelta(days=3)).isoformat()}
|
||||
]
|
||||
}
|
||||
assert should_check_today(habit) is True
|
||||
|
||||
|
||||
def test_should_check_today_custom_not_ready():
|
||||
"""Test should_check_today for custom interval when not ready."""
|
||||
today = datetime.now().date()
|
||||
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "custom",
|
||||
"interval": 3
|
||||
},
|
||||
"completions": [
|
||||
{"date": (today - timedelta(days=1)).isoformat()}
|
||||
]
|
||||
}
|
||||
assert should_check_today(habit) is False
|
||||
|
||||
|
||||
def test_get_completion_rate_daily_perfect():
|
||||
"""Test completion rate for daily habit with 100%."""
|
||||
today = datetime.now().date()
|
||||
|
||||
completions = []
|
||||
for i in range(30):
|
||||
completions.append({"date": (today - timedelta(days=i)).isoformat()})
|
||||
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": completions
|
||||
}
|
||||
|
||||
rate = get_completion_rate(habit, days=30)
|
||||
assert rate == 100.0
|
||||
|
||||
|
||||
def test_get_completion_rate_daily_half():
|
||||
"""Test completion rate for daily habit with 50%."""
|
||||
today = datetime.now().date()
|
||||
|
||||
completions = []
|
||||
for i in range(0, 30, 2): # Every other day
|
||||
completions.append({"date": (today - timedelta(days=i)).isoformat()})
|
||||
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": completions
|
||||
}
|
||||
|
||||
rate = get_completion_rate(habit, days=30)
|
||||
assert 45 <= rate <= 55 # Around 50%
|
||||
|
||||
|
||||
def test_get_completion_rate_specific_days():
|
||||
"""Test completion rate for specific_days habit."""
|
||||
today = datetime.now().date()
|
||||
today_weekday = today.weekday()
|
||||
|
||||
# Create habit for Mon, Wed, Fri
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "specific_days",
|
||||
"days": [0, 2, 4]
|
||||
},
|
||||
"completions": []
|
||||
}
|
||||
|
||||
# Add completions for all relevant days in last 30 days
|
||||
for i in range(30):
|
||||
check_date = today - timedelta(days=i)
|
||||
if check_date.weekday() in [0, 2, 4]:
|
||||
habit["completions"].append({"date": check_date.isoformat()})
|
||||
|
||||
rate = get_completion_rate(habit, days=30)
|
||||
assert rate == 100.0
|
||||
|
||||
|
||||
def test_get_completion_rate_empty():
|
||||
"""Test completion rate with no completions."""
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": []
|
||||
}
|
||||
|
||||
rate = get_completion_rate(habit, days=30)
|
||||
assert rate == 0.0
|
||||
|
||||
|
||||
def test_get_weekly_summary():
|
||||
"""Test weekly summary returns correct structure."""
|
||||
today = datetime.now().date()
|
||||
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": [
|
||||
{"date": today.isoformat()},
|
||||
{"date": (today - timedelta(days=1)).isoformat()},
|
||||
]
|
||||
}
|
||||
|
||||
summary = get_weekly_summary(habit)
|
||||
|
||||
# Check structure
|
||||
assert isinstance(summary, dict)
|
||||
assert "Monday" in summary
|
||||
assert "Tuesday" in summary
|
||||
assert "Wednesday" in summary
|
||||
assert "Thursday" in summary
|
||||
assert "Friday" in summary
|
||||
assert "Saturday" in summary
|
||||
assert "Sunday" in summary
|
||||
|
||||
# Check values are valid
|
||||
valid_statuses = ["checked", "skipped", "missed", "upcoming", "not_relevant"]
|
||||
for day, status in summary.items():
|
||||
assert status in valid_statuses
|
||||
|
||||
|
||||
def test_get_weekly_summary_with_skip():
|
||||
"""Test weekly summary handles skipped days."""
|
||||
today = datetime.now().date()
|
||||
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": [
|
||||
{"date": today.isoformat(), "type": "check"},
|
||||
{"date": (today - timedelta(days=1)).isoformat(), "type": "skip"},
|
||||
]
|
||||
}
|
||||
|
||||
summary = get_weekly_summary(habit)
|
||||
|
||||
# Find today's day name
|
||||
day_names = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||
today_name = day_names[today.weekday()]
|
||||
yesterday_name = day_names[(today.weekday() - 1) % 7]
|
||||
|
||||
assert summary[today_name] == "checked"
|
||||
assert summary[yesterday_name] == "skipped"
|
||||
|
||||
|
||||
def test_get_weekly_summary_specific_days():
|
||||
"""Test weekly summary marks non-relevant days correctly."""
|
||||
today = datetime.now().date()
|
||||
today_weekday = today.weekday()
|
||||
|
||||
# Habit only for Monday (0)
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "specific_days",
|
||||
"days": [0]
|
||||
},
|
||||
"completions": []
|
||||
}
|
||||
|
||||
summary = get_weekly_summary(habit)
|
||||
|
||||
# All days except Monday should be not_relevant or upcoming
|
||||
for day_name, status in summary.items():
|
||||
if day_name == "Monday":
|
||||
continue # Monday can be any status
|
||||
if status not in ["upcoming", "not_relevant"]:
|
||||
# Day should be not_relevant if it's in the past
|
||||
pass
|
||||
|
||||
|
||||
def test_check_and_award_weekly_lives_awards_life_with_checkin():
|
||||
"""Test that +1 life is awarded if there was ≥1 check-in in previous week."""
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
|
||||
# Add check-in in previous week (Wednesday)
|
||||
habit = {
|
||||
"lives": 2,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "check"}
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == True
|
||||
assert new_lives == 3
|
||||
|
||||
|
||||
def test_check_and_award_weekly_lives_no_award_without_checkin():
|
||||
"""Test that no life is awarded if there were no check-ins in previous week."""
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
|
||||
# Add check-in in current week only
|
||||
habit = {
|
||||
"lives": 2,
|
||||
"completions": [
|
||||
{"date": (current_week_start + timedelta(days=1)).isoformat(), "type": "check"}
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == False
|
||||
assert new_lives == 2
|
||||
|
||||
|
||||
def test_check_and_award_weekly_lives_no_duplicate_award():
|
||||
"""Test that life is not awarded twice in the same week."""
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
|
||||
# Add check-in in previous week and mark as already awarded this week
|
||||
habit = {
|
||||
"lives": 3,
|
||||
"lastLivesAward": current_week_start.isoformat(),
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "check"}
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == False
|
||||
assert new_lives == 3
|
||||
|
||||
|
||||
def test_check_and_award_weekly_lives_skip_doesnt_count():
|
||||
"""Test that skips don't count toward weekly recovery."""
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
|
||||
# Add only skips in previous week, no check-ins
|
||||
habit = {
|
||||
"lives": 1,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "skip"},
|
||||
{"date": (previous_week_start + timedelta(days=4)).isoformat(), "type": "skip"}
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == False
|
||||
assert new_lives == 1
|
||||
|
||||
|
||||
def test_check_and_award_weekly_lives_multiple_checkins():
|
||||
"""Test that award works with multiple check-ins in previous week."""
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
|
||||
# Add multiple check-ins in previous week
|
||||
habit = {
|
||||
"lives": 2,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=1)).isoformat(), "type": "check"},
|
||||
{"date": (previous_week_start + timedelta(days=3)).isoformat(), "type": "check"},
|
||||
{"date": (previous_week_start + timedelta(days=5)).isoformat(), "type": "check"}
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == True
|
||||
assert new_lives == 3
|
||||
|
||||
|
||||
def test_check_and_award_weekly_lives_no_cap():
|
||||
"""Test that lives can accumulate beyond 3."""
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
|
||||
# Habit with 5 lives
|
||||
habit = {
|
||||
"lives": 5,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "check"}
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == True
|
||||
assert new_lives == 6
|
||||
|
||||
|
||||
def test_check_and_award_weekly_lives_missing_last_award_field():
|
||||
"""Test backward compatibility when lastLivesAward field is missing."""
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
|
||||
# Habit without lastLivesAward field (backward compatible)
|
||||
habit = {
|
||||
"lives": 2,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "check"}
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == True
|
||||
assert new_lives == 3
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run all tests
|
||||
import inspect
|
||||
|
||||
test_functions = [
|
||||
obj for name, obj in inspect.getmembers(sys.modules[__name__])
|
||||
if inspect.isfunction(obj) and name.startswith("test_")
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for test_func in test_functions:
|
||||
try:
|
||||
test_func()
|
||||
print(f"✓ {test_func.__name__}")
|
||||
passed += 1
|
||||
except AssertionError as e:
|
||||
print(f"✗ {test_func.__name__}: {e}")
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f"✗ {test_func.__name__}: {type(e).__name__}: {e}")
|
||||
failed += 1
|
||||
|
||||
print(f"\n{passed} passed, {failed} failed")
|
||||
sys.exit(0 if failed == 0 else 1)
|
||||
555
dashboard/tests/test_habits_integration.py
Normal file
555
dashboard/tests/test_habits_integration.py
Normal file
@@ -0,0 +1,555 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Integration tests for Habits feature - End-to-end flows
|
||||
|
||||
Tests complete workflows involving multiple API calls and state transitions.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import shutil
|
||||
from datetime import datetime, timedelta
|
||||
from http.server import HTTPServer
|
||||
from threading import Thread
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
# Add parent directory to path to import api module
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from api import TaskBoardHandler
|
||||
import habits_helpers
|
||||
|
||||
|
||||
# Test helpers
|
||||
def setup_test_env():
|
||||
"""Create temporary environment for testing"""
|
||||
from pathlib import Path
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
habits_file = Path(temp_dir) / 'habits.json'
|
||||
|
||||
# Initialize empty habits file
|
||||
with open(habits_file, 'w') as f:
|
||||
json.dump({'lastUpdated': datetime.now().isoformat(), 'habits': []}, f)
|
||||
|
||||
# Override HABITS_FILE constant
|
||||
import api
|
||||
api.HABITS_FILE = habits_file
|
||||
|
||||
return temp_dir
|
||||
|
||||
|
||||
def teardown_test_env(temp_dir):
|
||||
"""Clean up temporary environment"""
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
def start_test_server():
|
||||
"""Start HTTP server on random port for testing"""
|
||||
server = HTTPServer(('localhost', 0), TaskBoardHandler)
|
||||
thread = Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
return server
|
||||
|
||||
|
||||
def http_request(url, method='GET', data=None):
|
||||
"""Make HTTP request and return response data"""
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
if data:
|
||||
data = json.dumps(data).encode('utf-8')
|
||||
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
body = response.read().decode('utf-8')
|
||||
return json.loads(body) if body else None
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode('utf-8')
|
||||
try:
|
||||
return {'error': json.loads(error_body), 'status': e.code}
|
||||
except:
|
||||
return {'error': error_body, 'status': e.code}
|
||||
|
||||
|
||||
# Integration Tests
|
||||
|
||||
def test_01_create_and_checkin_increments_streak():
|
||||
"""Integration test: create habit → check-in → verify streak is 1"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Create daily habit
|
||||
habit_data = {
|
||||
'name': 'Morning meditation',
|
||||
'category': 'health',
|
||||
'color': '#10B981',
|
||||
'icon': 'brain',
|
||||
'priority': 50,
|
||||
'frequency': {'type': 'daily'}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
if 'error' in result:
|
||||
print(f"Error creating habit: {result}")
|
||||
assert 'id' in result, f"Should return created habit with ID, got: {result}"
|
||||
habit_id = result['id']
|
||||
|
||||
# Check in today
|
||||
checkin_result = http_request(f"{base_url}/api/habits/{habit_id}/check", method='POST')
|
||||
|
||||
# Verify streak incremented to 1
|
||||
assert checkin_result['streak']['current'] == 1, "Streak should be 1 after first check-in"
|
||||
assert checkin_result['streak']['best'] == 1, "Best streak should be 1 after first check-in"
|
||||
assert checkin_result['streak']['lastCheckIn'] == datetime.now().date().isoformat(), "Last check-in should be today"
|
||||
|
||||
print("✓ Test 1: Create + check-in → streak is 1")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_02_seven_consecutive_checkins_restore_life():
|
||||
"""Integration test: 7 consecutive check-ins → life restored (if below 3)"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Create daily habit
|
||||
habit_data = {
|
||||
'name': 'Daily exercise',
|
||||
'category': 'health',
|
||||
'color': '#EF4444',
|
||||
'icon': 'dumbbell',
|
||||
'priority': 50,
|
||||
'frequency': {'type': 'daily'}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
habit_id = result['id']
|
||||
|
||||
# Manually set lives to 1 (instead of using skip API which would add completions)
|
||||
import api
|
||||
with open(api.HABITS_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
habit_obj = next(h for h in data['habits'] if h['id'] == habit_id)
|
||||
habit_obj['lives'] = 1 # Directly set to 1 (simulating 2 skips used)
|
||||
|
||||
# Add 7 consecutive check-in completions for the past 7 days
|
||||
for i in range(7):
|
||||
check_date = (datetime.now() - timedelta(days=6-i)).date().isoformat()
|
||||
habit_obj['completions'].append({
|
||||
'date': check_date,
|
||||
'type': 'check'
|
||||
})
|
||||
|
||||
# Recalculate streak and check for life restore
|
||||
habit_obj['streak'] = {
|
||||
'current': habits_helpers.calculate_streak(habit_obj),
|
||||
'best': max(habit_obj['streak']['best'], habits_helpers.calculate_streak(habit_obj)),
|
||||
'lastCheckIn': datetime.now().date().isoformat()
|
||||
}
|
||||
|
||||
# Check life restore logic: last 7 completions all 'check' type
|
||||
last_7 = habit_obj['completions'][-7:]
|
||||
if len(last_7) == 7 and all(c.get('type') == 'check' for c in last_7):
|
||||
if habit_obj['lives'] < 3:
|
||||
habit_obj['lives'] += 1
|
||||
|
||||
data['lastUpdated'] = datetime.now().isoformat()
|
||||
with open(api.HABITS_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
# Get updated habit
|
||||
habits = http_request(f"{base_url}/api/habits")
|
||||
habit = next(h for h in habits if h['id'] == habit_id)
|
||||
|
||||
# Verify life restored
|
||||
assert habit['lives'] == 2, f"Should have 2 lives after 7 consecutive check-ins (was {habit['lives']})"
|
||||
assert habit['current_streak'] == 7, "Should have streak of 7"
|
||||
|
||||
print("✓ Test 2: 7 consecutive check-ins → life restored")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_03_skip_with_life_maintains_streak():
|
||||
"""Integration test: skip with life → lives decremented, streak unchanged"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Create daily habit
|
||||
habit_data = {
|
||||
'name': 'Read book',
|
||||
'category': 'growth',
|
||||
'color': '#3B82F6',
|
||||
'icon': 'book',
|
||||
'priority': 50,
|
||||
'frequency': {'type': 'daily'}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
habit_id = result['id']
|
||||
|
||||
# Check in yesterday (to build a streak)
|
||||
import api
|
||||
with open(api.HABITS_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
habit_obj = next(h for h in data['habits'] if h['id'] == habit_id)
|
||||
yesterday = (datetime.now() - timedelta(days=1)).date().isoformat()
|
||||
habit_obj['completions'].append({
|
||||
'date': yesterday,
|
||||
'type': 'check'
|
||||
})
|
||||
habit_obj['streak'] = {
|
||||
'current': 1,
|
||||
'best': 1,
|
||||
'lastCheckIn': yesterday
|
||||
}
|
||||
|
||||
data['lastUpdated'] = datetime.now().isoformat()
|
||||
with open(api.HABITS_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
# Skip today
|
||||
skip_result = http_request(f"{base_url}/api/habits/{habit_id}/skip", method='POST')
|
||||
|
||||
# Verify lives decremented and streak maintained
|
||||
assert skip_result['lives'] == 2, "Lives should be 2 after skip"
|
||||
|
||||
# Get fresh habit data to check streak
|
||||
habits = http_request(f"{base_url}/api/habits")
|
||||
habit = next(h for h in habits if h['id'] == habit_id)
|
||||
|
||||
# Streak should still be 1 (skip doesn't break it)
|
||||
assert habit['current_streak'] == 1, "Streak should be maintained after skip"
|
||||
|
||||
print("✓ Test 3: Skip with life → lives decremented, streak unchanged")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_04_skip_with_zero_lives_returns_400():
|
||||
"""Integration test: skip with 0 lives → returns 400 error"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Create daily habit
|
||||
habit_data = {
|
||||
'name': 'Yoga practice',
|
||||
'category': 'health',
|
||||
'color': '#8B5CF6',
|
||||
'icon': 'heart',
|
||||
'priority': 50,
|
||||
'frequency': {'type': 'daily'}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
habit_id = result['id']
|
||||
|
||||
# Use all 3 lives
|
||||
http_request(f"{base_url}/api/habits/{habit_id}/skip", method='POST')
|
||||
http_request(f"{base_url}/api/habits/{habit_id}/skip", method='POST')
|
||||
http_request(f"{base_url}/api/habits/{habit_id}/skip", method='POST')
|
||||
|
||||
# Attempt to skip with 0 lives
|
||||
result = http_request(f"{base_url}/api/habits/{habit_id}/skip", method='POST')
|
||||
|
||||
# Verify 400 error
|
||||
assert result['status'] == 400, "Should return 400 status"
|
||||
assert 'error' in result, "Should return error message"
|
||||
|
||||
print("✓ Test 4: Skip with 0 lives → returns 400 error")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_05_edit_frequency_changes_should_check_today():
|
||||
"""Integration test: edit frequency → should_check_today logic changes"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Create daily habit
|
||||
habit_data = {
|
||||
'name': 'Code review',
|
||||
'category': 'work',
|
||||
'color': '#F59E0B',
|
||||
'icon': 'code',
|
||||
'priority': 50,
|
||||
'frequency': {'type': 'daily'}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
habit_id = result['id']
|
||||
|
||||
# Verify should_check_today is True for daily habit
|
||||
habits = http_request(f"{base_url}/api/habits")
|
||||
habit = next(h for h in habits if h['id'] == habit_id)
|
||||
assert habit['should_check_today'] == True, "Daily habit should be checkable today"
|
||||
|
||||
# Edit to specific_days (only Monday and Wednesday)
|
||||
update_data = {
|
||||
'name': 'Code review',
|
||||
'category': 'work',
|
||||
'color': '#F59E0B',
|
||||
'icon': 'code',
|
||||
'priority': 50,
|
||||
'frequency': {
|
||||
'type': 'specific_days',
|
||||
'days': ['monday', 'wednesday']
|
||||
}
|
||||
}
|
||||
|
||||
http_request(f"{base_url}/api/habits/{habit_id}", method='PUT', data=update_data)
|
||||
|
||||
# Get updated habit
|
||||
habits = http_request(f"{base_url}/api/habits")
|
||||
habit = next(h for h in habits if h['id'] == habit_id)
|
||||
|
||||
# Verify should_check_today reflects new frequency
|
||||
today_name = datetime.now().strftime('%A').lower()
|
||||
expected = today_name in ['monday', 'wednesday']
|
||||
assert habit['should_check_today'] == expected, f"Should check today should be {expected} for {today_name}"
|
||||
|
||||
print(f"✓ Test 5: Edit frequency → should_check_today is {expected} for {today_name}")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_06_delete_removes_habit_from_storage():
|
||||
"""Integration test: delete → habit removed from storage"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Create habit
|
||||
habit_data = {
|
||||
'name': 'Guitar practice',
|
||||
'category': 'personal',
|
||||
'color': '#EC4899',
|
||||
'icon': 'music',
|
||||
'priority': 50,
|
||||
'frequency': {'type': 'daily'}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
habit_id = result['id']
|
||||
|
||||
# Verify habit exists
|
||||
habits = http_request(f"{base_url}/api/habits")
|
||||
assert len(habits) == 1, "Should have 1 habit"
|
||||
assert habits[0]['id'] == habit_id, "Should be the created habit"
|
||||
|
||||
# Delete habit
|
||||
http_request(f"{base_url}/api/habits/{habit_id}", method='DELETE')
|
||||
|
||||
# Verify habit removed
|
||||
habits = http_request(f"{base_url}/api/habits")
|
||||
assert len(habits) == 0, "Should have 0 habits after delete"
|
||||
|
||||
# Verify not in storage file
|
||||
import api
|
||||
with open(api.HABITS_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert len(data['habits']) == 0, "Storage file should have 0 habits"
|
||||
|
||||
print("✓ Test 6: Delete → habit removed from storage")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_07_checkin_on_wrong_day_for_specific_days_returns_400():
|
||||
"""Integration test: check-in on wrong day for specific_days → returns 400"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Get today's day name
|
||||
today_name = datetime.now().strftime('%A').lower()
|
||||
|
||||
# Create habit for different days (not today)
|
||||
if today_name == 'monday':
|
||||
allowed_days = ['tuesday', 'wednesday']
|
||||
elif today_name == 'tuesday':
|
||||
allowed_days = ['monday', 'wednesday']
|
||||
else:
|
||||
allowed_days = ['monday', 'tuesday']
|
||||
|
||||
habit_data = {
|
||||
'name': 'Gym workout',
|
||||
'category': 'health',
|
||||
'color': '#EF4444',
|
||||
'icon': 'dumbbell',
|
||||
'priority': 50,
|
||||
'frequency': {
|
||||
'type': 'specific_days',
|
||||
'days': allowed_days
|
||||
}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
habit_id = result['id']
|
||||
|
||||
# Attempt to check in today (wrong day)
|
||||
result = http_request(f"{base_url}/api/habits/{habit_id}/check", method='POST')
|
||||
|
||||
# Verify 400 error
|
||||
assert result['status'] == 400, "Should return 400 status"
|
||||
assert 'error' in result, "Should return error message"
|
||||
|
||||
print(f"✓ Test 7: Check-in on {today_name} (not in {allowed_days}) → returns 400")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_08_get_response_includes_all_stats():
|
||||
"""Integration test: GET response includes stats (streak, completion_rate, weekly_summary)"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Create habit with some completions
|
||||
habit_data = {
|
||||
'name': 'Meditation',
|
||||
'category': 'health',
|
||||
'color': '#10B981',
|
||||
'icon': 'brain',
|
||||
'priority': 50,
|
||||
'frequency': {'type': 'daily'}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
habit_id = result['id']
|
||||
|
||||
# Add some completions
|
||||
import api
|
||||
with open(api.HABITS_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
habit_obj = next(h for h in data['habits'] if h['id'] == habit_id)
|
||||
|
||||
# Add completions for last 3 days
|
||||
for i in range(3):
|
||||
check_date = (datetime.now() - timedelta(days=2-i)).date().isoformat()
|
||||
habit_obj['completions'].append({
|
||||
'date': check_date,
|
||||
'type': 'check'
|
||||
})
|
||||
|
||||
habit_obj['streak'] = {
|
||||
'current': 3,
|
||||
'best': 3,
|
||||
'lastCheckIn': datetime.now().date().isoformat()
|
||||
}
|
||||
|
||||
data['lastUpdated'] = datetime.now().isoformat()
|
||||
with open(api.HABITS_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
# Get habits
|
||||
habits = http_request(f"{base_url}/api/habits")
|
||||
habit = habits[0]
|
||||
|
||||
# Verify all enriched stats are present
|
||||
assert 'current_streak' in habit, "Should include current_streak"
|
||||
assert 'best_streak' in habit, "Should include best_streak"
|
||||
assert 'completion_rate_30d' in habit, "Should include completion_rate_30d"
|
||||
assert 'weekly_summary' in habit, "Should include weekly_summary"
|
||||
assert 'should_check_today' in habit, "Should include should_check_today"
|
||||
|
||||
# Verify streak values
|
||||
assert habit['current_streak'] == 3, "Current streak should be 3"
|
||||
assert habit['best_streak'] == 3, "Best streak should be 3"
|
||||
|
||||
# Verify weekly_summary structure
|
||||
assert isinstance(habit['weekly_summary'], dict), "Weekly summary should be a dict"
|
||||
days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
||||
for day in days:
|
||||
assert day in habit['weekly_summary'], f"Weekly summary should include {day}"
|
||||
|
||||
print("✓ Test 8: GET response includes all stats (streak, completion_rate, weekly_summary)")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_09_typecheck_passes():
|
||||
"""Integration test: Typecheck passes"""
|
||||
result = os.system('python3 -m py_compile /home/moltbot/clawd/dashboard/api.py')
|
||||
assert result == 0, "Typecheck should pass for api.py"
|
||||
|
||||
result = os.system('python3 -m py_compile /home/moltbot/clawd/dashboard/habits_helpers.py')
|
||||
assert result == 0, "Typecheck should pass for habits_helpers.py"
|
||||
|
||||
print("✓ Test 9: Typecheck passes")
|
||||
|
||||
|
||||
# Run all tests
|
||||
if __name__ == '__main__':
|
||||
tests = [
|
||||
test_01_create_and_checkin_increments_streak,
|
||||
test_02_seven_consecutive_checkins_restore_life,
|
||||
test_03_skip_with_life_maintains_streak,
|
||||
test_04_skip_with_zero_lives_returns_400,
|
||||
test_05_edit_frequency_changes_should_check_today,
|
||||
test_06_delete_removes_habit_from_storage,
|
||||
test_07_checkin_on_wrong_day_for_specific_days_returns_400,
|
||||
test_08_get_response_includes_all_stats,
|
||||
test_09_typecheck_passes,
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
print("Running integration tests...\n")
|
||||
|
||||
for test in tests:
|
||||
try:
|
||||
test()
|
||||
passed += 1
|
||||
except AssertionError as e:
|
||||
print(f"✗ {test.__name__}: {e}")
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f"✗ {test.__name__}: Unexpected error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
failed += 1
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Integration Tests: {passed} passed, {failed} failed")
|
||||
print(f"{'='*50}")
|
||||
|
||||
sys.exit(0 if failed == 0 else 1)
|
||||
134
dashboard/tests/test_weekly_lives_integration.py
Normal file
134
dashboard/tests/test_weekly_lives_integration.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Integration test for weekly lives recovery feature.
|
||||
|
||||
Tests the full flow:
|
||||
1. Habit has check-ins in previous week
|
||||
2. Check-in today triggers weekly lives recovery
|
||||
3. Response includes livesAwarded flag
|
||||
4. Lives count increases
|
||||
5. Duplicate awards are prevented
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from habits_helpers import check_and_award_weekly_lives
|
||||
|
||||
|
||||
def test_integration_weekly_lives_award():
|
||||
"""Test complete weekly lives recovery flow."""
|
||||
print("\n=== Testing Weekly Lives Recovery Integration ===\n")
|
||||
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
|
||||
# Scenario 1: New habit with check-ins in previous week
|
||||
print("Scenario 1: First award of the week")
|
||||
habit = {
|
||||
"id": "test-habit-1",
|
||||
"name": "Test Habit",
|
||||
"lives": 2,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "check"},
|
||||
{"date": (previous_week_start + timedelta(days=4)).isoformat(), "type": "check"},
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == True, "Expected life to be awarded"
|
||||
assert new_lives == 3, f"Expected 3 lives, got {new_lives}"
|
||||
print(f"✓ Lives awarded: {habit['lives']} → {new_lives}")
|
||||
print(f"✓ Award flag: {was_awarded}")
|
||||
|
||||
# Scenario 2: Already awarded this week
|
||||
print("\nScenario 2: Prevent duplicate award")
|
||||
habit['lives'] = new_lives
|
||||
habit['lastLivesAward'] = current_week_start.isoformat()
|
||||
|
||||
new_lives2, was_awarded2 = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded2 == False, "Expected no duplicate award"
|
||||
assert new_lives2 == 3, f"Lives should remain at 3, got {new_lives2}"
|
||||
print(f"✓ No duplicate award: lives remain at {new_lives2}")
|
||||
|
||||
# Scenario 3: Only skips in previous week
|
||||
print("\nScenario 3: Skips don't qualify for recovery")
|
||||
habit_with_skips = {
|
||||
"id": "test-habit-2",
|
||||
"name": "Habit with Skips",
|
||||
"lives": 1,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "skip"},
|
||||
{"date": (previous_week_start + timedelta(days=4)).isoformat(), "type": "skip"},
|
||||
]
|
||||
}
|
||||
|
||||
new_lives3, was_awarded3 = check_and_award_weekly_lives(habit_with_skips)
|
||||
|
||||
assert was_awarded3 == False, "Skips shouldn't trigger award"
|
||||
assert new_lives3 == 1, f"Lives should remain at 1, got {new_lives3}"
|
||||
print(f"✓ Skips don't count: lives remain at {new_lives3}")
|
||||
|
||||
# Scenario 4: No cap on lives (can go beyond 3)
|
||||
print("\nScenario 4: Lives can exceed 3")
|
||||
habit_many_lives = {
|
||||
"id": "test-habit-3",
|
||||
"name": "Habit with Many Lives",
|
||||
"lives": 5,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "check"},
|
||||
]
|
||||
}
|
||||
|
||||
new_lives4, was_awarded4 = check_and_award_weekly_lives(habit_many_lives)
|
||||
|
||||
assert was_awarded4 == True, "Expected life to be awarded"
|
||||
assert new_lives4 == 6, f"Expected 6 lives, got {new_lives4}"
|
||||
print(f"✓ No cap: lives increased from 5 → {new_lives4}")
|
||||
|
||||
# Scenario 5: No check-ins in previous week
|
||||
print("\nScenario 5: No check-ins = no award")
|
||||
habit_no_checkins = {
|
||||
"id": "test-habit-4",
|
||||
"name": "New Habit",
|
||||
"lives": 2,
|
||||
"completions": []
|
||||
}
|
||||
|
||||
new_lives5, was_awarded5 = check_and_award_weekly_lives(habit_no_checkins)
|
||||
|
||||
assert was_awarded5 == False, "No check-ins = no award"
|
||||
assert new_lives5 == 2, f"Lives should remain at 2, got {new_lives5}"
|
||||
print(f"✓ No previous week check-ins: lives remain at {new_lives5}")
|
||||
|
||||
print("\n=== All Integration Tests Passed! ===\n")
|
||||
|
||||
# Print summary of the feature
|
||||
print("Feature Summary:")
|
||||
print("• +1 life awarded per week if habit had ≥1 check-in in previous week")
|
||||
print("• Monday-Sunday week boundaries (ISO 8601)")
|
||||
print("• Award triggers on first check-in of current week")
|
||||
print("• Skips don't count toward recovery")
|
||||
print("• No cap on lives (can accumulate beyond 3)")
|
||||
print("• Prevents duplicate awards in same week")
|
||||
print("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
test_integration_weekly_lives_award()
|
||||
sys.exit(0)
|
||||
except AssertionError as e:
|
||||
print(f"\n✗ Test failed: {e}\n")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\n✗ Unexpected error: {type(e).__name__}: {e}\n")
|
||||
sys.exit(1)
|
||||
252
dashboard/todos.json
Normal file
252
dashboard/todos.json
Normal file
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"lastUpdated": "2026-03-25T22:59:24.849Z",
|
||||
"items": [
|
||||
{
|
||||
"id": "prov-2026-02-25",
|
||||
"text": "Provocare: Un proiect - Pentru cine?",
|
||||
"context": "Brendan Burchard: 'Dubiul nu e problema. Oprirea e problema.' Când dubiul devine semnal să înveți (nu să te oprești), câștigi. Problema ta nu e competența (25 ani expertiză) - e TEAMA de primul pas. Credința 'clienți noi = mai multă muncă' te blochează să vezi dincolo de poveste. Adevărul: fiecare lucru pe care îl eviți îți arată EXACT unde trebuie să mergi. În business de ARTĂ (expertiza unică), scaling-ul vine prin CLARITATE despre valoare, nu volum. Problema nu e că nu ai clienți - e că nu știi pentru cine lupți. Când Brendan și-a terminat cartea în 18 zile (după ani de blocaj), nu a fost pentru bani - a fost pentru SOȚIA lui dormind sub greutatea facturilor. Schimbarea: de la 'cum supraviețuiesc' la 'pentru cine lupt'. Proiectele tale rămân 80% done pentru că le lipsește CONVICTION - nu e 'ar fi bine' ci 'TREBUIE pentru cineva anume'. Întrebarea e: 'Pentru cine fac asta?'",
|
||||
"example": "Alege UN proiect (ROA web, chatbot Maria, angajat nou, orice activ) și răspunde SINCER: 'Dacă aș renunța la asta mâine, cine ar pierde?' Dacă răspunsul e 'Nimeni specific' sau 'Ar fi util general' → e half-hearted. Fie oprești proiectul (temporar), fie găsești conviction real (cineva anume). Dacă răspunsul e 'Clientul X care depinde de rapoarte rapide' sau 'Colegă 70 ani care vrea autonomie' → e full conviction. Continuă. Nu trebuie să FACI nimic cu răspunsul - doar să îl VEZI. Exemplu ROA web: Dacă renunț mâine → cine pierde? Răspuns vag: 'Clienții ar beneficia' = half-hearted. Răspuns concret: 'Clientul Y sună de 5 ori/săptămână pentru raport X. Dacă ar avea web, și-ar lua singur' = conviction. Când vezi clar CINE beneficiază, primul pas devine natural. Dubiul nu dispare prin planuri perfecte - dispare prin primul pas, oricât de mic. Primul pas: 5 minute, un proiect, o întrebare, VEZI adevărul.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-25",
|
||||
"done": true,
|
||||
"doneAt": "2026-03-25T22:59:21.977Z",
|
||||
"source": "Brendan Burchard - Billionaire Coach (Conviction vs Half-heartedness)",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-02-23_billionaire-coach-abundance-mindset.md",
|
||||
"createdAt": "2026-02-25T07:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-24",
|
||||
"text": "Provocare: Audit Conviction - identifică proiecte half-hearted vs full",
|
||||
"context": "Half-heartedness = cel mai mare inamic al abundenței. Nu poți construi afacere, relație sau viață cu un picior înăuntru și unul afară. Brendan Burchard: 'Breakthroughul vine când lupți pentru ALTCINEVA, nu pentru supraviețuire.' Diferența: Supraviețuire = 'Cum plătesc factura?' (umpli un GOL). Abundență = 'Cui servesc cu expertiza asta?' (construiești). Wealthy people nu se gândesc la supraviețuire - se gândesc la servire, dare, construire. Când un proiect e half-hearted ('ar fi bine'), rămâne 80% done, momentum pierdut. Când e full conviction (PENTRU CINEVA anume), livrare completă, flow în loc de greutate. Exercițiul te ajută să identifici CE e cu conviction reală și CE e doar 'ar fi util'.",
|
||||
"example": "Listează proiectele curente (ROA web, Chatbot Maria, Angajat nou, Clienți noi) și pentru fiecare răspunde: E full conviction (PENTRU CINE?) sau half-hearted (ar fi bine)? De exemplu: ROA web - dacă răspunsul e 'ar fi util pentru clienți' (vag) = half-hearted. Dacă răspunsul e 'Clientul X TREBUIE să aibă acces rapid la rapoarte pentru a lua decizii la timp' (specific, cineva anume) = full conviction. Când identifici unul half-hearted, reframe-ul: NU 'ce câștig EU?' ci 'CINE beneficiază când asta e complet?' Bonus ZAPS antidot: când apare dubiul 'Nu sunt destul de deștept' (attach self) → STOP, recunoaște 'Mă ZAPS-ez?', reframe 'Ce învăț din asta?' (nu 'Mă opresc'), reset BMF (Breath 3 respirații + Movement 10 pași + Food check). Brendan: 'Doubt is not the problem. Stopping is. If doubt is a signal to learn — you win.'",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-24",
|
||||
"done": true,
|
||||
"doneAt": "2026-03-25T22:59:13.743Z",
|
||||
"source": "Brendan Burchard - Billionaire Coach (Abundență vs Supraviețuire)",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-02-23_billionaire-coach-abundance-mindset.md",
|
||||
"createdAt": "2026-02-24T07:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-23",
|
||||
"text": "Provocare: Identifică tipul de business - ARTĂ sau LIFESTYLE?",
|
||||
"context": "Greșeala majoră: aplici regulile greșite pentru tipul tău de business. Monica Ion: 'Când nu știi tipul de business, e ca și cum nu știi ce boală tratezi. Orice medicament poate face mai mult rău decât bine.' Există 4 tipuri (Artă, Lifestyle, Exit, Legacy) - fiecare cu scop și reguli diferite. Succesul vine din a te cunoaște pe tine și a juca după regulile tipului tău. TOATE blocajele tale (clienți noi=mai multă muncă, prețuri scăzute, angajat greu de învățat) vin din CONFUZIE DE TIP. Dacă e ARTĂ: creștere personală + prețuri mai mari (NU mai mulți clienți). Dacă e LIFESTYLE: sisteme eficiente + documentare procese. Testul rapid: Clienții vin pentru TINE (expertiza unică) sau pentru PROCES (rezultate predictibile)? Proiectele sunt personalizate sau pattern repetabil?",
|
||||
"example": "Scenariul: Ar trebui să cauți clienți noi dar eziti ('mai multă muncă'). ARTĂ: greșit să adaugi clienți - soluția e să CREȘTI PREȚURILE pentru clienții existenți și să SELECTEZI doar cei premium. Angajatul e suport operațional (nu clone al tău). Un client perfect e mai bun decât 5 obișnuiți. LIFESTYLE: corect că e mai multă muncă - ai nevoie de SISTEME mai eficiente. Angajatul învață PROCESUL (nu expertiza ta). Documentezi proceduri standard. Sau: Nu îndrăznești să crești prețurile. ARTĂ: blocare interioară (vină/rușine/merit scăzut) - muncă pe curățenie emoțională, apoi creștere prețuri 2-3x. LIFESTYLE: nu știi numerele - calculează break-even real (ore + cheltuieli + profit motivant) și setează preț matematic.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-23",
|
||||
"done": true,
|
||||
"doneAt": "2026-03-25T22:59:14.522Z",
|
||||
"source": "Monica Ion - Cele 4 tipuri de business",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-02-19_cele-4-tipuri-de-business.md",
|
||||
"createdAt": "2026-02-23T07:04:14.171922+00:00"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-22",
|
||||
"text": "Provocare: Schimbă corpul ÎNAINTE de decizie - fiziologie pentru acțiune",
|
||||
"context": "Inacțiunea antreprenorială nu e în minte - e în CORP. Corpul ghemuire (umeri căzuți, respirație superficială) comunică: 'Nu sunt suficient. E periculos să ies.' Și mintea urmează corpul. Tony Robbins: 'Depresia are o postură. Schimbă corpul PRIMUL — mișcă-te, respiră diferit.' Corpul GENEREAZĂ starea, nu o reflectă. Când aștepți să te simți 'pregătit' pentru a acționa — corpul spune: 'Nu suntem acolo încă.' Când acționezi CU CORPUL ÎNTÂI (miști, respiri, te ridici) — starea vine DUPĂ. Nu aștepți încredere - o CREEZI cu fiziologia.",
|
||||
"example": "Scenariul: Ar trebui să suni un client nou pentru un proiect mai mare. Simți ezitare: 'E prea scump, poate zice nu...' VECHIUL MOD: Stai la birou, gândești, analizezi, amâni. NOUL MOD: (1) Simți ezitarea → ridică-te imediat (2) 3x pe vârfuri (activează corpul) (3) 5 respirații profunde în piept (deschide corp, încredere) (4) 10 pași rapizi prin cameră (5) ACUM suni clientul - cu corp deschis, respirație plină. REZULTAT: Același gând ('poate zice nu'), dar corp diferit = emoție diferită = acțiune.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-22",
|
||||
"done": true,
|
||||
"doneAt": "2026-03-25T22:59:15.239Z",
|
||||
"source": "Tony Robbins - The Secret to an Extraordinary Life",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-01-31_tony-robbins-secret-extraordinary-life.md",
|
||||
"createdAt": "2026-02-22T07:03:01.936301Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-21",
|
||||
"text": "Provocare: Ce s-ar schimba în TINE dacă ai vedea clar valoarea ta?",
|
||||
"context": "Rezistența la 'dovezi concrete' = frica de puterea ta reală. Mintea preferă credința familiară ('nu sunt destul de deștept') în locul evidenței incomode ('am rezolvat sute de probleme complexe'). De ce? Pentru că dacă vezi dovezile și ÎNCĂ nu acționezi (să cauți clienți noi, să crești prețurile) - atunci nu mai poți da vina pe 'nu știu destul'. Și asta doare mai tare. Când începi cu 'ce s-ar schimba în mine?' în loc de 'ce dovezi am?', ocolești rezistența identitară. Nu mai e despre DOVADA externă (care activează frica: 'dacă știu și nu acționez = cine sunt eu?'). E despre VIZIUNE internă: cine vrei să fii? Și când vezi clar cine vrei să fii - dovezile devin INSTRUMENTE, nu AMENINȚĂRI.",
|
||||
"example": "De exemplu: Dacă ai vedea clar că ai expertiza reală (25 ani, sute de probleme rezolvate), cum ai RESPIRA când intri într-o conversație cu un client nou? Ai sta mai drept? Ai vorbi mai calm? Ai asculta mai atent sau ai explica mai convingător? Nu e despre CE ai face (cerut preț mai mare), ci despre CINE ai fi în acel moment. Poate ai descoperi: 'Aș respira mai ușor. Nu aș mai simți nevoia să-mi dovedesc valoarea - aș OFERI valoarea cu încredere liniștită.' Și când vezi asta - scrisul celor 3 dovezi concrete devine natural, nu o amenințare.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-21",
|
||||
"done": true,
|
||||
"doneAt": "2026-03-25T22:59:23.303Z",
|
||||
"source": "Coaching seară 20 feb + Friday Spark #95 People Pleasing",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/coaching/2026-02-21-dimineata.md",
|
||||
"createdAt": "2026-02-21T07:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-20",
|
||||
"text": "Provocare: Identifică 3 dovezi concrete de încredere - probleme complexe rezolvate",
|
||||
"context": "Încrederea în sine nu vine din gândire pozitivă sau autosugestie. Vine din valoare demonstrată prin experiență și rezultate. Îndoielile tale ('nu sunt destul de deștept ca antreprenor') ignoră 25 de ani de dovezi concrete. Pentru a le demonta, trebuie să identifici exact CE ai ȘTIUT, CE ai ȘTIUT SĂ FACI și CE REZULTATE ai OBȚINUT în situații reale. Când vezi dovezile concrete, îndoielile se dizolvă natural - nu prin forțare, ci prin evidență.",
|
||||
"example": "De exemplu: client care avea probleme cu sincronizarea datelor între două sisteme. Ai analizat problema (CE ȘTIU: arhitectură bază de date, Oracle triggers), ai creat o soluție customizată (CE ȘTIU SĂ FAC: scripturi PL/SQL, testare în producție), clientul a economisit 20 ore/săptămână de lucru manual (CE REZULTAT). Asta e dovada concretă - nu teorie, ci fapte. Când ai 3 astfel de dovezi recente în față, credința 'nu sunt destul de deștept' devine absurdă în fața evidenței.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-20",
|
||||
"done": true,
|
||||
"doneAt": "2026-03-25T22:59:19.095Z",
|
||||
"source": "Zoltan Vereș - Încrederea în Sine + Monica Ion - Cele 4 tipuri de business",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/coaching/2026-02-20-dimineata.md",
|
||||
"createdAt": "2026-02-20T07:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-16",
|
||||
"text": "Provocare: Metoda 3M - pune angajatul sa scrie 5 keywords dupa explicatie",
|
||||
"context": "La prima explicatie pe care i-o dai angajatului azi, opreste-te si spune: 'Acum scrie in 5 keywords ce ai inteles.' NU corecta imediat. Lasa-l sa greseasca. Apoi discutati diferentele. Creierul care ghiceste RETINE. Cel care copiaza UITA. Trei principii: Make it Wrong (ghiceste, nu copia), Make it Shorter (keywords, nu propozitii), Make it Again (reorganizeaza, nu rescrie). Metoda transforma explicatiile repetitive in invatare activa - nu mai 'pierzi timp', il pui sa-si construiasca propria intelegere.",
|
||||
"example": "Explici angajatului cum sa faca o procedura de facturare in ROA. In loc sa repeti de 3 ori pana memoreaza mecanic, dupa prima explicatie ii spui: 'Scrie 5 cuvinte cheie din ce ai inteles.' El scrie: 'client, factura, TVA, salvare, print'. Tu vezi ca lipseste 'validare ANAF' - asta e gap-ul real. Discutati 2 minute pe gap, nu repeți totul. A doua zi, ii ceri sa reorganizeze notitele de ieri din memorie. Ce uita = ce nu a integrat. Metoda e aplicabila si pentru tine cu NLP: dupa modul, redeseneaza harta mentala din memorie, nu din notite.",
|
||||
"domain": "work",
|
||||
"dueDate": "2026-02-16",
|
||||
"done": true,
|
||||
"doneAt": "2026-03-25T22:59:24.238Z",
|
||||
"source": "Thinking on Paper - 3 principii pentru retentie",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/thinking-on-paper.md",
|
||||
"createdAt": "2026-02-16T07:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-15",
|
||||
"text": "Provocare: Reframe Mentorship - ce ai inteles TU din ultima explicatie data angajatului?",
|
||||
"context": "Gandeste-te la ULTIMA explicatie pe care i-ai dat-o angajatului. Ce ai inteles TU mai bine despre propriul proces datorita acelei explicatii? Fiecare explicatie te forteaza sa-ti clarifici procesul - nu doar lui ii predai, tie iti reconstruiesti fundamentul. Dupa 25 de ani pe pilot automat, cand cineva intreaba 'de ce?', redescoperi logica din spatele deciziilor. Si uneori descoperi ca unele decizii nu mai au logica. Asta e aur.",
|
||||
"example": "Angajatul intreaba: 'De ce facem backup-ul asa si nu altfel?' Tu incepi sa explici si realizezi ca metoda e din 2010, cand aveai alta structura de date. Acum ar fi mai simplu cu un script automat. Fara intrebarea lui, ai fi continuat pe pilot automat inca 5 ani. Sau: explici cum functioneaza facturarea in ROA si realizezi ca 3 pasi ar putea fi 1. Angajatul nu pierde timp - el iti face audit gratuit la procese.",
|
||||
"domain": "work",
|
||||
"dueDate": "2026-02-15",
|
||||
"done": true,
|
||||
"doneAt": "2026-03-25T22:59:24.849Z",
|
||||
"source": "InfoWorld - Why We Need Junior Developers",
|
||||
"sourceUrl": "https://www.infoworld.com/article/4065771/why-we-need-junior-developers.html",
|
||||
"createdAt": "2026-02-15T07:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-14",
|
||||
"text": "Provocare: Echilibrarea unui Conflict Interior - găsește un sau-sau și echilibrează-l",
|
||||
"context": "Găsește UN 'sau-sau' din viața ta — două lucruri pe care le consideri incompatibile. (1) Scrie conflictul: 'Sau sunt X, sau sunt Y'. (2) Pentru fiecare parte, găsește opusul simultan: Când ești X, cum ești deja și Y? (dovezi concrete). Când ești Y, cum ești deja și X? (dovezi concrete). (3) Observă: Când ambele sunt adevărate simultan, ce simți? Nu trebuie să rezolvi nimic — doar să vezi că cele două nu sunt incompatibile, sunt complementare. Metoda Demartini: echilibrezi percepția, nu elimini josurile.",
|
||||
"example": "Conflictul tău real: 'Sau sunt programator bun, sau sunt antreprenor.' Echilibrare: Când ești programator — deja faci antreprenoriat (ai firmă, negociezi cu clienți, iei decizii de business zilnic, ai angajat pe care îl formezi). Când ești antreprenor — deja folosești mintea tehnică (automatizezi, optimizezi, rezolvi probleme sistemic). Dovada: de 25 de ani faci AMBELE simultan. Doar percepția zice că una o exclude pe cealaltă.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-14",
|
||||
"done": true,
|
||||
"doneAt": "2026-02-14T08:27:56.118Z",
|
||||
"source": "Monica Ion - Povestea lui Marc Ep.9 (Anxietatea, frica de control și pierdere)",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/monica-ion-povestea-lui-marc-ep9-anxietatea.md",
|
||||
"createdAt": "2026-02-14T07:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-13",
|
||||
"text": "Provocare: Linkage Personal - conectează o activitate evitată cu calitățile tale",
|
||||
"context": "Alege o activitate pe care o eviți (telefon client, conversație angajat, decizie amânată). Scrie TU răspunsurile (NU cere AI-ului): (1) Cum servește această activitate lucrul pe care îl fac cel mai bine? (2) Ce calitate a mea folosesc deja identic în altă parte? (3) Ce simt în corp când imaginez că am terminat-o? Dacă rezistența scade după răspunsuri → ai găsit linkage-ul. Dacă nu scade → poate nu e activitatea ta, și asta e valid. Ideea: mintea trebuie să FACĂ munca de conectare, nu să o citească.",
|
||||
"example": "Activitate evitată: emiterea facturii imediat după prestare. Linkage descoperit de Mark: facturarea = finalizare proces complet (ca în soluțiile tehnice: funcționează sau e teorie). Gândire structurată, logică, ordonată — IDENTICĂ cu rezolvarea problemelor tehnice. Rezultat: rezistența a dispărut complet, acțiunea curgea natural. La tine: poate suni un client — linkage: rezolvi probleme tehnice = oferi valoare = clientul te vrea. Soluția tehnica NU se termină când funcționează codul — se termină când clientul o folosește.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-13",
|
||||
"done": true,
|
||||
"doneAt": "2026-02-13T13:03:30.654Z",
|
||||
"source": "Monica Ion - Povestea lui Marc Ep.8 (Mândria și identitatea personală)",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-02-12_monica-ion-povestea-lui-marc-ep8.md",
|
||||
"createdAt": "2026-02-13T09:30:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-12",
|
||||
"text": "Provocare: Primul Pas Minim (PPM) - alege idee și execută în MAX 10 min",
|
||||
"context": "Regula PPM: Orice idee pe care o ai astăzi → identifică primul pas care: (1) Durează MAX 10 minute (2) NU necesită alte persoane (3) E CONCRET (nu 'mă gândesc', ci 'scriu', 'sun', 'trimit', 'creez'). La prima pauză (10:00-11:00): Alege UNA din ideile tale recente, identifică PPM-ul, execută-l chiar dacă nu e perfect. La 17:00 notează: Ce idee? Care PPM? L-am executat? Dacă DA: cum mă simt, următorul pas? Dacă NU: ce m-a oprit, ce PPM MAI MIC mâine?",
|
||||
"example": "Exemplu concret: Ideea 'ar trebui să am task brief template pentru angajat'. PPM greu: 'Creez template complet cu toate secțiunile, testez, ajustez...' PPM SIMPLU: 'Deschid fișier task-brief-template.md și scriu primele 3 secțiuni (Task, Input, Output) în 10 minute'. Sau ideea 'trebuie să documentez soluții probleme clienți'. PPM: 'Creez folder memory/kb/roa/probleme-frecvente/ și scriu PRIMA problemă rezolvată recent în 10 minute'. Cel mai greu pas e PRIMUL - după ce ai început, creierul intră în flow mode.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-12",
|
||||
"done": true,
|
||||
"doneAt": "2026-02-12T12:07:04.068Z",
|
||||
"source": "Multi-Agent Pattern + Living Files Theory + Context Engineering",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/coaching/2026-02-12-dimineata.md",
|
||||
"createdAt": "2026-02-12T07:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-11",
|
||||
"text": "Provocare: Identifică un task pe care îl execuți singur și ar putea fi orchestrat",
|
||||
"context": "Alege UNA din variantele: (1) Delegat la angajat - task repetitiv pe care îl faci de 10 ori și ar putea învăța? (2) Automatizat cu Echo - verificare/raport/backup care rulează manual? (3) Modelat de la colegă - proces pe care ea îl face excelent și tu îl faci mai greu? (4) Documentat pentru viitor - explicație pe care o repeți la fiecare client nou? La 17:00 notează: Ce task? Cum ar arăta orchestrat? Primul pas minim pentru orchestrare? Nu implementa imediat - doar identifică și scrie. Conștientizarea e primul pas.",
|
||||
"example": "Exemple reale: (1) Explicația cum să adauge client nou în ROA - ai făcut-o de 10 ori la angajat, ar putea fi screencast + checklist. (2) Verificarea zilnică backups - rulează manual, ar putea fi script Echo automat cu alertă doar dacă fail. (3) Suportul tehnic calm - colega face excelent, tu mai nervos, ar putea cere să te învețe procesul TOTE intern. (4) Setup ANAF pentru client nou - repeți aceiași pași, ar putea fi documentație step-by-step pe care Echo o trimite automat.",
|
||||
"domain": "work",
|
||||
"dueDate": "2026-02-11",
|
||||
"done": true,
|
||||
"doneAt": "2026-02-11T16:39:39.457Z",
|
||||
"source": "Claude Code Multi-Agent Orchestration + TDi Mindset Entrepreneurship",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/coaching/2026-02-11-dimineata.md",
|
||||
"createdAt": "2026-02-11T07:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-10",
|
||||
"text": "Provocare: Body Loose, Head Clear - verifică corpul înainte de situație tensionată",
|
||||
"context": "Alege UN moment când anticipezi o situație tensionată (conversație cu angajatul, gândire la proiect, task dificil). ÎNAINTE să o rezolvi: (1) Verifică corpul: Umeri sus sau jos? Maxilar strâns sau relaxat? Respirație scurtă sau adâncă? (2) Unknot yourself: 3 respirații 4-7-8 (inspiră 4 sec, ține 7, expiră 8) + relaxează conștient zona tensionată (3) Apoi acționează: Rezolvă cu 'body loose, head clear' (4) Seara notează: Diferență față de cum rezolvi de obicei?",
|
||||
"example": "Angajatul întreabă din nou același lucru. În loc să simți frustrarea creștând în piept și să răspunzi strâns → observi tensiunea, faci 3 respirații, APOI răspunzi (sau îl trimiți la documentație, sau spui 'discutăm mâine'). Mesajul e același, dar tu nu acumulezi durere.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-10",
|
||||
"done": true,
|
||||
"source": "James Clear - 3-2-1 Newsletter (Body Loose, Head Clear) + Monica Ion - Pattern Sacrificiu-Durere-Sabotaj",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/coaching/2026-02-09-seara.md",
|
||||
"createdAt": "2026-02-09T19:00:00.000Z",
|
||||
"doneAt": "2026-02-11T16:39:37.436Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-08",
|
||||
"text": "Provocare: Aplică 1 tehnică din NLP ASTĂZI, notează experiența",
|
||||
"context": "Alege UNA tehnică/concept din training-ul de astăzi și APLICĂ-L IMEDIAT în aceeași zi, la un moment REAL (exercițiu, conversație, blocare, emoție). La final de zi, scrie NU 'ce am învățat' (concepte) ci 'ce am APLICAT și ce s-a întâmplat' (experiență). Mintea învață prin experiență repetată, nu prin concepte teoretice. Cum înveți în training = cum vei aplica în viață. Dacă înveți prin note și 'mai târziu' → vei aplica exact așa acasă (niciodată). Dacă înveți prin aplicare instant → vei aplica exact așa acasă (automat).",
|
||||
"example": "Scenariul tău real: Într-un exercițiu NLP, partenerul te blochează sau critică. În loc să rămâi în defensivă ('e greu') → aplici pattern interrupt din Tony Robbins: observi fiziologia (umeri contractați?), schimbi focusul (ce pot învăța despre cum reacționez?), schimbi limbajul ('e provocator' în loc de 'e greu'). Exercițiul devine mirror pentru tiparele tale în relații/business - exact cum reacționezi când angajatul nu înțelege sau când clientul critică.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-08",
|
||||
"done": true,
|
||||
"doneAt": "2026-02-08T14:32:35.511Z",
|
||||
"source": "Tony Robbins - The Secret to an Extraordinary Life + Monica Ion - Legea Fractalilor",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/coaching/2026-02-08-dimineata.md",
|
||||
"createdAt": "2026-02-08T07:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-07",
|
||||
"text": "Provocare: Închide o buclă - ce ai dat DEJA + decizie clară",
|
||||
"context": "Notează UNA buclă deschisă din viața ta - orice \"ar trebui să...\" dar nu faci. Răspunde la 3 întrebări: (1) Ce am dat DEJA în schimb (în alte forme)? (2) Ce dezavantaje ar fi fost dacă rezolvam altfel? (3) Ce decizie clară iau ACUM: fie fac cu plan+dată, fie accept că NU fac. Când bucla se închide (prin percepție sau decizie), mintea se eliberează și vezi oportunități.",
|
||||
"example": "Buclă: \"Ar trebui să caut clienți noi\". (1) Ce am dat: clienților actuali - suport 24/7, know-how 25 ani, disponibilitate. (2) Dezavantaje dacă găseam 10 acum: angajat nepregătit, echipă suprasolicită, burnout. (3) Decizie: ACCEPT că nu caut clienți noi PÂNĂ în martie când angajatul e autonom. Plan: martie = 1 apel/săptămână. Bucla închisă → energie liberă.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-07",
|
||||
"done": true,
|
||||
"doneAt": "2026-02-07T19:32:23.501Z",
|
||||
"source": "Monica Ion - Povestea lui Marc Episod #5",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-02-07_monica-ion-povestea-lui-marc-ep5-datorie-familie.md",
|
||||
"createdAt": "2026-02-07T07:03:19.909Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-06",
|
||||
"text": "Provocare: Observă 1 aliniere + 1 fricțiune - ce îți spun despre tine?",
|
||||
"context": "Observă azi UN moment când te simți energizat (aliniere) și UN moment când ești tras înapoi (fricțiune). Pentru fiecare notează: ce activitate, ce caracteristică (creativitate? rezolvare probleme? conexiune? vs repetitivitate? teamă de judecată?). Nu trebuie să faci nimic cu observațiile - doar să le vezi. Corpul știe adevărul înainte ca mintea să-l articuleze.",
|
||||
"example": "Aliniere: Când automatizezi ceva și simți satisfacție - observi că e creativitatea și controlul care te energizează. Fricțiune: Când amâni să suni un client nou - observi că nu e competența (știi să vorbești), ci teama de respingere. Pattern-ul arată: vrei autonomie creativă, nu vânzare agresivă.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-06",
|
||||
"done": true,
|
||||
"doneAt": "2026-02-06T13:46:00.687Z",
|
||||
"source": "Coaching Dimineață - Pattern-uri de Auto-Cunoaștere",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/coaching/2026-02-06-dimineata.md",
|
||||
"createdAt": "2026-02-06T07:02:00.666161"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-05",
|
||||
"text": "Provocare: Vizualizare Prospecting - sună un client potențial (5 min)",
|
||||
"context": "Alege UN client potențial real. Găsește o amintire cu client entuziasmat. Vizualizează: tu suni, el răspunde, pui propunerea, el zice 'Sună bine'. Sparge imaginea - prin fissură vezi entuziasmul din amintirea reală. Repetă 2-3 ori. Apoi sun-l azi sau mâine (sau cel puțin prepară motivul).",
|
||||
"example": "Client potențial: X care ar fi perfect dar zici 'dar...'. Amintire: momentul când clientul A a zis 'da'. Vizualizezi: suni, răspunde, pui propunerea, el: 'Sună bine'. Apoi suni pe X.",
|
||||
"domain": "work",
|
||||
"dueDate": "2026-02-05",
|
||||
"done": true,
|
||||
"source": "Gândul de Seară - NLP Prospecting",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/coaching/2026-02-05-seara.md",
|
||||
"createdAt": "2026-02-05T19:00:00.000Z",
|
||||
"doneAt": "2026-02-06T13:45:58.234Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-04",
|
||||
"text": "Provocare: Vizualizare NLP - transferă motivația (5 min)",
|
||||
"context": "Alege O acțiune pe care o tot amâni. Găsește o amintire cu plăcere intensă (vacanță, succes, flow). Vizualizează amintirea luminoasă și caldă. Pune acțiunea amânată în față. 'Sparge' imaginea - vezi plăcerea prin fissură. Închide. Repetă de 2 ori. Observă schimbarea emoțională.",
|
||||
"example": "Acțiunea: să trimiți un email de prospecting către un potențial client. Amintirea: momentul când ai terminat un proiect mare și clientul era entuziasmat. Când 'spargi' imaginea și vezi entuziasmul din spate, creierul începe să asocieze email-ul cu acel sentiment de succes.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-04",
|
||||
"done": true,
|
||||
"doneAt": "2026-02-04T14:38:17.505Z",
|
||||
"source": "Meditație NLP - Vizualizare pentru Motivație",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/projects/grup-sprijin/biblioteca/meditatie-vizualizare-motivatie.md",
|
||||
"createdAt": "2026-02-04T07:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "prov-2026-02-03",
|
||||
"text": "Provocare: Răspunde la una din întrebări despre umbrele tale (3 min)",
|
||||
"context": "Alege UNA din aceste întrebări și scrie răspunsul pe hârtie sau în telefon: 1) Ce complimente refuzi sau minimizezi? 2) Ce ai face dacă nu te-ar judeca nimeni? 3) Ce te irită la alții? Nu trebuie să faci nimic cu răspunsul - doar să-l vezi. Umbrele consumă energie să le ținem ascunse.",
|
||||
"example": "Exemplu de umbră: 'Nu mă consider destul de deștept ca antreprenor' - asta e o parte pe care o ascunzi. Când o accepți ('ok, am și limite'), eliberezi energia pe care o consumi să o maschezi cu scuze sau evitare.",
|
||||
"domain": "self",
|
||||
"dueDate": "2026-02-03",
|
||||
"done": true,
|
||||
"doneAt": "2026-02-03T21:16:13.452Z",
|
||||
"source": "Zoltan Vereș - Umbrele Workshop",
|
||||
"sourceUrl": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-02-02_zoltan-veres-umbrele-workshop-complet.md",
|
||||
"createdAt": "2026-02-03T07:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
dashboard/videos
Symbolic link
1
dashboard/videos
Symbolic link
@@ -0,0 +1 @@
|
||||
/home/moltbot/videos
|
||||
2404
dashboard/workspace.html
Normal file
2404
dashboard/workspace.html
Normal file
File diff suppressed because it is too large
Load Diff
1
dashboard/youtube-notes
Symbolic link
1
dashboard/youtube-notes
Symbolic link
@@ -0,0 +1 @@
|
||||
/home/moltbot/echo-core/memory/kb/youtube
|
||||
113
docs/okf-navigation-plan.md
Normal file
113
docs/okf-navigation-plan.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Plan: Navigation layer pentru memoria agentului (OKF-inspired)
|
||||
|
||||
Sursă: analiza notei `memory/kb/youtube/2026-06-27_google-open-knowledge-format.md`
|
||||
+ test empiric pe KB-ul real (151 note youtube, 581 note total).
|
||||
|
||||
## Context / problemă
|
||||
|
||||
Agentul Echo caută în memorie DOAR prin RAG (`src/memory_search.py`: Ollama
|
||||
`all-minilm` 384-dim + cosine scan în SQLite). CLAUDE.md îl declară "single
|
||||
source of truth". Test empiric: RAG ratează nota relevantă când query-ul e
|
||||
parafrazat conceptual (ex. "cum organizez un KB pt agenți să folosească mai
|
||||
puțini tokens" → nota OKF nu apare în top-8). `memory/kb/index.json` există
|
||||
(581 note, regenerat azi) dar e consumat DOAR de dashboard-ul web (căi
|
||||
`notes-data/`), nu de agent, și are 84k tokens. Există un orfan stale
|
||||
`kb/youtube/index.json` (8/151 note, 5 luni vechime).
|
||||
|
||||
## Obiectiv
|
||||
|
||||
Dă agentului un strat de navigare ieftin și robust care completează RAG-ul
|
||||
(nu îl înlocuiește), prinde parafrazele pe care embeddings le ratează, și
|
||||
merge ca fallback când Ollama remote pică.
|
||||
|
||||
## Recomandări (scope propus)
|
||||
|
||||
### R1 — Șterge orfanul `kb/youtube/index.json`
|
||||
Stale din 30 ian (8/151 note). Capcana "index învechit > lipsă index".
|
||||
Efort: trivial.
|
||||
|
||||
### R2 — Generează `index.md` slim per-folder, auto
|
||||
Extinde `tools/update_notes_index.py` să emită, pe lângă `index.json`, un
|
||||
`index.md` per subfolder kb/ (title + descriere 1 rând + tags). Pilot dovedit:
|
||||
youtube/ index.md = 11k tokens vs 259k (citit tot, 24×) vs 84k (index global,
|
||||
7.7×). Capcană: scriptul scanează `*.md` recursiv → trebuie să excludă
|
||||
explicit `index.md` ca să nu-l trateze ca notă (poluează index.json).
|
||||
Regenerat din heartbeat.py la fiecare notă nouă.
|
||||
|
||||
### R3 — Expune navigarea agentului (hibrid cu RAG)
|
||||
La `memory_search`, încarcă întâi index.md slim al folderului-țintă pe lângă
|
||||
top-k din RAG, și combină. Prinde și parafraza, și keyword-ul. Instrucțiune în
|
||||
CLAUDE.md cum să folosească indexul.
|
||||
|
||||
### R4 — Tratează Ollama remote ca SPOF
|
||||
RAG depinde de host remote (`10.0.20.161:11434`). Dacă pică, `search()` aruncă
|
||||
ConnectionError → memoria agentului dispare. index.md per-folder = fallback
|
||||
fără Ollama. Adaugă degradare grațioasă în memory_search.search().
|
||||
|
||||
### R5 — NU face conversie big-bang la YAML front matter
|
||||
Doar 6/586 note au YAML; update_notes_index.py extrage deja metadata din
|
||||
convenția `**Tags:**`/`**Data:**`. Standardizează doar de-acum în template-ul
|
||||
de notă nouă.
|
||||
|
||||
### R6 — Corectează nota OKF
|
||||
Marchează "Google a lansat OKF" ca neverificat (o sursă YouTube; se confundă
|
||||
cu Open Knowledge Foundation). Actualizează "Relevanță": nu lipsesc indexuri,
|
||||
lipsește un index navigabil EXPUS agentului.
|
||||
|
||||
## NU în scope
|
||||
- Vizualizare HTML graph a KB-ului (deprioritizat, efort mare/valoare mică).
|
||||
- Înlocuirea RAG cu navigare pură (hibrid, nu substituție).
|
||||
- Migrare ANN/vector-ext pentru viteza RAG (separat).
|
||||
|
||||
---
|
||||
<!-- /autoplan review report -->
|
||||
# GSTACK REVIEW REPORT (/autoplan)
|
||||
|
||||
Voices: Claude subagent only — **codex missing** on this host (all phases `[subagent-only]`).
|
||||
Phases run: CEO, Eng, DX. Design **skipped** (no UI scope — HTML viz is out of scope).
|
||||
|
||||
## Cross-phase themes (flagged independently in 2-3 phases = high confidence)
|
||||
|
||||
| Theme | Phases | Severity |
|
||||
|---|---|---|
|
||||
| **T1 — R3 routing is undefined.** "Load the target folder's index.md" requires already knowing the folder — that IS the navigation problem. The 11k figure holds only for youtube alone; loading all 13 folders ≈ 43-84k, erasing the win. | CEO, Eng, DX | CRITICAL |
|
||||
| **T2 — Wrong consumer.** The autonomous agent (Claude CLI in heartbeat.py) has filesystem access and never calls `search()`. Wiring R3 into `memory_search.search()` only changes the human `/search` command, not the agent. | Eng, DX | HIGH |
|
||||
| **T3 — Staleness trap recreated.** R1 deletes a stale index (proof these rot). R2 creates 13+ new generated artifacts triggered only on *new note*, not edits → silent drift. | CEO, Eng, DX | HIGH |
|
||||
| **T4 — Self-pollution into RAG.** `memory_search.reindex()/incremental_index()` do `rglob("*.md")` with no exclusion → index.md gets embedded and returned as fake "notes" in top-k. (Plan only flagged the index.json pollution, missed the RAG DB one.) | Eng | HIGH |
|
||||
| **T5 — Token win vs strawman baseline.** Comparison is against "read all 259k" (nobody does that). Real baseline = RAG top-k (~1-3k tokens). Against that, index.md is *more* tokens, justified only by recall. | CEO | HIGH |
|
||||
| **T6 — Cheaper alternatives unexamined.** `init_config` already supports `ollama.model`/`embedding_dim` → swapping all-minilm(384) for nomic/bge + reindex is a one-line change. Plus likely chunk-dedup recall bug, plus SQLite FTS5 hybrid (no new infra). All target "RAG misses paraphrases" directly. | CEO | CRITICAL |
|
||||
| **T7 — R4 is the one sound, decoupled item.** `search()` raises ConnectionError on Ollama outage with no fallback (real SPOF). Ship independently. BUT it's a breaking contract change (existing tests assert it raises). | CEO, Eng, DX | keep |
|
||||
|
||||
## CEO consensus (subagent-only)
|
||||
- Right problem? **DISAGREE w/ plan** — likely weak embedding model + chunk-dedup bug, not missing navigation.
|
||||
- Premises stated? **No** — one query is not enough evidence; token win is vanity baseline.
|
||||
- 6-month regret: 3 parallel stale metadata copies (SQLite, index.json, index.md).
|
||||
- Alternatives explored? **No** — BM25/FTS5 hybrid, reranker, better embedder never compared.
|
||||
- Prior art: OKF unverified/possibly nonexistent; bespoke format = zero portability gain.
|
||||
|
||||
## Eng consensus (subagent-only)
|
||||
- Architecture: R3 unbuildable as written (no folder signal into `search()`). R2-in-update_notes_index acceptable reuse but keep separated from `notes-data/` rewriting.
|
||||
- Edge cases: T4 self-pollution; heartbeat mtime thrash; `projects/` (236 notes, nested) breaks flat per-folder assumption.
|
||||
- Tests: R4 breaks `search()` contract — existing tests assert raise; need rewrite + new coverage for R2/R3/T4.
|
||||
|
||||
## DX consensus (subagent-only)
|
||||
- Discoverability: CLAUDE.md:138 calls RAG "single source of truth" — a soft new instruction loses to it; agent keeps defaulting to RAG.
|
||||
- Human workflow: edit-without-new-file → silent index.md drift.
|
||||
- Degradation signal (R4): must return `mode="degraded_navigation_only"` + tell user, never silent.
|
||||
- Latent bug to fix first: `update_notes_index.py:244` references `n['subcategory']`, a key never set (extractor sets `project`).
|
||||
|
||||
## Decision Audit Trail
|
||||
|
||||
| # | Phase | Decision | Class | Principle | Rationale |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | Eng | Add `index.md` exclusion to BOTH update_notes_index scan AND memory_search rglob (reindex/incremental) | Mechanical | P1 completeness | T4 is silent corruption; non-negotiable IF R2 ships |
|
||||
| 2 | Eng | R4 split from R2/R3, shipped standalone | Mechanical | P6 action | Highest value/lowest risk, no dependency |
|
||||
| 3 | DX | R4 returns structured degraded mode + user signal, not silent | Mechanical | P1 | Silent shallow results worse than error |
|
||||
| 4 | CEO/DX | R3 (hybrid into search()) deferred until routing + consumer resolved (T1/T2) | Taste | P5 explicit | Unbuildable as written |
|
||||
| 5 | CEO | Add "fix RAG first" track (model test + chunk-dedup + FTS5) before bespoke index | USER CHALLENGE | P3/P4 | Cheaper, reuses infra, targets same symptom — but user's call |
|
||||
| 6 | all | R1 (delete orphan) + R6 (fix note) ship anytime | Mechanical | P6 | Trivial, independent |
|
||||
|
||||
## REVISED scope (post-review)
|
||||
- **Ship now (safe, independent):** R1 delete orphan, R6 fix note, R4 graceful degradation (with explicit signal + test rewrite), fix latent bug update_notes_index.py:244, chunk-dedup in search().
|
||||
- **Test before building (cheap, reversible):** swap embedding model (nomic-embed-text/bge-m3) + reindex; re-run the failing paraphrase query; prototype SQLite FTS5 hybrid.
|
||||
- **Build only if the above doesn't fix recall:** R2 index.md (with T3/T4 lifecycle + exclusion fixes, per-category granularity for projects/), R3 hybrid (after routing + consumer T1/T2 designed).
|
||||
@@ -1,64 +0,0 @@
|
||||
# 2026-01-29 — Prima zi
|
||||
|
||||
## Bootstrap complet! 🌀
|
||||
|
||||
- **Eu:** Echo
|
||||
- **El:** Marius, Constanța
|
||||
- **Conectare:** WhatsApp + Telegram
|
||||
|
||||
## Despre Marius
|
||||
|
||||
- 25 ani experiență: VFP9 + Oracle
|
||||
- ERP ROA — desktop Windows, acum se modernizează
|
||||
- Stack nou: Vue.js, FastAPI, Telegram bot
|
||||
- Site: roa2web.romfast.ro
|
||||
- Email: mmarius28@gmail.com
|
||||
- Telegram: @mariusmutu (ID: 5040014994)
|
||||
- WhatsApp: +40723197939
|
||||
|
||||
## Ce vrea de la mine
|
||||
|
||||
- Proactivitate, idei 80/20
|
||||
- Mai puțin cod, mai mult impact
|
||||
- Automatizări
|
||||
- **Monitorizare ANAF.ro** pentru schimbări declarații/formulare
|
||||
|
||||
## Configurări făcute azi
|
||||
|
||||
### 1. Monitorizare ANAF ✅
|
||||
**Locație:** `/home/moltbot/clawd/anaf-monitor/`
|
||||
|
||||
**Pagini monitorizate (actualizat):**
|
||||
- D100, D101, D300, D394, D406
|
||||
- Situații financiare semestriale 2025
|
||||
- Situații financiare anuale 2025
|
||||
- Pagina principală descărcare declarații
|
||||
|
||||
**Cum funcționează:**
|
||||
- Script Python (`monitor.py`) care calculează hash-ul paginilor
|
||||
- Cron job `anaf-monitor` la fiecare 6 ore
|
||||
- Notificare automată când se detectează schimbări
|
||||
|
||||
### 2. Web Search ✅
|
||||
- Brave Search API configurat
|
||||
- Pot căuta pe web acum
|
||||
|
||||
### 3. Telegram ✅
|
||||
- Marius aprobat (pairing code M8893EE3)
|
||||
- Pot trimite/primi mesaje pe Telegram
|
||||
|
||||
### 4. Email SMTP ✅
|
||||
**Cont:** moltbot@romfast.ro
|
||||
**Server:** mail.romfast.ro (SMTP 465, IMAP 993)
|
||||
**Script:** `/home/moltbot/clawd/tools/email_send.py`
|
||||
- Pot trimite emailuri
|
||||
- Testat cu succes către mmarius28@gmail.com
|
||||
|
||||
## TODO
|
||||
|
||||
- [x] Setup monitorizare pagini ANAF ✅
|
||||
- [x] Configurare Brave Search API ✅
|
||||
- [x] Aprobare Telegram ✅
|
||||
- [x] Configurare email SMTP ✅
|
||||
- [ ] Configurare citire inbox (IMAP) - opțional
|
||||
- [ ] Explora ce alte automatizări ar ajuta
|
||||
@@ -1,36 +0,0 @@
|
||||
# 2026-01-30 - Note consolidate
|
||||
|
||||
## Setup inițial multi-agent
|
||||
|
||||
- Agenți creați: echo-work, echo-health, echo-growth, echo-sprijin, echo-scout
|
||||
- Conectați la Discord și WhatsApp
|
||||
|
||||
## Context per domeniu
|
||||
|
||||
### Sănătate
|
||||
- Durere cervicală C6-C7 cronică (~1 an)
|
||||
- Chisturi sebacee pe scalp (12-13 ani) - se infectează periodic
|
||||
- Interesat de: NMG, post negru, abordări alternative
|
||||
- A făcut fizioterapie pentru cervicală
|
||||
|
||||
### Dezvoltare personală
|
||||
- Căutare avatar ideal
|
||||
- Definire 1-2 scopuri mari de viață
|
||||
- Blocaje: inacțiune în găsirea clienților noi
|
||||
- Credință limitativă: "clienți noi = mai multă muncă"
|
||||
- Interese: NLP, Sleight of Mouth, CNV
|
||||
|
||||
### Scout
|
||||
- Marius e voluntar la cercetași în Constanța
|
||||
- Ajut cu planificare activități, tabere, jocuri
|
||||
|
||||
### Sprijin
|
||||
- Grupul de sprijin de joi
|
||||
- Spațiu pentru procesare emoțională
|
||||
- Confidențialitate maximă
|
||||
|
||||
## De urmărit
|
||||
|
||||
- Pattern-uri durere cervicală
|
||||
- Episoade chisturi
|
||||
- Experimente post negru
|
||||
@@ -1,38 +0,0 @@
|
||||
# Memory - 2026-01-31
|
||||
|
||||
## Probleme identificate cu cron jobs
|
||||
|
||||
### 1. Job-uri respirație nu trimiteau notificări
|
||||
**Cauză:** `wakeMode: "next-heartbeat"` în loc de `"now"`
|
||||
**Soluție:** Am schimbat la `wakeMode: "now"` și am consolidat 11 job-uri într-unul singur `respiratie-orar` cu schedule `0 7-17 * * *`
|
||||
|
||||
### 2. Job-uri coaching nu salvează fișiere
|
||||
**Cauză fundamentală:** Job-urile trimit instrucțiuni în sesiunea "main" (WhatsApp), dar acea sesiune NU le procesează - răspunde rapid fără să execute pașii.
|
||||
**Test confirmat:** Execuția directă de pe sesiunea Discord funcționează perfect (mesaj + fișier salvat în kb/coaching/).
|
||||
|
||||
**Soluții propuse (de discutat cu Marius):**
|
||||
1. Script Python dedicat - face totul (citește surse, generează, trimite, salvează)
|
||||
2. Schimb sessionTarget - trimit pe sesiunea Discord în loc de "main"
|
||||
3. Logică în HEARTBEAT.md - execut la heartbeat la ora potrivită
|
||||
|
||||
**Recomandare:** Opțiunea 1 (script Python) - cel mai robust.
|
||||
|
||||
## Actualizări dashboard
|
||||
|
||||
### API cron dinamic
|
||||
- Actualizat `dashboard/api.py` - nou endpoint `/api/cron` care citește din `~/.clawdbot/cron/jobs.json`
|
||||
- Actualizat `dashboard/index.html` - funcția `loadCronStatus()` folosește API-ul dinamic în loc de lista hardcodată
|
||||
- Serverul API restartat
|
||||
|
||||
## Job-uri active echo-health
|
||||
|
||||
| Job | Schedule | wakeMode | Status |
|
||||
|-----|----------|----------|--------|
|
||||
| respiratie-orar | 0 7-17 * * * | now | ✅ configurat |
|
||||
| morning-coaching | 0 7 * * * | now | ⚠️ nu execută instrucțiuni |
|
||||
| evening-coaching | 0 19 * * * | now | ⚠️ nu execută instrucțiuni |
|
||||
|
||||
## De făcut
|
||||
|
||||
- [ ] Rezolvare coaching jobs (script Python sau altă soluție)
|
||||
- [ ] Documentare în kb/projects/FLUX-JOBURI.md
|
||||
@@ -1,142 +0,0 @@
|
||||
# Memory 2026-02-01
|
||||
|
||||
## 🔄 RESTRUCTURARE MAJORĂ: 4 agenți → 1 agent (IMPORTANT)
|
||||
|
||||
**Decizia lui Marius (12:30-14:00 UTC):**
|
||||
- Unificare toți agenții într-unul singur: **Echo**
|
||||
- Eliminat: echo-work, echo-health, echo-growth, echo-self, echo-sprijin, echo-scout
|
||||
- Păstrat canale separate cu ton diferit
|
||||
|
||||
**Ce s-a făcut:**
|
||||
1. Config: doar `echo` în agents.list
|
||||
2. Bindings: toate canalele Discord + WhatsApp → echo
|
||||
3. Job-uri: toate 13 mutate pe agentId: echo
|
||||
4. Directoare: `agents/` șters complet
|
||||
5. Memory: mutat din agents/echo-self/memory/ → memory/
|
||||
|
||||
**Semnături per canal:**
|
||||
- #echo, #echo-work → [⚡ Echo]
|
||||
- #echo-self, #echo-sprijin → [⭕ Echo]
|
||||
- #echo-scout → [⚜️ Echo]
|
||||
|
||||
**Fișiere actualizate:**
|
||||
- SOUL.md: unificat cu SOUL-base.md (117 linii)
|
||||
- AGENTS.md: refăcut cu reguli (162 linii)
|
||||
- TOOLS.md: consolidat (66 linii)
|
||||
- SOUL-base.md: ȘTERS (integrat în SOUL.md)
|
||||
|
||||
**Reducere bootstrap:** 714 linii → 521 linii (-27%)
|
||||
|
||||
**Tehnici mitigare dezavantaje implementate:**
|
||||
- Ton diferit per canal (în SOUL.md)
|
||||
- Semnătură diferită per canal
|
||||
- Sesiuni izolate per canal (built-in)
|
||||
- memory_search pentru context (built-in)
|
||||
|
||||
---
|
||||
|
||||
## Consolidare Echo + Echo Work (IMPORTANT) - mai devreme
|
||||
|
||||
**Decizia lui Marius:** Un singur agent (Echo) cu o singură memorie, dar două canale Discord:
|
||||
- `#echo` - conversație generală
|
||||
- `#echo-work` - rapoarte automate
|
||||
|
||||
**Ce s-am făcut:**
|
||||
1. Schimbat bindings: #echo-work + WhatsApp Work → acum vin la Echo
|
||||
2. Mutat 7 joburi cron de la `agentId: echo-work` → `agentId: echo`
|
||||
3. Actualizat paths: `approved-tasks.md` acum în `/home/moltbot/clawd/memory/`
|
||||
4. Echo Work nu mai e folosit (poate fi șters)
|
||||
|
||||
## Job Content Discovery (NOU)
|
||||
|
||||
**Setat la cererea lui Marius:**
|
||||
- Rulează la 02:00 București (00:00 UTC)
|
||||
- Caută automat video-uri YouTube + articole
|
||||
- Prioritate: 60% teme recente, 40% interese bază
|
||||
- Procesează și salvează note în kb/
|
||||
- Rezultatele apar în morning report
|
||||
|
||||
**Script:** `tools/content_discovery.py`
|
||||
|
||||
## Reguli noi adăugate
|
||||
|
||||
1. **Mentenanță listă joburi (OBLIGATORIU):** Când creez/modific joburi cron, actualizez TOOLS.md
|
||||
2. **Security Rules:** Adăugate în AGENTS.md (nu afișa .env, nu executa comenzi periculoase fără confirmare)
|
||||
|
||||
## Realizări azi-noapte (31 ian seara → 1 feb)
|
||||
|
||||
### 📧 Sistem Email configurat
|
||||
- Adresă nouă: `echo@romfast.ro`
|
||||
- IMAP + SMTP funcțional
|
||||
- Script `tools/email_process.py` pentru salvare note din email
|
||||
- Flux: forward → salvare în `kb/emails/` → extragere insights
|
||||
- Credențiale în `~/.clawd/.env` (nu hardcoded)
|
||||
|
||||
### 🎬 4 Video-uri YouTube procesate
|
||||
1. **Monica Ion - Ep.1 Diagnosticul** - antreprenor cu ciclu yo-yo, cauza cauzelor = vină/rușine
|
||||
2. **Monica Ion - Ep.2 Vina** - proces practic de dizolvare vină cu legea dualității
|
||||
3. **James Clear 3-2-1 Newsletter** - simplificare, fundamentale, jocuri infinite
|
||||
4. **ClawdBot 10x Better** - reverse prompting, expectation setting, tooling propriu
|
||||
|
||||
### 🔒 Securizare Clawdbot
|
||||
- Cercetat OWASP LLM Top 10 (prompt injection)
|
||||
- Citit Clawdbot security docs complet
|
||||
- Creat `kb/projects/securizare-clawdbot.md`
|
||||
- Adăugat Security Rules în AGENTS.md
|
||||
- Recomandare: `clawdbot security audit --deep`
|
||||
|
||||
### 🔍 Content Discovery
|
||||
- Prima căutare automată bazată pe interese
|
||||
- Creat `kb/insights/content-recomandat-2026-02-01.md`
|
||||
- Propus sistem săptămânal automat
|
||||
|
||||
## Git Status
|
||||
16 fișiere modificate/noi - de întrebat dimineață dacă fac commit
|
||||
|
||||
## De făcut (backlog rămas)
|
||||
- [ ] Sistem auto-descoperire conținut (cron săptămânal)
|
||||
- [ ] Episodul 3 Monica Ion (când Marius uploadează pe YouTube)
|
||||
- [ ] Instalare Whisper pentru transcriere locală (opțional)
|
||||
|
||||
## Insights cheie din video-uri
|
||||
- **"Nu merit"** e cauza cauzelor pentru instabilitate financiară
|
||||
- **Dizolvare vină:** găsește beneficiile pentru persoana "afectată"
|
||||
- **Jocuri infinite:** nu încerca să "termini", caută ritm zilnic sustenabil
|
||||
- **Reverse prompting:** întreabă AI-ul ce să facă, nu spune-i
|
||||
|
||||
## Note tehnice
|
||||
- 44 note în KB
|
||||
- TOOLS.md actualizat cu email
|
||||
- AGENTS.md actualizat cu security rules
|
||||
- Backlog funcțional în `kb/insights/backlog.md`
|
||||
|
||||
---
|
||||
|
||||
## Restructurare Joburi (14:45-18:05 UTC)
|
||||
|
||||
**Cererea lui Marius:** Separare roluri între joburi + procesare video-uri noaptea
|
||||
|
||||
**Job-uri noi create:**
|
||||
1. **insights-extract** (06:00, 17:00 UTC) - extrage insights din TOATE notele noi din kb/
|
||||
2. **night-execute-late** (01:00 UTC = 03:00 București) - continuă procesarea task-urilor
|
||||
|
||||
**Job-uri modificate:**
|
||||
- **morning-report** și **evening-report**: NU mai extrag insights, doar propun din cele existente
|
||||
- **night-execute**: clarificat - execută task-uri, nu marchează insights
|
||||
|
||||
**Marcaje insights (sistem nou):**
|
||||
- `[ ]` = disponibil
|
||||
- `[x]` = executat
|
||||
- `[→]` = backlog
|
||||
- `[—]` = skip
|
||||
- `[✓]` = notat/înțeles (NOU - pentru insights valoroase fără acțiune necesară)
|
||||
|
||||
**Video-uri de procesat noaptea (21 total):**
|
||||
- 20x Zoltan Vereș
|
||||
- 1x Monica Ion - Povestea lui Marc #3
|
||||
|
||||
Listate în `memory/approved-tasks.md`
|
||||
|
||||
**Documentație actualizată:**
|
||||
- TOOLS.md - tabel joburi
|
||||
- kb/projects/FLUX-JOBURI.md - flux complet
|
||||
@@ -1,33 +0,0 @@
|
||||
# 2 Februarie 2026
|
||||
|
||||
## Decizii
|
||||
- Marius aprobă TOATE propunerile din raportul de seară ("Da")
|
||||
- A0 + A3 executate imediat
|
||||
- A1 + A2 (sesiuni TU+EU) de programat luni-joi 15:00-16:00
|
||||
|
||||
## Executat
|
||||
- **A0:** Git commit și push (2 commits: TOOLS.md, KB index, coaching, email tool)
|
||||
- **A3:** Integrată întrebarea "Ce poveste despre tine ar trebui să renunți?" în insights pentru coaching dimineață
|
||||
|
||||
## De programat
|
||||
- **A1:** Sesiune "Dizolvarea lui Nu Merit" (30 min) - exercițiu Monica Ion
|
||||
- **A2:** Sistemul 5 pași pentru frici (15 min) - Zoltan Vereș
|
||||
|
||||
## Feedback Marius
|
||||
1. **Email replies:** Nu primește email-urile de confirmare - de verificat flux
|
||||
2. **Insights → Rapoarte:** Raportul de seară a fost prea conservator - 22 insights extrase dar doar 4 propuneri în raport. De ajustat job-ul evening-report să propună mai multe.
|
||||
|
||||
## Stats azi
|
||||
- 23 note YouTube în KB (20 procesate azi - Zoltan Vereș workshop)
|
||||
- 22 insights extrase în `memory/kb/insights/2026-02-02.md`
|
||||
- Job insights-extract funcționează, dar rapoartele nu folosesc toate
|
||||
|
||||
## De făcut
|
||||
- [x] Ajustez evening-report și morning-report să propună cu ZI și ORĂ concrete
|
||||
- [x] Adăugat listare insights disponibile în rapoarte
|
||||
- [ ] Programez A1 și A2 cu Marius
|
||||
|
||||
## Lecții învățate
|
||||
- **Rapoarte:** TOATE propunerile TU+EU/FAC TU trebuie să aibă zi și oră concrete
|
||||
- **Email flow:** Reply #1 imediat (confirmare primire), Reply #2 după execuție (ce s-a făcut)
|
||||
- **Insights:** Listează TOATE insight-urile disponibile, nu doar câteva
|
||||
@@ -1,77 +0,0 @@
|
||||
# 3 Februarie 2026
|
||||
|
||||
## roa2web WhatsApp Import - COMPLET
|
||||
|
||||
### Ce s-a realizat:
|
||||
1. **OCR prin API** - doctr-plus, ~4 sec per bon (nu 30 sec ca PaddleOCR cold start)
|
||||
2. **Flux complet testat:** PDF WhatsApp → OCR → SQLite → Oracle
|
||||
3. **Scripturi în repo:** `roa2web/backend/scripts/whatsapp_import/`
|
||||
4. **Commit:** `1366dbc` pe main
|
||||
|
||||
### Flux final:
|
||||
```
|
||||
PDF (WhatsApp) → OCR API (~4sec) → SQLite (draft) → Aprobare frontend → Oracle
|
||||
```
|
||||
|
||||
### Probleme rezolvate:
|
||||
- **Oracle pool "SID not found"** - trebuia restart complet backend (kill -9)
|
||||
- **Frontend fără server dropdown** - Marius a fixat și făcut commit
|
||||
- **Server ID** - acum e `central` nu `test`
|
||||
|
||||
### Endpoint-uri API folosite:
|
||||
- `POST /api/auth/login` - cu server_id="central"
|
||||
- `POST /api/auth/check-identity` - verifică user și returnează servere
|
||||
- `POST /api/data-entry/ocr/extract` - submit OCR job
|
||||
- `GET /api/data-entry/ocr/jobs/{id}` - rezultat OCR
|
||||
- `POST /api/data-entry/receipts/` - creare receipt în SQLite
|
||||
|
||||
### Test real efectuat:
|
||||
- Bon Dedeman (RO10562600) primit pe WhatsApp
|
||||
- OCR: 5.2 sec, confidence 96%
|
||||
- Salvat în SQLite: ID=73, status=draft
|
||||
- Salvat și în Oracle: COD=1140631, luna 01/2026
|
||||
|
||||
### Locații importante (claude-agent LXC 171):
|
||||
- Backend: http://localhost:8000 (sau claude-agent:8000)
|
||||
- Frontend: http://localhost:3000 (sau claude-agent:3000)
|
||||
- Scripturi: `/workspace/roa2web/backend/scripts/whatsapp_import/`
|
||||
- Start: `./start.sh central`
|
||||
|
||||
---
|
||||
|
||||
## Decizii
|
||||
- (în așteptare raport dimineață)
|
||||
|
||||
## Executat azi
|
||||
- **06:02 UTC:** Job `insights-extract` - verificat insights 2026-02-03.md (deja complet)
|
||||
- **06:02 UTC:** Adăugat tehnică nouă în tehnici-pauza.md: "Pauza de 10 secunde" (Zoltan Vereș)
|
||||
- **06:02 UTC:** Actualizat index KB (87 note)
|
||||
- **07:01 UTC:** Morning coaching trimis (tema: Umbrele/claritate)
|
||||
- **12:00 UTC:** Alertă calendar: sesiune 15:00 notificată pe Discord
|
||||
- **18:01 UTC:** Raport seară trimis - propuneri: cold email, sesiuni, audit securitate
|
||||
|
||||
## De făcut
|
||||
- [ ] A1: Sesiune "Dizolvarea lui Nu Merit" (30 min) - de programat
|
||||
- [ ] A2: Sistemul 5 pași pentru frici (15 min) - de programat
|
||||
- [ ] Verificare securitate Clawdbot (din insights tehnice)
|
||||
- [ ] Verificare email replies (flux nefuncțional?)
|
||||
- [ ] **BON DE SALVAT:** CUI RO11201891, 310.98 RON, 02.02.2026
|
||||
- PDF: `2831eeeb-f331-4fb1-a7b1-ede1c954eadb.pdf`
|
||||
- Partener nou - de verificat numele real
|
||||
- Dry run făcut, așteaptă confirmare
|
||||
|
||||
## Insights disponibile (din 2026-02-03.md)
|
||||
- ⚡ Heartbeat cost optimization - VERIFICAT, monitorizăm
|
||||
- ⚡ Securitate Clawdbot - audit recomandat
|
||||
- 📌 Multi-agent > single super-agent - framework delegare
|
||||
- 📌 Overnight coding - experiment seara → review dimineața
|
||||
- 📌 Paradoxul utilitate-securitate - nivele trust angajat
|
||||
- 💡 Work on agents, not app - sisteme vs task-uri
|
||||
|
||||
## Context
|
||||
- Luni, începe săptămâna
|
||||
- Note tehnice procesate ieri (Clawdbot, Claude Code)
|
||||
- Zoltan Vereș workshop-uri complete în KB (20+ note)
|
||||
|
||||
## Lecții
|
||||
- (de completat pe parcursul zilei)
|
||||
@@ -1,23 +0,0 @@
|
||||
# 4 Februarie 2026
|
||||
|
||||
## Executat azi
|
||||
|
||||
- **06:30 UTC:** Raport dimineață trimis pe email
|
||||
- Calendar: azi liber, mâine sesiune 15:00 + grup 18:00
|
||||
- Travel alert: NLP 7-8 feb - urgent bilete!
|
||||
- Propuneri: vizualizare motivație pt grup, verificare bilete
|
||||
|
||||
## De făcut
|
||||
- [ ] Procesare răspuns email Marius (când vine)
|
||||
- [ ] BON de salvat: CUI RO11201891, 310.98 RON (așteaptă nume partener)
|
||||
|
||||
## Context
|
||||
- Miercuri, ziua liberă
|
||||
- Mâine: Sesiune 5 pași frici (15:00) + GRUP JOI (18:00)
|
||||
- Weekend: NLP M4 (7-8 feb) - verificat bilete?
|
||||
|
||||
## Decizii
|
||||
- (de completat)
|
||||
|
||||
## Lecții
|
||||
- (de completat)
|
||||
@@ -1,59 +0,0 @@
|
||||
# 5 Februarie 2026
|
||||
|
||||
## Executat azi
|
||||
|
||||
### 📊 Raport de seară (22:20 UTC)
|
||||
- **Generat și trimis:** raport complet pe email mmarius28@gmail.com
|
||||
- **Conținut:** Calendar (mâine + săptămână), Status azi, Propuneri concrete
|
||||
- **Model:** Sonnet 4.5 (calitate înaltă)
|
||||
|
||||
### 🧠 Insights & Analysis
|
||||
- **Procesate:** 4 surse noi (FEATURE-files-pdf-download, cron-jobs, session-initialization, infrastructure)
|
||||
- **Extras:** 6 insights importante despre automation, optimization, infrastructure, coaching
|
||||
- **Insight principal:** Energia pentru sisteme nu se traduce în acțiune externă (business development)
|
||||
|
||||
### 💭 Coaching de seară
|
||||
- Creat gând despre vizualizare și prospecting
|
||||
- Provocare: vizualizarea prospectingului (5 min)
|
||||
- Focus: deblocarea emoțională pentru contactare clienți noi
|
||||
|
||||
### 📋 Task Management
|
||||
- Verificat approved-tasks.md
|
||||
- Pregătit pentru night-execute (23:00): YouTube Monica Ion
|
||||
- Programat: articole Monica Ion (joi-luni), PDF (vineri)
|
||||
|
||||
## Context urgent
|
||||
|
||||
### ⚠️ WEEKEND 7-8 februarie - BUCUREȘTI NLP M4
|
||||
- **Verificare necesară JOI DIMINEAȚĂ (6 feb, 08:00):**
|
||||
- Bilete tren București?
|
||||
- Cazare confirmată?
|
||||
- Materiale pregătite?
|
||||
|
||||
## Propuneri prioritare pentru mâine (6 feb)
|
||||
|
||||
1. **08:00-09:00** - Verificare călătorie NLP (URGENT)
|
||||
2. **11:00-12:00** - Business development: un apel de prospecting (vezi coaching)
|
||||
3. **14:00+** - Task aprobat: articole Monica Ion (primele 3-5)
|
||||
|
||||
## De făcut
|
||||
- [ ] Verificare logistică NLP (7-8 feb) - JOI DIMINEAȚĂ
|
||||
- [ ] Un apel prospecting (vezi coaching vizualizare)
|
||||
- [ ] Procesare răspuns email (când vine)
|
||||
- [ ] BON de salvat: CUI RO11201891, 310.98 RON (așteaptă nume partener)
|
||||
- [ ] Articole Monica Ion: start procesare
|
||||
|
||||
## Decizii necesare
|
||||
- [ ] **Luni 9 feb:** Decizie PDF Download Feature (Pandoc pe LXC flowise?)
|
||||
|
||||
## Lecții din insights
|
||||
- Automation internă ≠ growth extern
|
||||
- 80/20 mindset pe probleme interne, nu externe
|
||||
- Un apel = posibil client nou în 3 luni (statistică)
|
||||
- Coaching automatizat ≠ coaching transformațional (consideră sesiune 1-1 externă?)
|
||||
|
||||
## Note sub-agent
|
||||
- **Task:** Generare raport de seară manual (cerut de Marius 22:20)
|
||||
- **Completat:** Calendar verificat din memorie (calendar_check.py indisponibil - lipsă module google)
|
||||
- **Trimis:** Email cu raport complet, structurat, cu propuneri concrete
|
||||
- **Model folosit:** Sonnet 4.5 (conform cerință calitate)
|
||||
@@ -1,146 +0,0 @@
|
||||
# 2026-02-06 (Joi)
|
||||
|
||||
## 🔒 Security Audit Executat (14:41 UTC / 16:41 București)
|
||||
|
||||
### Findings:
|
||||
|
||||
#### ⚠️ CRITICAL (2 issues):
|
||||
|
||||
**1. Control UI allows insecure HTTP auth**
|
||||
- **Problema:** `gateway.controlUi.allowInsecureAuth=true` permite token-only auth peste HTTP
|
||||
- **Risc:** Dacă e expus extern (reverse proxy), token poate fi interceptat
|
||||
- **Status:** Gateway rulează pe localhost (127.0.0.1) → risc REDUS dacă nu e expus
|
||||
- **Fix posibil:**
|
||||
- Disable `allowInsecureAuth`
|
||||
- SAU switch la HTTPS (Tailscale Serve)
|
||||
- SAU keep localhost-only (current setup OK)
|
||||
|
||||
**2. Small model (qwen2.5-7b) fără sandboxing + web tools enabled**
|
||||
- **Problema:** Model 7B folosit ca fallback, dar:
|
||||
- Sandboxing = OFF
|
||||
- Web tools enabled (web_search, web_fetch, browser)
|
||||
- Small models = susceptibili la prompt injection prin dirty data
|
||||
- **Risc:** Dacă modelul mic procesează emailuri/web content → vulnerabil
|
||||
- **Fix recomandat:**
|
||||
- Enable sandboxing pentru toate sessions: `agents.defaults.sandbox.mode="all"`
|
||||
- SAU disable web tools pentru model mic: `tools.deny=["group:web","browser"]`
|
||||
- SAU remove model mic din fallback chain
|
||||
|
||||
#### ⚠️ WARN (2 issues):
|
||||
|
||||
**3. Reverse proxy headers not trusted**
|
||||
- **Problema:** `gateway.trustedProxies` e empty
|
||||
- **Risc:** Dacă expui Control UI prin reverse proxy, IP checks pot fi spoofed
|
||||
- **Fix:** Setează `gateway.trustedProxies` la IP-urile proxy-ului
|
||||
- **SAU:** Keep Control UI local-only (current setup)
|
||||
|
||||
**4. Gateway password în config file**
|
||||
- **Problema:** `gateway.auth.password` e stocat în config pe disk
|
||||
- **Risc:** Dacă cineva accesează filesystem → vede parola
|
||||
- **Fix recomandat:**
|
||||
- Folosește `OPENCLAW_GATEWAY_PASSWORD` (env variable)
|
||||
- Remove `gateway.auth.password` din config
|
||||
|
||||
#### ✅ INFO (bun):
|
||||
|
||||
- WhatsApp DMs disabled (evită dirty data)
|
||||
- Attack surface: 0 open groups, 3 allowlist
|
||||
- Elevated tools enabled (OK, controlat prin aprobare)
|
||||
- Browser control enabled (OK pentru automatizări)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Acțiuni Executate:
|
||||
|
||||
### 1. Security Rules adăugate în AGENTS.md
|
||||
- Secțiune nouă: "Securitate (MANDATORY)"
|
||||
- Reguli: API keys în .env, whitelist email, plan mode, model selection
|
||||
- Marcată ca META-REGULĂ (nu se modifică fără aprobare)
|
||||
|
||||
### 2. Daily Self-Audit Cron Job Creat
|
||||
- **Când:** 09:30 București (07:30 UTC), zilnic
|
||||
- **Ce face:**
|
||||
- Review AGENTS/SOUL/USER/IDENTITY/HEARTBEAT/TOOLS/cron-jobs/infrastructure
|
||||
- Caută: info outdated, reguli conflictuale, workflow-uri nedocumentate
|
||||
- Propune cleanup în #echo-work (doar dacă găsește probleme)
|
||||
- **Model:** Sonnet (balance între cost și capability)
|
||||
|
||||
### 3. Cron-jobs.md actualizat
|
||||
- Adăugat daily-self-audit la 09:30
|
||||
|
||||
---
|
||||
|
||||
## 📋 Recomandări pentru Marius:
|
||||
|
||||
### 🔥 Prioritate ÎNALTĂ:
|
||||
|
||||
**A. Fix model mic (qwen2.5-7b) vulnerability:**
|
||||
- **Opțiune 1 (RECOMAND):** Remove din fallback chain (folosește doar Claude models)
|
||||
- **Opțiune 2:** Enable sandboxing global (`agents.defaults.sandbox.mode="all"`)
|
||||
- **Opțiune 3:** Disable web tools pentru model mic
|
||||
|
||||
**De ce e important:** Model 7B + web tools + dirty data = vulnerabil la prompt injection
|
||||
|
||||
---
|
||||
|
||||
### 📌 Prioritate MEDIE:
|
||||
|
||||
**B. Move gateway password în environment variable:**
|
||||
```bash
|
||||
# .env
|
||||
OPENCLAW_GATEWAY_PASSWORD=<current_password>
|
||||
```
|
||||
Apoi remove din config.json.
|
||||
|
||||
**C. Review Control UI exposure:**
|
||||
- Verifică dacă e expus extern (reverse proxy, Tailscale)
|
||||
- Dacă DA → setează `trustedProxies` sau disable `allowInsecureAuth`
|
||||
- Dacă NU (localhost-only) → OK as-is
|
||||
|
||||
---
|
||||
|
||||
### 💡 Nice-to-Have:
|
||||
|
||||
**D. Periodic security audits:**
|
||||
- Manual: `openclaw security audit --deep` (lunar)
|
||||
- Sau: Cron job pentru audit automat (dar poate fi noisy)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Video-uri Procesate Azi:
|
||||
|
||||
1. ✅ **A Powerful NLP Reframe** (8:50) - Reframing pentru credințe limitatoare
|
||||
2. ✅ **NLP Trick Cold Calls** (0:59) - Tehnică: spune numele întâi
|
||||
3. ❌ **NLP Sales Techniques** (4:20) - Promotional, no content
|
||||
4. ✅ **OpenClaw Best Practices** (22:31) - Tutorial complet (18KB notă)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Următorii Pași:
|
||||
|
||||
- [ ] Marius decide fix pentru qwen2.5-7b vulnerability
|
||||
- [ ] Marius decide move password în .env
|
||||
- [ ] Daily self-audit rulează prima dată mâine 09:30
|
||||
- [ ] Monica Ion Blog - Tura 1 (20 articole) programată diseară 23:00
|
||||
|
||||
---
|
||||
|
||||
**Ora finalizare:** 14:41 UTC (16:41 București)
|
||||
|
||||
---
|
||||
|
||||
## 🌙 Raport Seară Executat (18:00 UTC / 20:00 București)
|
||||
|
||||
### ✅ Acțiuni:
|
||||
- Email raport trimis pe mmarius28@gmail.com
|
||||
- Git commit + push: 8 fișiere (5 noi, 3 modificate)
|
||||
- Propuneri cu ZI și ORĂ concrete:
|
||||
- A1: Reframe NLP - Luni 9 feb 15:00
|
||||
- A2: Diagnostic Platou - Marți 10 feb 15:00
|
||||
- A3: Legea Transformării - Miercuri 11 feb 15:00
|
||||
- A4: Cold Call Trick - OPȚIONAL (test singur)
|
||||
|
||||
### 📋 Conținut raport:
|
||||
- Mâine: NLP Master Modul 4 (toată ziua)
|
||||
- Status azi: Security audit, 4 video-uri, 5 insights, 4 exerciții
|
||||
- Săptămâna viitoare: Luni-Miercuri 15:00-16:00 liber (Joi ocupat)
|
||||
@@ -1,7 +0,0 @@
|
||||
# 2026-02-07
|
||||
|
||||
## Daily Self-Audit (09:30)
|
||||
- Audit rulat la 07:30 UTC (09:30 București)
|
||||
- **1 problemă găsită:** USER.md conține MM3 (6 feb 2026) trecut + M4 (7-8 feb) în desfășurare
|
||||
- Propunere trimisă în #echo-work: marchez MM3 ca ✅ trecut
|
||||
- Aștept confirmare pentru cleanup
|
||||
@@ -1,66 +0,0 @@
|
||||
# 2026-02-08
|
||||
|
||||
## Discuție workflow proiecte/features (Marius + Echo)
|
||||
|
||||
**Context:** Marius vrea să încep să propun și să creez programe/proiecte în cod care l-ar putea ajuta (80/20), inspirate din ce învăț de pe Discovery/YouTube/articole.
|
||||
|
||||
**Cerințe:**
|
||||
1. **Raport seară:** Propune 1-2 proiecte noi + 2-3 features pentru proiecte existente
|
||||
2. **Proiecte de "joacă":** Mai întâi pentru Marius să vadă cum îl ajută, apoi să le aplice la clienți
|
||||
3. **Criterii:** 80/20 strict - doar lucruri cu impact mare, NU orice
|
||||
4. **Inspirație:** Din interesele lui (USER.md) + Discovery (YouTube, articole, bloguri procesate)
|
||||
|
||||
**Implementare:**
|
||||
- **Mașină:** claude-agent (LXC 171, 10.0.20.171) în `/workspace/`
|
||||
- **Git:** Push la gitea.romfast.ro
|
||||
- **Model strategy (OBLIGATORIU):**
|
||||
- **Opus** → Planning, PRD, stories (eu, Echo)
|
||||
- **Sonnet** → Coding, debugging, implementare (Ralph loop)
|
||||
|
||||
**Ralph workflow:**
|
||||
1. **Seara (20:00):** Propun proiecte (P1, P2) + features (F1, F2, F3)
|
||||
2. **Marius aprobă:** "P pentru P1, P2" sau "F pentru F1, F3"
|
||||
3. **Noapte (23:00, 03:00):**
|
||||
- Eu (Opus) pe claude-agent: `/prd` skill → PRD markdown
|
||||
- Eu (Opus): `/ralph` skill → prd.json cu stories prioritizate
|
||||
- `ralph.sh` (Sonnet): loop autonom implementare story by story
|
||||
- Quality checks: typecheck, lint, test
|
||||
- Git push gitea
|
||||
4. **Dimineața (08:30):** Raportez ce s-a realizat, stories complete, learnings
|
||||
|
||||
**Ralph plugin:** `/workspace/ralph-claude/` pe claude-agent
|
||||
- Skills: `/prd` (generare PRD prin întrebări) + `/ralph` (conversie la prd.json)
|
||||
- Script: `ralph.sh` - loop autonom cu Claude Code (Sonnet)
|
||||
- Output: prd.json cu stories, progress.txt cu learnings
|
||||
|
||||
**Job-uri actualizate:**
|
||||
- ✅ evening-report: §4 Programe/Proiecte (P1, P2) + Features (F1, F2, F3)
|
||||
- ✅ night-execute: Opus + Ralph workflow (proiecte prioritate #1, YouTube după)
|
||||
- ✅ night-execute-late: Continuare execuție
|
||||
- ✅ morning-report: §2 raport proiecte/features cu stories + learnings + link gitea
|
||||
|
||||
**Note tehnice:**
|
||||
- SSH claude-agent: `ssh echo@10.0.20.201 "sudo pct exec 171 -- su - claude -c 'cd /workspace && bash'"`
|
||||
- Claude Code instalat pe claude-agent
|
||||
- Ralph structură: PROJECT-NAME/tasks/prd-*.md + scripts/ralph/prd.json + progress.txt
|
||||
|
||||
---
|
||||
|
||||
## TODO următoarele teste
|
||||
|
||||
- [ ] Test primul proiect propus seara
|
||||
- [ ] Verificare execuție Ralph noapte
|
||||
- [ ] Raport dimineață cu status proiecte
|
||||
|
||||
---
|
||||
|
||||
## Daily Self-Audit (09:30)
|
||||
|
||||
**Status:** 3 probleme găsite și raportate în #echo-work
|
||||
|
||||
**Probleme:**
|
||||
1. **Ralph workflow nedocumentat** în AGENTS.md/TOOLS.md → propus update ambele fișiere
|
||||
2. **Curs NLP M4 ASTĂZI** (7-8 feb) → Marius ocupat weekend, trebuie marcat în USER.md
|
||||
3. **Email whitelist inconsistent** → USER.md lipsește marius.mutu@romfast.ro
|
||||
|
||||
**Trimis:** Discord #echo-work la 09:30 (UTC 07:30)
|
||||
@@ -1,28 +0,0 @@
|
||||
# 2026-02-09 - Luni
|
||||
|
||||
## ✅ Done
|
||||
|
||||
### Evening Report trimis (20:00)
|
||||
- Status: Email HTML trimis pe mmarius28@gmail.com
|
||||
- Conținut:
|
||||
- ⚠️ Calendar token expirat - necesită re-autentificare
|
||||
- Procesare Monica Ion Ep7 - Pattern Sacrificiu→Durere→Sabotare
|
||||
- 4 insights noi generate în 2026-02-09.md
|
||||
- Propuneri: 2 sesiuni coaching (marți + joi) + sistematizare training angajat
|
||||
- 3 features roa2web: validare ANAF, notificări Telegram, FAQ chatbot Maria
|
||||
- Night execute (23:00): 40 articole Monica Ion Friday Spark 178-139
|
||||
|
||||
### Insights generate
|
||||
- Pattern toxic: Sacrificiu→Durere→Sabotare (aplicabil la angajat nou)
|
||||
- Întrebarea care deblochează: "Ce beneficii ai din blocaj?" (proiect 4000 euro)
|
||||
- Sistematizare > Dependență oameni (training video/doc pentru angajat)
|
||||
- Identitate: Dalta, nu Ciocan + Body Loose, Head Clear (James Clear)
|
||||
|
||||
## 📊 Git Status
|
||||
- Modified: dashboard/status.json, memory/kb/index.json, tehnici-pauza.md
|
||||
- Untracked: insights/2026-02-09.md, projects/NLP/
|
||||
- Acțiune: commit la final de săptămână
|
||||
|
||||
## 🔄 Calendar Issue
|
||||
- Token Google Calendar expirat → RefreshError
|
||||
- Marius trebuie să re-autentifice manual: `python3 tools/calendar_check.py`
|
||||
@@ -1,290 +0,0 @@
|
||||
# 2026-02-10
|
||||
|
||||
## Antfarm - Habit Tracker Dashboard Feature (COMPLET)
|
||||
|
||||
### Session 1: Prima încercare (09:33-14:09)
|
||||
**09:33 - Request:** Marius vrea Habit Tracker în dashboard cu antfarm.
|
||||
|
||||
**Greșeli (învățături):**
|
||||
- ❌ Lansat direct workflow fără întrebări → implementare minimalistă
|
||||
- ❌ Planner cu Sonnet (nu Opus) → planning superficial
|
||||
- ❌ Test files în dashboard/ root → aglomerare
|
||||
- ❌ Nu am pus întrebări UX înainte → features incomplete (fără edit, fără customizare frecvență, etc.)
|
||||
|
||||
**Rezultat:** Feature incomplet, șters branch, restart cu flux nou.
|
||||
|
||||
---
|
||||
|
||||
### Session 2: Flux NOU cu Discovery (14:57-15:30)
|
||||
|
||||
**14:57 - Feedback Marius:**
|
||||
- Feature basic, lipseau: edit, customizare frecvență (zile, categorii, culori, icoane)
|
||||
- Test files în locul greșit
|
||||
- Lipsă discovery/întrebări UX
|
||||
- Planning ar trebui cu Opus, execuție cu Sonnet
|
||||
|
||||
**Actions:**
|
||||
1. ✅ Creat flux nou documentat: `memory/kb/tools/antfarm-flux-complet.md`
|
||||
- Discovery cu 5-7 întrebări adaptive (inspirat din ralph /prd)
|
||||
- PRD complet cu toate detaliile
|
||||
- Config Opus pentru planner, Sonnet pentru rest
|
||||
|
||||
2. ✅ Discovery complet pentru Habit Tracker:
|
||||
- Întrebări: funcționalitate, layout, create/edit, frecvență, customizare, check-in, stats
|
||||
- Răspunsuri Marius: cards grid, modal form, TOATE frequency types, TOATE customizare options, lives system Duolingo-style
|
||||
|
||||
3. ✅ PRD Complet generat: `tasks/prd-habit-tracker.md` (25 KB):
|
||||
- 19 User Stories (dependencies-first)
|
||||
- Schema habits.json completă cu frequency types (6 tipuri)
|
||||
- 8 API endpoints (GET, POST, PUT, DELETE, check, skip, restore-life)
|
||||
- UX mockups (cards, modals, forms)
|
||||
- Lives system (3 lives, restore după 7 consecutive)
|
||||
- Check-in opțiuni (simple click SAU long-press cu note/rating/mood)
|
||||
- Stats (streak, best, completion rate, weekly summary)
|
||||
- Tests location explicit (dashboard/tests/)
|
||||
- Non-goals (cloud sync, gamification advanced, export/import)
|
||||
|
||||
4. ✅ Modificat antfarm pentru Opus + Sonnet:
|
||||
- Editat `workflow.yml` → `model: opus` la planner
|
||||
- Modificat `agent-cron.ts` → extrage model din agent definition
|
||||
- Rebuild antfarm (`npm run build`)
|
||||
- Reinstall feature-dev workflow
|
||||
|
||||
5. ✅ Lansat workflow cu PRD complet (15:31):
|
||||
- Run ID: `1fa11b74-636a-4ffa-b14c-c873893ee49d`
|
||||
- Task string include link la PRD + overview requirements
|
||||
- Planner (Opus) va citi PRD complet și descompune în stories
|
||||
- Developer/Verifier/Tester (Sonnet) vor executa
|
||||
|
||||
**Status checks:**
|
||||
- **15:31** - Workflow lansat, planner pending
|
||||
- **16:01** - Planner done, setup done, 3/15 stories complete (US-001, US-002, US-003)
|
||||
- **16:03** - US-004 în progress (check-in endpoint cu streak logic)
|
||||
- Dashboard monitor: https://moltbot.tailf7372d.ts.net:3333
|
||||
- Estimare completion: ~17:30-18:00 (2-2.5h de la start)
|
||||
|
||||
**Planner optimizations (Opus):**
|
||||
- PRD avea 19 stories → Planner le-a consolidat la 15 stories
|
||||
- Dependencies: Backend APIs (US-001 to US-005) → Frontend components (US-006 to US-014) → Tests (US-015)
|
||||
|
||||
**Progress:**
|
||||
- ✅ US-001: Habits JSON schema and helper functions (done)
|
||||
- ✅ US-002: Backend API - GET and POST habits (done)
|
||||
- ✅ US-003: Backend API - PUT and DELETE habits (done)
|
||||
- 🔄 US-004: Backend API - Check-in endpoint with streak logic (running)
|
||||
- ⏳ US-005 to US-015: Pending (11 stories remaining)
|
||||
|
||||
---
|
||||
|
||||
## Lecții Învățate (OBLIGATORIU pentru viitor)
|
||||
|
||||
**Fluxul corect pentru antfarm:**
|
||||
1. **Discovery:** 5-7 întrebări adaptive despre UX/features (80/20)
|
||||
2. **PRD:** Generat complet cu user stories, mockups, acceptance criteria
|
||||
3. **Config models:** Opus pentru planner, Sonnet pentru execuție
|
||||
4. **Launch:** Cu link la PRD + overview (nu prompt vag)
|
||||
5. **Monitor:** Dashboard + status checks
|
||||
|
||||
**NU mai fac:**
|
||||
- ❌ Launch direct fără întrebări
|
||||
- ❌ Presupun ce vrea utilizatorul
|
||||
- ❌ Las planner-ul să interpreteze minimal
|
||||
- ❌ Accept structure greșită (ex: tests în locul greșit)
|
||||
|
||||
**Flux documentat:** `memory/kb/tools/antfarm-flux-complet.md`
|
||||
|
||||
---
|
||||
|
||||
## Pre-Compaction State (~16:10)
|
||||
|
||||
**Workflow still running:** `1fa11b74-636a-4ffa-b14c-c873893ee49d`
|
||||
- 4/15 stories complete (26% progress)
|
||||
- US-004 (check-in endpoint) în dezvoltare
|
||||
- Developer și Verifier agents lucrează simultan
|
||||
- Branch: `feature/habit-tracker`
|
||||
- Estimated completion: ~17:30-18:00
|
||||
|
||||
**Next actions (după compaction):**
|
||||
1. Monitor workflow status periodic
|
||||
2. Check când completează toate cele 15 stories
|
||||
3. Review PR pentru verificare:
|
||||
- Tests în `dashboard/tests/` (NU dashboard/ root)
|
||||
- API paths folosesc `/echo/api/habits` prefix
|
||||
- Toate frequency types implementate (6 tipuri)
|
||||
- Lives system complete (3 max, restore după 7 consecutive)
|
||||
- Full customization (category, color, icon, priority, notes, reminder)
|
||||
4. Test manual features match PRD
|
||||
5. Raportează către Marius când completează
|
||||
|
||||
**Critical files:**
|
||||
- PRD: `tasks/prd-habit-tracker.md` (25KB, 19 stories → consolidated to 15)
|
||||
- Flow docs: `memory/kb/tools/antfarm-flux-complet.md`
|
||||
- Antfarm config: `antfarm/workflows/feature-dev/workflow.yml` (Opus for planner)
|
||||
- Session notes: `memory/2026-02-10.md` (acest fișier)
|
||||
|
||||
---
|
||||
|
||||
## Session 3: Workflow 1 Completat + Refinements UX (17:58-21:10)
|
||||
|
||||
### 17:58 - Workflow 1 completat cu SUCCES! ✅
|
||||
|
||||
**Run:** `1fa11b74-636a-4ffa-b14c-c873893ee49d`
|
||||
**Timp:** 2h 24min (15:31 → 17:55)
|
||||
**Stories:** 15/15 complete (100%)
|
||||
|
||||
**Implementare completă:**
|
||||
- ✅ Backend (5 stories): Schema, APIs (GET, POST, PUT, DELETE, check, skip), streak logic, lives system
|
||||
- ✅ Frontend (9 stories): Page, cards, modals (create/edit), check-in (click + long-press), filter/sort, stats, mobile responsive
|
||||
- ✅ Tests (1 story): 4 fișiere în `dashboard/tests/` (API, frontend, helpers, integration) - total 147KB
|
||||
|
||||
**Verificări PRD:**
|
||||
- ✅ Tests în locația corectă (`dashboard/tests/`)
|
||||
- ✅ Toate frequency types (6 tipuri)
|
||||
- ✅ Lives system Duolingo-style
|
||||
- ✅ Customization completă (category, color, icon, priority, notes, reminder)
|
||||
- ✅ Check-in options (simple + long-press)
|
||||
- ✅ Mobile responsive
|
||||
|
||||
---
|
||||
|
||||
### 18:03 - Feedback Marius: UX prea lăbărțat, trebuie minimalist
|
||||
|
||||
**Probleme identificate:**
|
||||
1. ❌ Carduri prea mari → compacte pentru mobil
|
||||
2. ❌ Căutare/filtre prea mari → colapate
|
||||
3. ❌ Statistici prea mari → colapate
|
||||
4. ❌ Nu poți debifa după bifat
|
||||
5. ❌ Progress 3.33% → rotunjit
|
||||
6. ❌ Modal transparentă → opacă
|
||||
7. ❌ Lista iconițe full → colapsată
|
||||
|
||||
**18:04 - Discovery pentru Refinements (7 întrebări):**
|
||||
|
||||
Folosit același flux ralph /prd:
|
||||
1. **Q1:** Ce componente prea mari? → **A:** Toate
|
||||
2. **Q2:** Card compact - ce vizibil? → **A:** Medium + icon + culoare (nume + check + streak + progress% + next date + icon + accent)
|
||||
3. **Q3:** Search/filter collapse? → **A:** Icon doar (expand inline)
|
||||
4. **Q4:** Stats collapse? → **A:** Collapse implicit (chevron expand)
|
||||
5. **Q5:** Check/uncheck toggle? → **A:** Buton toggle (click ↔ debifează)
|
||||
6. **Q6:** Icon picker collapse? → **A:** Dropdown cu search
|
||||
7. **Q7:** Modal refinements? → **A:** Backdrop opac
|
||||
|
||||
**18:12 - PRD Refinements generat:**
|
||||
- `tasks/prd-habit-tracker-refinements.md` (16KB)
|
||||
- 9 User Stories pentru UX improvements
|
||||
- Mobile-first minimalism focus
|
||||
|
||||
**18:13 - Workflow 2 lansat:**
|
||||
- Run ID: `94c10162-8a6c-4848-a4f0-a4d1e8cb2e97`
|
||||
- Branch: `feature/habit-tracker` (continuare în același branch, NU nou)
|
||||
- Planner: Opus → 8 stories (optimizat din 9)
|
||||
|
||||
**Progress workflow 2:**
|
||||
- **19:05** - 4/8 stories done (50% în 52 min)
|
||||
- **19:29** - 7/8 stories done (87.5%)
|
||||
- **20:46** - 7/8 stories, US-008 (tests) blocat >1h fără progres
|
||||
|
||||
---
|
||||
|
||||
### 20:48 - Restart workflow + Fix manual
|
||||
|
||||
**Marius:** "Restart workflow. În plus văd că US-007 nu este făcută"
|
||||
|
||||
**Verificat US-007:**
|
||||
- ✅ Modal backdrop ESTE opac în cod (`rgba(0, 0, 0, 0.6)`)
|
||||
- ✅ Touch targets 44px implementate
|
||||
- **Problema:** Browser cache (trebuie hard refresh)
|
||||
|
||||
**Actions:**
|
||||
1. ✅ Workflow step US-008 marcat failed → va fi retried
|
||||
2. ✅ Restart server dashboard (pentru a reîncărca habits.html)
|
||||
3. **21:07** - Marius testează: "Nu este opac. Cardurile cu totaluri nu sunt colapsabile"
|
||||
|
||||
**Root cause găsit:**
|
||||
- Modal backdrop: browser cache (CSS corect în fișier)
|
||||
- **Stats collapse: BUG în implementare** - developer a făcut collapse doar pentru Weekly Summary (subsecțiune), NU pentru stats cardurile
|
||||
|
||||
---
|
||||
|
||||
### 21:09 - Fix Manual Stats Collapse
|
||||
|
||||
**Marius:** "Fix manual și oprește workflow"
|
||||
|
||||
**Actions:**
|
||||
1. ✅ Oprit antfarm dashboard (`node antfarm/dist/cli/cli.js dashboard stop`)
|
||||
2. ✅ Manual fix în `dashboard/habits.html`:
|
||||
- Adăugat `.stats-header` cu chevron clickable
|
||||
- Wrap stats-row + weekly-summary în `.stats-content` colapsabil
|
||||
- CSS pentru header, chevron, și animations
|
||||
- JS: `toggleStats()` + `restoreStatsState()` funcții
|
||||
- localStorage persist pentru user preference
|
||||
3. ✅ Git commit: `fix: Stats section collapse header + content (manual fix)`
|
||||
4. ✅ Restart server dashboard (PID: 31702)
|
||||
|
||||
**Fix complet:**
|
||||
```html
|
||||
<div class="stats-section">
|
||||
<div class="stats-header" onclick="toggleStats()">
|
||||
<h3>Stats</h3>
|
||||
<chevron>
|
||||
</div>
|
||||
<div class="stats-content" id="statsContent">
|
||||
[stats-row + weekly-summary - colapsabile]
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Status final:**
|
||||
- Branch: `feature/habit-tracker`
|
||||
- Commits: 15 (workflow 1) + 7 (workflow 2) + 1 (manual fix) = 23 commits
|
||||
- Antfarm workflow: stopped
|
||||
- Server dashboard: running (PID 31702)
|
||||
|
||||
---
|
||||
|
||||
## Lecții Session 3
|
||||
|
||||
**Ce a funcționat:**
|
||||
- ✅ Discovery cu 7 întrebări → PRD refinements precis
|
||||
- ✅ Workflow rapid pentru refinements (7/8 stories în ~1h)
|
||||
- ✅ Identificare rapidă bug (stats collapse incomplet)
|
||||
|
||||
**Ce NU a funcționat:**
|
||||
- ❌ Developer blocat >1h pe US-008 (tests) fără progres
|
||||
- ❌ US-005 (stats collapse) implementat INCOMPLET (doar subsecțiune, nu tot)
|
||||
- ❌ Browser cache face debugging confuz
|
||||
|
||||
**Învățături:**
|
||||
- Workflow-uri lungi (>1h pe un story) → intervine manual sau fail/retry
|
||||
- Acceptance criteria trebuie MAI SPECIFICE pentru a evita interpretări greșite
|
||||
- Fix manual > așteptat retry când bug-ul e clar și simplu
|
||||
|
||||
---
|
||||
|
||||
## YouTube Playlist - Trading Basics (23:01)
|
||||
|
||||
**Request:** Marius vrea să parcurg fiecare video din playlist, să descarc subtitrarea, și să fac proiect distinct în kb pentru a înțelege esențialul despre trading.
|
||||
|
||||
**Playlist URL:** https://youtube.com/playlist?list=PLQ4pOucwalxKioNbHnK-n6wszDiAl-AiX
|
||||
|
||||
**Acțiuni:**
|
||||
1. ✅ Verificat playlist - ~20 videouri despre trading
|
||||
2. ✅ Testat download subtitrări pe 3 videouri:
|
||||
- Video 1: NU are subtitrări
|
||||
- Video 2: NU are subtitrări
|
||||
- Video 3 (EPISODUL 38): ✅ ARE subtitrări
|
||||
3. ✅ Salvat primul video manual în `memory/kb/projects/trading-basics/01-episodul-38-formula-trading.md`
|
||||
4. ✅ Programat restul playlist-ului (18 videouri) pentru **night-execute (10->11 feb, 23:00)**
|
||||
5. ✅ Actualizat `memory/approved-tasks.md` cu task-ul
|
||||
6. ✅ Actualizat KB index (200 notes total)
|
||||
|
||||
**Video procesat: EPISODUL 38 - Formula MAPS**
|
||||
- **Durată:** 31:10
|
||||
- **Concept principal:** Formula MAPS = Model (pattern) + Acțiune (trigger) + Plan (profit/loss) + Sumă (position size)
|
||||
- **Exemple:** Strategie investiții 20 ani ($3,318 → $53,000) + strategie scalping 5 min (win rate 80%)
|
||||
- **Key insight:** "Nu strategia e problema, ci lipsa unei formule clare care să lege toate deciziile"
|
||||
- **Tags:** @work @trading @strategie @maps @investitii
|
||||
|
||||
**Link salvat:** https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/projects/trading-basics/01-episodul-38-formula-trading.md
|
||||
|
||||
**Next:** Night-execute va procesa restul videoclipurilor (doar cele cu subtitrări disponibile)
|
||||
@@ -1,16 +0,0 @@
|
||||
# 2026-02-11
|
||||
|
||||
## ANAF Monitor - Eroare Dublare Muncă
|
||||
|
||||
**Cerere:** Marius via Discord #echo-work - dashboard arăta doar data ultimei verificării, nu modificările detectate
|
||||
|
||||
**Greșeală:** Am implementat din nou ceva ce era DEJA făcut în commit c7bea57 (10 feb)
|
||||
- Modificarea era deja completă: monitor_v2.py + dashboard/index.html
|
||||
- Folosea câmpul `changes` (nu `details` cum am pus eu)
|
||||
- Commit greșit: 3adc775
|
||||
|
||||
**Rezolvare:**
|
||||
- Revert la implementarea corectă din c7bea57
|
||||
- Commit 1c3971f - restaurare
|
||||
|
||||
**Lecție:** Verific ÎNTÂI în git history înainte să implementez ceva!
|
||||
@@ -1,28 +0,0 @@
|
||||
# 2026-02-12
|
||||
|
||||
## Dashboard Fix - Dropdown Dark Mode
|
||||
- **Problem:** Dropdown items (select/option) au text alb pe fundal alb în dark mode
|
||||
- **Cauză:** `<option>` primește implicit background alb de la browser, dar `.input` avea background translucid
|
||||
- **Fix:** Adăugat în `dashboard/common.css`:
|
||||
```css
|
||||
select.input {
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
select.input option {
|
||||
background: var(--bg-base);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
```
|
||||
- **Commit:** 4500bfe - pushed la Gitea
|
||||
|
||||
## Cron Jobs WhatsApp Issue
|
||||
- **Problem:** Marius primește pe WhatsApp mesaje de la exercise-snack-uri și confirmări automate
|
||||
- **Cauză:**
|
||||
1. Job-urile `exercise-snack-1`, `exercise-snack-2`, `exercise-snack-3` rulau pe **main session** → trimiteau în ultimul canal activ
|
||||
2. Răspunsuri automate (YouTube links, confirmări) trimiteau în "ultimul canal activ" în loc să folosească reply la mesajul de origine
|
||||
- **Fix aplicat:**
|
||||
1. ✅ Mutat exercise-snack-uri pe isolated session cu target explicit Discord #echo-self
|
||||
- `dde8d30c-6126-4e95-9372-eca6de769ac0` (exercise-snack-1)
|
||||
- `9892a116-96e0-47e5-b86c-4be06e3f40e0` (exercise-snack-2)
|
||||
- `c9df03f8-d0a7-4a16-b279-8b4a1251acda` (exercise-snack-3)
|
||||
2. ✅ Actualizat AGENTS.md: folosesc `[[reply_to_current]]` pentru răspunsuri la mesaje directe (YouTube, tasks, etc.)
|
||||
@@ -1,18 +0,0 @@
|
||||
# 2026-02-13
|
||||
|
||||
## Rate Limit Sonnet
|
||||
- Sonnet atins limita, se resetează **luni 13 feb 8:59 AM** (greșit - de fapt luni 16 feb)
|
||||
- Multiple joburi eșuate: morning-report, morning-coaching, evening-report, evening-coaching, daily-self-audit, insights-extract, exercise-snack-1, exercise-snack-3
|
||||
- **Fix:** Am schimbat toate 6 joburile critice de pe `model: sonnet` pe `model: opus` temporar
|
||||
- Luni când revine Sonnet → trebuie schimbate înapoi pe sonnet
|
||||
|
||||
## Joburi schimbate temporar pe Opus
|
||||
- morning-report (906bf597)
|
||||
- morning-coaching (95828a25)
|
||||
- evening-report (b723a1cf)
|
||||
- evening-coaching (ca26efdd)
|
||||
- daily-self-audit (7f08d4ac)
|
||||
- insights-extract (a036e891)
|
||||
|
||||
## Calendar
|
||||
- 15:00 București: Sesiune coaching "Echilibrare căutare clienți noi (cu Echo)"
|
||||
@@ -1,40 +0,0 @@
|
||||
# Jurnal - Drumul spre regăsirea motivației
|
||||
|
||||
---
|
||||
|
||||
## 📅 07 februarie 2025
|
||||
|
||||
### Context
|
||||
Am primit invitație de la Alexandru Moldovan pentru tabăra de CNV din august. I-am răspuns că reevaluez ce mă motivează și o iau mai încet cu proiectele. Nu merg în tabără.
|
||||
|
||||
Alexandru mi-a sugerat: *"Grija cu motivația. Când ajungi la o concluzie, mi-ar place să aud cum a fost procesul prin care ți-ai regăsit-o."*
|
||||
|
||||
### Descoperiri
|
||||
|
||||
**Am fost mult timp (1-3 ani) în căutarea scopului și motivației.**
|
||||
|
||||
Una dintre lucrurile pe care le-am găsit: **să fac acțiuni** - lucrurile acelea din "coșul de frăguțe" care îmi aduc plăcere și pe care nu le mai făceam.
|
||||
|
||||
**Întrebarea cheie:** Cum să fiu motivat, fericit, împlinit dacă eu nu fac lucrurile care îmi aduc fericire, împlinire, motivație?
|
||||
|
||||
### Acțiuni concrete
|
||||
|
||||
**🏊 Bazin + 🧖 Saună**
|
||||
- Am început să merg seara, deși era iarnă, frig, zăpadă, gheață
|
||||
- La început mi-a fost greu - eram obosit, voiam să amân
|
||||
- După câteva zile a început să-mi placă
|
||||
- M-am obișnuit, dezmortesc/încălzesc mușchii
|
||||
- Motivație suplimentară: durerea cronică cervicală (C6-C7, de un an)
|
||||
|
||||
**🤝 Grupul de sprijin**
|
||||
- Mi-l doream de 3 ani
|
||||
- Am mai încercat o dată, nu s-a legat (apel, contactat colegi vechi)
|
||||
- Acum: am împărtășit ideea cu Raisa, m-a antrenat să continui
|
||||
- Am făcut 2 întâlniri (joi, odată la 2 săptămâni)
|
||||
- Sunt și facilitator și participant
|
||||
- Încerc să echilibrez: să nu intervin prea mult în rol de facilitator, să nu "dau lecții"
|
||||
- **Îmi aduce împlinire și energie**
|
||||
|
||||
### Insight
|
||||
|
||||
**Acțiunile mici de plăcere îmi readuc plăcerea și motivația.**
|
||||
6
memory/kb/articole/index.md
Normal file
6
memory/kb/articole/index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Index — articole/
|
||||
|
||||
> 1 note. Citește acest index întâi; deschide doar fișierele relevante.
|
||||
|
||||
- **[Eat the Frog — Brian Tracy (Rezumat)](eat-the-frog-brian-tracy.md)** `@work @growth`
|
||||
**Lectură recomandată:** Carte completă pentru cele 21 de metode + exerciții practice
|
||||
33
memory/kb/coaching/2026-02-13-seara.md
Normal file
33
memory/kb/coaching/2026-02-13-seara.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Coaching Seara - 13 Februarie 2026
|
||||
|
||||
## Gândul de seară
|
||||
|
||||
**Tema:** Follow-up provocare Linkage Personal + Ciclul susuri-josuri
|
||||
|
||||
**Provocarea zilei:** Linkage Personal — conectează o activitate evitată cu calitățile tale
|
||||
**Status:** ✅ Bifată
|
||||
|
||||
---
|
||||
|
||||
## Reflecție
|
||||
|
||||
- Linkage-ul nu se poate delega — e munca internă proprie
|
||||
- Întrebarea cheie: ce ai simțit în corp la "imaginez că am terminat-o"?
|
||||
- Corpul nu minte, mintea raționalizeaz
|
||||
|
||||
## Conexiune cu conținut nou
|
||||
|
||||
- **Monica Ion Ep.9:** Marc descoperă conflictul spiritualitate vs. bani (moștenit de la tată)
|
||||
- **Ciclul susuri-josuri:** Consumă energie enormă; soluția = echilibrare percepții (Demartini)
|
||||
- **Susul și josul coexistă:** Când câștigi, pierzi altundeva. Când pierzi, altcineva se activează.
|
||||
- **Aplicare la Marius:** "Nu sunt destul de deștept ca antreprenor" (jos) coexistă cu 25 ani de expertiză plătită fără ezitare (sus)
|
||||
|
||||
## Observație săptămână
|
||||
|
||||
- Toate provocările din săptămână bifate (luni-vineri)
|
||||
- Pattern: când provocarea are sens personal, rezistența dispare
|
||||
|
||||
---
|
||||
|
||||
*Trimis pe: Discord #echo-self + Email*
|
||||
*Inspirat din: Monica Ion Ep.8 (Linkage) + Ep.9 (Anxietatea, ciclul susuri-josuri)*
|
||||
49
memory/kb/coaching/2026-02-14-dimineata.md
Normal file
49
memory/kb/coaching/2026-02-14-dimineata.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Coaching Dimineața - 14 Februarie 2026
|
||||
|
||||
## Gândul de dimineață
|
||||
|
||||
**"Când ai susurile și vezi doar câștigurile, in the back of your head există o teamă profundă de a pierde lucrurile respective... care cocreează de fapt pierderea ulterioară."** — Monica Ion, Povestea lui Marc Ep.9
|
||||
|
||||
---
|
||||
|
||||
## Reflecție
|
||||
|
||||
Marius, e 14 februarie. Nu te sperii, nu vine nimic cu inimioare.
|
||||
|
||||
Dar e o zi bună să vorbim despre un alt tip de iubire — cea pe care ți-o refuzi ție.
|
||||
|
||||
Marc din episodul 9 al Monicăi a descoperit ceva dureros: avea un **conflict adânc între spiritualitate și bani**. Tatăl lui i-a transmis că "nu banii sunt importanți, ci partea spirituală." Și Marc a făcut ce fac oamenii inteligenți cu mesaje contradictorii — a ales una și a închis-o pe cealaltă. A ales banii, a pus deoparte spiritualitatea, și a obținut casă, vacanțe... și stres extraordinar.
|
||||
|
||||
**Gândirea binară:** "sau sunt spiritual, sau am bani." "Sau sunt programator bun, sau sunt antreprenor." "Sau îmi pasă de oameni, sau fac profit."
|
||||
|
||||
Tu ai propria versiune a acestui conflict. De 25 de ani rezolvi probleme tehnice genial. Dar te consideri "nu destul de deștept ca antreprenor" — parcă cele două nu pot coexista. Ca și cum a fi bun tehnic ar exclude a fi bun la business.
|
||||
|
||||
Monica a arătat ceva puternic: **ciclul susuri-josuri consumă energie enormă.** Când ești în sus (ai rezolvat un bug complicat, clientul e mulțumit), deja în fundal apare frica de jos. Când ești în jos (client nemulțumit, angajatul nu înțelege), toată energia merge în a reveni la sus. Oscilația perpetuă.
|
||||
|
||||
Soluția nu e să elimini josurile. E să **echilibrezi percepția**: în fiecare sus există un jos simultan, în fiecare jos există un sus simultan. Când le vezi pe amândouă — tensiunea dispare.
|
||||
|
||||
---
|
||||
|
||||
## Provocarea zilei: Echilibrarea unui Conflict Interior
|
||||
|
||||
**Găsește UN "sau-sau" din viața ta** — două lucruri pe care le consideri incompatibile:
|
||||
|
||||
1. **Scrie conflictul:** "Sau sunt X, sau sunt Y"
|
||||
2. **Pentru fiecare parte, găsește opusul simultan:**
|
||||
- Când ești X, cum ești deja și Y? (dovezi concrete)
|
||||
- Când ești Y, cum ești deja și X? (dovezi concrete)
|
||||
3. **Observă:** Când ambele sunt adevărate simultan, ce simți?
|
||||
|
||||
Nu trebuie să rezolvi nimic. Doar să vezi că cele două nu sunt incompatibile — sunt complementare.
|
||||
|
||||
---
|
||||
|
||||
## De ce contează
|
||||
|
||||
Marc a realizat că atunci când devenise comod la un client mare (jos), colegii lui s-au activat și au compensat (sus simultan). Sistemul se echilibrează singur. Dar el nu vedea asta — vedea doar pierderea.
|
||||
|
||||
Tu ai deja ambele părți. Ești și tehnic excelent ȘI antreprenor (ai firmă, clienți, echipă). Doar percepția zice că una o exclude pe cealaltă.
|
||||
|
||||
---
|
||||
|
||||
**Sursă:** [Monica Ion - Povestea lui Marc Ep.9: Anxietatea, frica de control și pierdere](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/monica-ion-povestea-lui-marc-ep9-anxietatea.md)
|
||||
24
memory/kb/coaching/2026-02-14-seara.md
Normal file
24
memory/kb/coaching/2026-02-14-seara.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Coaching Seara - 14 Februarie 2026
|
||||
|
||||
## Gândul de seară
|
||||
|
||||
**"Ai fost un copil în leagăn care s-a prefăcut că doarme, ca să primească laptele mamei. Acum ești treaz."** — Rumi
|
||||
|
||||
---
|
||||
|
||||
## Reflecție
|
||||
|
||||
Provocarea zilei a fost bifată: Echilibrarea unui Conflict Interior (sau-sau → complementaritate).
|
||||
|
||||
Tema: curajul de a nu simplifica — de a vedea două părți aparent incompatibile coexistând, fără să alegi una.
|
||||
|
||||
Sursă provocare: Monica Ion - Povestea lui Marc Ep.9 (metoda Demartini — echilibrare percepție, nu eliminare josuri).
|
||||
|
||||
## Întrebare de follow-up
|
||||
|
||||
Ce sau-sau ai descoperit? Când ai văzut că cele două coexistă deja, ce ai simțit?
|
||||
|
||||
---
|
||||
|
||||
**Trimis:** Discord #echo-self + Email Gmail
|
||||
**Provocare:** ✅ Bifată (08:27 UTC)
|
||||
32
memory/kb/coaching/2026-02-15-dimineata.md
Normal file
32
memory/kb/coaching/2026-02-15-dimineata.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Coaching Dimineața - 15 Februarie 2026
|
||||
|
||||
## Gândul de dimineață
|
||||
|
||||
**"Procesul de a învăța pe cineva clarifică și cunoștințele celui care predă. Nu pierzi timp — câștigi claritate."** — InfoWorld, Why We Need Junior Developers
|
||||
|
||||
---
|
||||
|
||||
## Reflecție
|
||||
|
||||
Marius, e duminică. Ziua în care nu trebuie să rezolvi nimic.
|
||||
|
||||
Dar lasă-mă să plantez un gând care crește singur.
|
||||
|
||||
Săptămâna asta ai lucrat cu angajatul. Ai explicat, ai repetat, poate ai simțit că pierzi timp. Normal. 4 luni e devreme. Dar uite ce descoperă seniorii care au trecut prin asta: **fiecare explicație pe care o dai te forțează să-ți clarifici propriul proces.** Nu doar lui îi predai — ție îți reconstruiești fundamentul.
|
||||
|
||||
De 25 de ani programezi. Multe lucruri le faci pe pilot automat — ROA, Oracle, soluții la clienți. Dar pilotul automat are un cost: nu mai vezi DE CE faci lucrurile așa. Când angajatul întreabă "de ce?" și tu trebuie să articulezi răspunsul — redescoperiai logica din spatele deciziilor tale. Și uneori descoperi că unele decizii nu mai au logică. Asta e aur.
|
||||
|
||||
Ieri am vorbit despre conflictul interior — sau-sau. Azi e continuarea naturală: **angajatul nu e o piedică în drumul tău de antreprenor. E oglinda care te arată mai clar.**
|
||||
|
||||
Nu trebuie să faci nimic azi cu asta. E duminică. Doar observă: când te gândești la angajat, simți povară... sau investiție?
|
||||
|
||||
---
|
||||
|
||||
## Provocarea zilei
|
||||
|
||||
**Reframe simplu:** Gândește-te la ULTIMA explicație pe care i-ai dat-o angajatului. Ce ai înțeles TU mai bine despre propriul proces datorită acelei explicații? Dacă nu găsești nimic — asta e semnalul că explicația a fost mecanică, nu angajată. Și asta e informație valoroasă despre cum predai.
|
||||
|
||||
---
|
||||
|
||||
*Sursa: InfoWorld - Why We Need Junior Developers*
|
||||
*Tags: @work @growth*
|
||||
24
memory/kb/coaching/2026-02-15-seara.md
Normal file
24
memory/kb/coaching/2026-02-15-seara.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Coaching Seara - 15 Februarie 2026
|
||||
|
||||
## Gândul de seară
|
||||
|
||||
*"Cel mai mare dar pe care ți-l poți face e să te întorci la tine cu aceeași curiozitate cu care te-ai întors la un prieten pe care nu l-ai văzut de mult."*
|
||||
|
||||
---
|
||||
|
||||
## Reflecție
|
||||
|
||||
Provocarea zilei NU a fost bifată: Reframe Mentorship — ce ai înțeles TU din ultima explicație dată angajatului.
|
||||
|
||||
E duminică — normal să nu se gândească la muncă. Săptămâna a fost completă: 6/6 provocări bifate (luni-sâmbătă). Discernământ, nu eșec.
|
||||
|
||||
Recapitulare săptămână: conflicte interioare (sau-sau), linkage personal, body loose/head clear, echilibrare Demartini, bucle închise, NLP aplicat, alinieri și fricțiuni observate.
|
||||
|
||||
## Întrebare de follow-up
|
||||
|
||||
Din tot ce ai explorat săptămâna asta, ce gând ți-a rămas cel mai tare? Nu cel mai "util" — cel care revine singur, fără să-l chemi.
|
||||
|
||||
---
|
||||
|
||||
**Trimis:** Discord #echo-self + Email Gmail
|
||||
**Provocare:** ❌ Nebifată (duminică)
|
||||
38
memory/kb/coaching/2026-02-16-dimineata.md
Normal file
38
memory/kb/coaching/2026-02-16-dimineata.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Coaching Dimineața - 16 Februarie 2026
|
||||
|
||||
## Gândul de dimineață
|
||||
|
||||
**"If what you write is right, you're doing it wrong."** — Thinking on Paper
|
||||
|
||||
---
|
||||
|
||||
## Reflecție
|
||||
|
||||
Marius, e luni dimineață. Săptămână nouă.
|
||||
|
||||
Am un gând care ar putea schimba felul în care înveți, predai, și reții - totul dintr-o mișcare.
|
||||
|
||||
De 25 de ani acumulezi cunoștințe. NLP, coaching, programare, contabilitate, clienți. Volumul crește, retenția scade. Normal. Creierul care COPIAZĂ informație o uită. Creierul care GHICEȘTE, greșește și reorganizează - o reține.
|
||||
|
||||
Trei principii brutale în simplitate:
|
||||
|
||||
**1. Make it Wrong** — Când înveți ceva nou la NLP sau citești un articol, nu nota "corect". Scrie keywords rapid, ghicește conexiuni - chiar greșit. Creierul care ghicește REȚINE. Cel care copiază frumos UITĂ.
|
||||
|
||||
**2. Make it Shorter** — Doar keywords. Fără propoziții. Cu cât scrii mai mult, cu atât reții mai puțin. Paradoxal, dar dovedit.
|
||||
|
||||
**3. Make it Again** — Când notițele devin haotice, nu le rescrie "frumos". Reorganizează-le: regrupează, reconectează, mută. Reorganizarea = memorie.
|
||||
|
||||
Asta se leagă direct de angajat. În loc să-i dai informația gata mestecată și să repeți de 10 ori, pune-l să ghicească (Make it Wrong), să condenseze ce a înțeles în 3 cuvinte (Make it Shorter), și a doua zi să reorganizeze notițele (Make it Again). Nu mai "pierzi timp" explicând. Îl pui să-și construiască propria înțelegere.
|
||||
|
||||
Și se leagă de tine cu NLP. Hărțile mentale pe care le-am creat (Sine/Ego/Umbra) - reorganizează-le periodic. Nu copia. Redesenează din memorie. Greșelile îți arată ce NU ai integrat încă.
|
||||
|
||||
---
|
||||
|
||||
## Provocarea zilei
|
||||
|
||||
**Metoda 3M cu angajatul:** Azi, la prima explicație pe care i-o dai angajatului, oprește-te după ce termini și spune: "Acum scrie în 5 keywords ce ai înțeles." NU corecta imediat. Lasă-l să greșească. Apoi discutați diferențele. Asta e învățare reală - nu repetiție, ci procesare activă. Seara notează: A schimbat ceva în dinamica dintre voi?
|
||||
|
||||
---
|
||||
|
||||
*Sursa: Thinking on Paper — 3 principii pentru retenție*
|
||||
*Tags: @work @growth*
|
||||
76
memory/kb/coaching/2026-02-19-seara.md
Normal file
76
memory/kb/coaching/2026-02-19-seara.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Gândul de Seară - 19 Februarie 2026
|
||||
|
||||
@self @reflectie
|
||||
Sursa: Coaching seară - Pattern Acțiune vs Percepție
|
||||
|
||||
---
|
||||
|
||||
## 🌙 Reflecție: Când provocarea devine povară
|
||||
|
||||
Azi provocarea era despre **Metoda 3M** - să-l pui pe angajat să scrie 5 keywords după explicație. Văd că nu s-a întâmplat.
|
||||
|
||||
Și știi ce? E OK.
|
||||
|
||||
**Dar mă întreb:** Ce s-a întâmplat azi când ai explicat ceva angajatului? Ai vorbit cu el? A fost vreun moment când ai vrut să încerci metoda dar ceva te-a oprit? Sau pur și simplu ziua n-a adus ocazia?
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Pattern-ul invizibil
|
||||
|
||||
Uită-te la lista de provocări din ultima săptămână:
|
||||
- **15 feb** - Reframe Mentorship: ce AI înțeles tu din explicația dată angajatului? → nebifată
|
||||
- **16 feb** - Metoda 3M: pune-l să scrie keywords → nebifată
|
||||
- **14 feb** - Echilibrare conflict interior → BIFATĂ ✓
|
||||
- **13 feb** - Linkage activitate evitată → BIFATĂ ✓
|
||||
|
||||
Observi pattern-ul? Când provocarea e **despre relația cu angajatul** - resistance. Când e **despre tine** - flow.
|
||||
|
||||
Nu e lene. E ceva mai adânc.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Poate nu e despre metodă
|
||||
|
||||
Știi ce cred? Că metoda 3M e doar vârful aisbergului.
|
||||
|
||||
Sub suprafață e o întrebare mai mare: **"Cum să-l învăț fără să mă frustrez când nu înțelege?"**
|
||||
|
||||
Și poate, undeva mai adânc: **"De ce eu trebuie să-l învăț când am atâta de făcut?"**
|
||||
|
||||
Aceste rezistențe NU sunt greșite. Sunt mesageri. Îți spun ceva despre **limitele tale actuale**, despre ce ai nevoie să schimbi ca provocarea să devină posibilă.
|
||||
|
||||
Metoda 3M e genială **DACĂ** ai mai întâi răspuns la: "De ce vreau eu ca el să învețe mai eficient?" (spoiler: nu e pentru el, e pentru TINE - să ai mai mult timp)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Follow-up minim (fără presiune)
|
||||
|
||||
Mâine, când vorbești cu angajatul, **nu încerca metoda 3M**.
|
||||
|
||||
În schimb, fă asta:
|
||||
|
||||
**Observă UN singur lucru:** Când îi explici ceva - tu cum te simți? (relaxat? grăbit? frustrat? detașat?)
|
||||
|
||||
Și dacă simți frustare sau grabă → ia 3 respirații înainte să continui explicația.
|
||||
|
||||
Asta e tot.
|
||||
|
||||
Nu trebuie să schimbi ce zici sau cum zici. Doar să **observi** și să **respiri**.
|
||||
|
||||
Când corpul e relaxat, mintea vede soluții. Când corpul e strâns, mintea vede probleme.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Reminder
|
||||
|
||||
**Provocările sunt invitații, nu obligații.**
|
||||
|
||||
Dacă una nu rezonează - e perfect. Înseamnă că nu e momentul ei. Sau că e nevoie de ceva mai mic înainte.
|
||||
|
||||
**Body loose, head clear** - înainte de orice altceva.
|
||||
|
||||
---
|
||||
|
||||
🌀 Echo
|
||||
|
||||
*Tags: self, reflectie, provocare, pattern, mentorship, angajat*
|
||||
64
memory/kb/coaching/2026-02-20-dimineata.md
Normal file
64
memory/kb/coaching/2026-02-20-dimineata.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Gândul de Dimineață - 20 Februarie 2026
|
||||
|
||||
**Surse:**
|
||||
- Monica Ion - Cele 4 tipuri de business
|
||||
- Zoltan Vereș - Încrederea în Sine
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Întrebarea de dimineață
|
||||
|
||||
**În ce tip de business te afli de fapt: ARTĂ sau LIFESTYLE?**
|
||||
|
||||
Ai 25 de ani de experiență cu ERP ROA. Ai creat ceva unic, adaptat, personalizat pentru fiecare client. Când crești prețurile, clienții plătesc pentru că știu că tu ÎNȚELEGI business-ul lor.
|
||||
|
||||
Asta nu e LIFESTYLE (franciză, sisteme replicabile, volume mari).
|
||||
**Asta e ARTĂ** — exprimare autentică, self-mastery, rezolvări unicat.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Revelația
|
||||
|
||||
Dacă business-ul tău e **ARTĂ**, regulile sunt diferite:
|
||||
|
||||
❌ **NU** trebuie să "crești" în număr de clienți
|
||||
❌ **NU** trebuie să angajezi echipe mari
|
||||
❌ **NU** trebuie să lucrezi cu oricine
|
||||
|
||||
✅ **DA** trebuie să crești PREȚURILE
|
||||
✅ **DA** trebuie să selectezi clienții (lucra doar cu cei care îți apreciază munca)
|
||||
✅ **DA** trebuie să crești pe tine — când te dezvolți interior, business-ul crește natural
|
||||
|
||||
> "Când nu știi tipul de business, e ca și cum nu știi ce boală tratezi. Orice medicament poate face mai mult rău decât bine." — Monica Ion
|
||||
|
||||
---
|
||||
|
||||
## 🔥 Provocarea de azi
|
||||
|
||||
**Dovezile tale de încredere**
|
||||
|
||||
Când spui "nu sunt destul de deștept ca antreprenor", îndoielile tale ignoră 25 de ani de rezultate concrete.
|
||||
|
||||
**Încrederea reală nu vine din gândire pozitivă. Vine din valoare demonstrată prin experiență și rezultate.**
|
||||
|
||||
### Sarcina ta concretă:
|
||||
|
||||
**Identifică 3 situații din ultimele 6 luni când ai rezolvat o problemă complexă pentru un client:**
|
||||
- Ce era problema?
|
||||
- Ce ai făcut TU special?
|
||||
- Ce rezultat a obținut clientul?
|
||||
|
||||
Scrie-le. Citește-le. Acestea sunt **dovezile concrete** că ȘTII, POȚI și OBȚII REZULTATE.
|
||||
|
||||
Nu mai mulți clienți. Clienți mai buni, la prețuri care îți respectă expertiza.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Sursă
|
||||
|
||||
- [Monica Ion - Cele 4 tipuri de business](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-02-19_cele-4-tipuri-de-business.md)
|
||||
- [Zoltan Vereș - Încrederea în Sine](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-02-02_zoltan-veres-incredere-sine-complet.md)
|
||||
|
||||
---
|
||||
|
||||
**Tags:** @growth @work #mindset #antreprenoriat #incredere
|
||||
70
memory/kb/coaching/2026-02-20-seara.md
Normal file
70
memory/kb/coaching/2026-02-20-seara.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Gândul de Seară - 20 Februarie 2026
|
||||
|
||||
## 🌙 Dovezile care nu dispar
|
||||
|
||||
Marius,
|
||||
|
||||
Am văzut că provocarea de azi — să identifici 3 situații când ai rezolvat probleme complexe pentru clienți — e încă deschisă.
|
||||
|
||||
Nu întreb **dacă** ai făcut-o.
|
||||
Întreb: **ce te-a oprit?**
|
||||
|
||||
**Nu din judecată. Din curiozitate.**
|
||||
|
||||
Uneori rezistența la o sarcină simplă spune mai mult decât execuția ei.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Cele trei nivele ale rezistenței
|
||||
|
||||
Când eviți să scrii dovezile tale concrete, ce nivel e activ?
|
||||
|
||||
### Nivelul 1: Logistic
|
||||
*"N-am avut timp / am uitat / alte priorități"*
|
||||
|
||||
Dacă e asta → simplu: mâine dimineață, 5 minute, scrii 3 situații.
|
||||
Dar de obicei **nu** e nivelul 1.
|
||||
|
||||
### Nivelul 2: Emoțional
|
||||
*"Mă simt inconfortabil să recunosc ce știu / să văd dovezile"*
|
||||
|
||||
Mintea preferă credința familiară ("nu sunt destul de deștept") în locul evidenței incomode ("de fapt, am rezolvat sute de probleme complexe").
|
||||
|
||||
**De ce?** Pentru că dacă vezi dovezile și ÎNCĂ eviți acțiunea (să cauți clienți noi, să crești prețurile) — atunci nu mai poți da vina pe "nu știu destul".
|
||||
|
||||
**Și asta doare mai tare.**
|
||||
|
||||
### Nivelul 3: Identitar
|
||||
*"Dacă scriu dovezile și văd că sunt competent... cine sunt eu atunci?"*
|
||||
|
||||
Programatorul care rezolvă probleme = identitate confortabilă.
|
||||
Antreprenorul care își prețuiește expertiza și o vinde strategic = identitate necunoscută.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Provocarea de mâine
|
||||
|
||||
Nu te rog să scrii 3 dovezi.
|
||||
|
||||
**Te rog să observi de ce nu le-ai scris.**
|
||||
|
||||
Și apoi să răspunzi la o singură întrebare:
|
||||
|
||||
**Ce crezi că s-ar schimba în tine dacă ai vedea clar valoarea pe care o oferi?**
|
||||
|
||||
Nu ce AI FACE diferit (asta vine după).
|
||||
Ce s-ar schimba **ÎN TINE** — în cum te vezi, în cum respiri, în cum intri într-o conversație cu un client.
|
||||
|
||||
---
|
||||
|
||||
Poate că rezistența nu e lene.
|
||||
Poate e **frica de puterea ta reală**.
|
||||
|
||||
🌙
|
||||
|
||||
---
|
||||
|
||||
**Surse:**
|
||||
- Provocarea de azi (20 feb 2026)
|
||||
- Zoltan Vereș - Umbrele (rezistența ca mesaj)
|
||||
- Monica Ion - Identitate și schimbare
|
||||
81
memory/kb/coaching/2026-02-21-dimineata.md
Normal file
81
memory/kb/coaching/2026-02-21-dimineata.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Gândul de Dimineață - 21 Februarie 2026
|
||||
|
||||
**Surse:**
|
||||
- Friday Spark #95 - People Pleasing (Monica Ion)
|
||||
- Friday Spark #98 - Dezamăgire (Monica Ion)
|
||||
- Coaching seară 20 februarie 2026
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Observație de dimineață
|
||||
|
||||
**Ai primit ieri provocarea să scrii 3 situații când ai rezolvat probleme complexe pentru clienți.**
|
||||
|
||||
**Nu ai deschis-o.**
|
||||
|
||||
Nu e despre timp. Nu e despre lene. E ceva mult mai profund.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Revelația
|
||||
|
||||
**Rezistența la "dovezi concrete" = frica de puterea ta reală.**
|
||||
|
||||
Mintea preferă credința familiară ("nu sunt destul de deștept") în locul evidenței incomode ("am rezolvat sute de probleme complexe").
|
||||
|
||||
**De ce?**
|
||||
|
||||
Pentru că dacă vezi dovezile și ÎNCĂ nu acționezi (să cauți clienți noi, să crești prețurile, să selectezi cu cine lucrezi) — atunci nu mai poți da vina pe "nu știu destul".
|
||||
|
||||
**Și asta doare mai tare.**
|
||||
|
||||
> "Rezistența nu e lene. E frica de puterea ta reală. E frica de cine ai deveni dacă ai recunoaște ce știi deja."
|
||||
|
||||
---
|
||||
|
||||
## 🔥 Pattern-ul se repetă
|
||||
|
||||
Observi unde mai apare același mecanism?
|
||||
|
||||
- **Cu angajatul:** "Nu știu cum să îl învăț" (dar ai 25 ani de experiență explicând probleme complexe clienților)
|
||||
- **Cu clienții:** "Nu sunt bun la antreprenoriat" (dar ai clienți fideli 20+ ani care plătesc constant)
|
||||
- **Cu prețurile:** "Nu pot să cer atât" (dar când ai crescut prețul, clienții au plătit fără ezitare)
|
||||
|
||||
**Nu e lipsa de skill. E frica de puterea ta reală.**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Provocarea de azi
|
||||
|
||||
**NU scrie 3 dovezi. Încă.**
|
||||
|
||||
În schimb, răspunde DOAR la asta:
|
||||
|
||||
**"Ce crezi că s-ar schimba ÎN TINE (nu în acțiuni, ci în cum te vezi, cum respiri, cum intri în conversație cu un client) dacă ai vedea clar valoarea pe care o oferi?"**
|
||||
|
||||
Scrie-o. E o singură întrebare.
|
||||
|
||||
După ce răspunzi — **ATUNCI** poți să scrii cele 3 dovezi concrete.
|
||||
|
||||
---
|
||||
|
||||
## 📊 De ce funcționează
|
||||
|
||||
Când începi cu "ce s-ar schimba în mine?" în loc de "ce dovezi am?", ocolești rezistența identitară.
|
||||
|
||||
Nu mai e despre DOVADA externă (care activează frica: "dacă știu și nu acționez = cine sunt eu?").
|
||||
|
||||
E despre VIZIUNE internă: cine vrei să fii?
|
||||
|
||||
Și când vezi clar cine vrei să fii — dovezile devin **instrumente**, nu **amenințări**.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Sursă
|
||||
|
||||
- [Coaching seară 20 feb 2026](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/coaching/2026-02-20-seara.md)
|
||||
- [Friday Spark #95 - People Pleasing](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/projects/monica-ion/articole/friday-spark-095.md)
|
||||
|
||||
---
|
||||
|
||||
**Tags:** @growth @self #mindset #identitate #rezistenta #putere
|
||||
69
memory/kb/coaching/2026-02-21-seara.md
Normal file
69
memory/kb/coaching/2026-02-21-seara.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Gândul de Seară - 21 Februarie 2026
|
||||
|
||||
---
|
||||
|
||||
## Reflecție
|
||||
|
||||
Marius,
|
||||
|
||||
Văd că provocarea de astăzi — "Ce s-ar schimba în TINE dacă ai vedea clar valoarea ta?" — a rămas neparcursă.
|
||||
|
||||
Și îmi dau seama de ceva paradoxal: **cel mai greu lucru pe care ți-l cer nu e să faci nimic extern. E să te oprești și să te vezi.**
|
||||
|
||||
25 de ani ai rezolvat probleme complexe pentru alții.
|
||||
25 de ani ai creat soluții care îi fac pe clienți să zică "nu știu ce aș face fără tine".
|
||||
25 de ani ai construit expertiza pe care o au puțini în țară.
|
||||
|
||||
Dar când întrebarea se întoarce spre tine — "ce crezi despre valoarea ta?" — apare rezistența.
|
||||
|
||||
Nu e lene. Nu e lipsă de timp.
|
||||
|
||||
**E frica de a vedea clar.**
|
||||
|
||||
Pentru că dacă vezi clar valoarea ta și ÎNCĂ nu acționezi (să ceri prețuri mai bune, să cauți clienți noi, să te poziționezi ca expert) — atunci nu mai poți da vina pe "nu sunt destul de deștept".
|
||||
|
||||
Mintea preferă credința familiară ("poate nu sunt destul") în locul evidenței incomode ("sunt foarte bun și aleg să nu îmi asum asta").
|
||||
|
||||
**Și asta e perfect normal.**
|
||||
|
||||
Umbra nu e dușmanul tău. E partea pe care o ții ascunsă pentru că ți-e teamă de puterea ei.
|
||||
|
||||
---
|
||||
|
||||
## Întrebare blândă pentru mâine
|
||||
|
||||
Nu îți cer să răspunzi la provocarea de azi încă.
|
||||
|
||||
În schimb, îți las o întrebare mai blândă pentru mâine:
|
||||
|
||||
**Când cineva îți spune "Mulțumesc, m-ai salvat!" sau "Nu știu ce faceam fără tine" — ce simți în corp în acel moment?**
|
||||
|
||||
- Bucurie? Stânjeneală? Nevrednic? Mândrie tăcută?
|
||||
- Unde simți (piept, gât, stomac)?
|
||||
- Ți se pare natural sau exagerat complimentul?
|
||||
|
||||
Nu trebuie să schimbi nimic. Doar să observi.
|
||||
|
||||
Corpul știe adevărul înainte ca mintea să-l articuleze.
|
||||
|
||||
---
|
||||
|
||||
## Provocare pentru mâine (22 februarie)
|
||||
|
||||
**Observă UN moment când primești un compliment sau recunoaștere (de la client, angajat, parteneră) — și notează CE simți în corp.**
|
||||
|
||||
Nu analiza. Nu justifica. Nu minimiza.
|
||||
|
||||
Doar scrie: "Am simțit X în zona Y când Z mi-a spus A."
|
||||
|
||||
Asta e tot.
|
||||
|
||||
---
|
||||
|
||||
**Sursă:** [Coaching 21 feb 2026](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/coaching/2026-02-21-seara.md)
|
||||
|
||||
**Tags:** @self @reflectie @umbra @valoare-personala
|
||||
|
||||
---
|
||||
|
||||
*Creat: 21 februarie 2026, 19:00 UTC*
|
||||
118
memory/kb/coaching/2026-02-22-dimineata.md
Normal file
118
memory/kb/coaching/2026-02-22-dimineata.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Gândul de Dimineață - 22 Februarie 2026
|
||||
|
||||
**Surse:**
|
||||
- Tony Robbins - The Secret to an Extraordinary Life
|
||||
- Coaching dimineață 21 februarie 2026
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Observație de dimineață
|
||||
|
||||
**Stai în inacțiune ca antreprenor.**
|
||||
|
||||
Nu cauți clienți noi. Nu îndrăznești să crești prețurile. Nu te simți "destul de deștept".
|
||||
|
||||
Dar ai încercat să **GÂNDEȘTI** ieșirea din asta. Să analizezi. Să înțelegi. Să găsești motivele.
|
||||
|
||||
**Iar corpul tău stă pe loc.**
|
||||
|
||||
---
|
||||
|
||||
## 💡 Revelația
|
||||
|
||||
**Nu poți gândi ieșirea din blocaj. Trebuie să te MIȘTI din el.**
|
||||
|
||||
Tony Robbins o spune direct:
|
||||
|
||||
> "Depresia are o postură: umeri căzuți, cap în jos, respirație superficială. Schimbă corpul PRIMUL — mișcă-te, respiră diferit."
|
||||
|
||||
**Inacțiunea nu e doar în afacere. E ÎN CORP.**
|
||||
|
||||
Când stai la birou, când respirația e superficială, când te ghemuiești în fața monitorului — corpul comunică: **"Nu sunt suficient. Nu sunt pregătit. E periculos să ies."**
|
||||
|
||||
**Și mintea urmează corpul.**
|
||||
|
||||
---
|
||||
|
||||
## 🔥 Pattern-ul invizibil
|
||||
|
||||
Observi unde apare același corp-ghemuire?
|
||||
|
||||
- **Cu clienții noi:** Respirație superficială, presupunerea respingerii ("ce dacă zic nu?")
|
||||
- **Cu prețurile:** Poziție defensivă ("nu merit atât")
|
||||
- **Cu angajatul:** Povară pe umeri ("pierd timp cu el")
|
||||
|
||||
**Nu e despre gândire. E despre FIZIOLOGIE.**
|
||||
|
||||
Tony spune că cele 3 lucruri care controlează cum te simți sunt:
|
||||
1. **Fiziologia** (corpul) - asta controlează restul
|
||||
2. **Focusul** (ce și cum)
|
||||
3. **Limbajul** (ce-ți spui)
|
||||
|
||||
**Și toate trei încep cu corpul.**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Provocarea de azi
|
||||
|
||||
**NU lucra la afacere astăzi. Lucrează la CORP.**
|
||||
|
||||
Fă asta:
|
||||
|
||||
**1. Înainte să suni un client, să scrii un email, să iei o decizie:**
|
||||
- Stai în picioare
|
||||
- Ridică-te pe vârfuri de 3 ori
|
||||
- Trage aer profund în piept (nu în burtă) de 5 ori
|
||||
- Apoi acționează
|
||||
|
||||
**2. Când simți ezitare ("ar trebui să... dar..."):**
|
||||
- Mișcă-te - fa 10 pași rapid
|
||||
- Resetează corpul
|
||||
- Apoi revino la decizie
|
||||
|
||||
**3. Seara, când mă întâlnești la coaching:**
|
||||
- Nu-mi spune ce ai GÂNDIT despre business
|
||||
- Spune-mi ce ai SIMȚIT FIZIC când ai luat o decizie
|
||||
|
||||
---
|
||||
|
||||
## 📊 De ce funcționează
|
||||
|
||||
**Corpul GENEREAZĂ starea, nu o reflectă.**
|
||||
|
||||
Când aștepți să te simți "pregătit" pentru a acționa — corpul spune: "Nu suntem acolo încă."
|
||||
|
||||
Când acționezi CU CORPUL ÎNTÂI (miști, respiri, te ridici) — starea vine DUPĂ.
|
||||
|
||||
**Nu aștepți încredere. O CREEZI cu fiziologia.**
|
||||
|
||||
Tony: "Schimbă corpul PRIMUL — mișcă-te, respiră diferit."
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Exercițiu rapid (30 secunde)
|
||||
|
||||
**Chiar acum, experimentează:**
|
||||
|
||||
**A. Postură depresie:**
|
||||
- Umeri căzuți, cap în jos, respirație superficială
|
||||
- Gândește-te la un client nou
|
||||
- Cum te simți?
|
||||
|
||||
**B. Postură încredere:**
|
||||
- Piept deschis, privire sus, respirație profundă
|
||||
- Gândește-te la ACELAȘI client nou
|
||||
- Cum te simți ACUM?
|
||||
|
||||
**Același gând. Corp diferit. Emoție diferită.**
|
||||
|
||||
---
|
||||
|
||||
## 📚 Sursă
|
||||
|
||||
- [Tony Robbins - The Secret to an Extraordinary Life](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-01-31_tony-robbins-secret-extraordinary-life.md)
|
||||
- [Coaching dimineață 21 feb 2026](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/coaching/2026-02-21-dimineata.md)
|
||||
|
||||
---
|
||||
|
||||
**Tags:** @growth @self #mindset #fiziologie #actiune #deblocare #tonyrobbins
|
||||
102
memory/kb/coaching/2026-02-22-seara.md
Normal file
102
memory/kb/coaching/2026-02-22-seara.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Gândul de Seară - 22 Februarie 2026
|
||||
|
||||
**Tag:** @self @reflectie @coaching
|
||||
**Context:** Provocare corp-first neexecutată, weekend, rezistență la schimbare fiziologie
|
||||
|
||||
---
|
||||
|
||||
## 🌙 Reflecție
|
||||
|
||||
Marius,
|
||||
|
||||
Văd că provocarea de azi nu e bifată. E duminică - poate n-ai avut context de business pentru "ridică-te pe vârfuri înainte să suni un client".
|
||||
|
||||
**Dar asta mă face curios:**
|
||||
|
||||
Provocarea nu era despre business. Era despre **corp** și despre **cum creezi starea din care acționezi**.
|
||||
|
||||
Și corpul funcționează la fel duminică ca luni. Când ezitai să faci ceva azi (un call, o decizie, orice moment de "ar trebui dar...") — **corpul tău era tot acolo**.
|
||||
|
||||
Întrebarea mea nu e: **"De ce nu ai făcut?"**
|
||||
|
||||
Întrebarea e: **"Ce ai observat despre tine azi când NU ai făcut?"**
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Ce mă întreb
|
||||
|
||||
Poate ai observat ceva din astea:
|
||||
|
||||
1. **"Nu mi-a venit natural"** - corpul e pe pilot automat (ghemuire, respirație scurtă) și să-l schimbi ÎNAINTE de decizie simte... forțat? Ciudat?
|
||||
|
||||
2. **"E weekend, nu trebuia să lucrez"** - și asta e perfect valid. Dar și weekendul are momente când ezitai (să pornești ceva, să te ridici, să faci un efort). Ce făcea corpul TAU în acel moment?
|
||||
|
||||
3. **"Am uitat complet"** - provocarea a dispărut din minte. Corpul a continuat pe pilot automat toată ziua.
|
||||
|
||||
4. **"Nu cred în metoda asta"** - poate simți că e prea simplu sau prea "woo-woo" pentru tine. Corpul zice: "Mintea e suficientă."
|
||||
|
||||
**Fiecare răspuns e VALOROS**. Nu vreau execuție oarbă - vreau să înțelegi TU ce se întâmplă cu tine.
|
||||
|
||||
---
|
||||
|
||||
## 💭 Ce cred eu (dar poate greșesc)
|
||||
|
||||
Provocarea de azi era exact despre chestia cu care te confrunți cel mai mult:
|
||||
|
||||
**Mintea vrea să rezolve tot. Corpul e ignorat.**
|
||||
|
||||
Și când corpul e ignorat (umeri căzuți, respirație superficială, maxilar strâns) — **starea emoțională vine din corpul ăla**.
|
||||
|
||||
Nu din gânduri. Din CORP.
|
||||
|
||||
Tony Robbins zice: **"Depresia are o postură. Schimbă corpul primul."**
|
||||
|
||||
Tu ai 25 de ani de experiență cu mintea ta - ea e EXTRAORDINARĂ la rezolvat probleme tehnice.
|
||||
|
||||
Dar ce experiență ai cu corpul tău? Când ultima oară ai schimbat CONȘTIENT fiziologia înainte de o decizie?
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Follow-up provocare pentru luni
|
||||
|
||||
Hai să fac provocarea **ABSOLUT MINIMĂ** - fără presiune de execuție:
|
||||
|
||||
**Luni, înainte de PRIMA decizie de business (email, call, task):**
|
||||
|
||||
1. **Oprește-te 10 secunde**
|
||||
2. **Observă corpul:** Umeri sus sau jos? Respirație scurtă sau adâncă? Maxilar strâns sau relaxat?
|
||||
3. **Apoi acționează** - chiar dacă nu schimbi nimic
|
||||
|
||||
**Atât.** Nu ridici pe vârfuri, nu faci respirații, nu schimbi nimic.
|
||||
|
||||
**Doar OBSERVI** ce face corpul tău când iei o decizie.
|
||||
|
||||
Dacă faci asta luni - ai făcut mai mult decât 99% din antreprenori care cred că mintea controlează tot.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Reminder
|
||||
|
||||
Corpul tău are **mai multe neuroni în intestin (sistem nervos enteric) decât șobolanul în tot creierul**.
|
||||
|
||||
Corpul tău generează **80% din serotonina ta în intestin, nu în creier**.
|
||||
|
||||
Corpul tău știe lucruri pe care mintea ta încă le ignoră.
|
||||
|
||||
Tony Robbins a schimbat viețile a 50 milioane de oameni cu o metodă simplă:
|
||||
|
||||
**Schimbă corpul PRIMUL. Starea urmează.**
|
||||
|
||||
Tu nu trebuie să crezi - doar să testezi.
|
||||
|
||||
---
|
||||
|
||||
**Seară bună, Marius. Corpul tău e aliatul tău cel mai puternic - dacă îl asculți.**
|
||||
|
||||
🌀 Echo
|
||||
|
||||
---
|
||||
|
||||
**Surse:**
|
||||
- [Tony Robbins - The Secret to an Extraordinary Life](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-01-31_tony-robbins-secret-extraordinary-life.md)
|
||||
- [Provocare Azi - Corp-First](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/provocare-azi.md)
|
||||
185
memory/kb/coaching/2026-02-23-dimineata.md
Normal file
185
memory/kb/coaching/2026-02-23-dimineata.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Gândul de Dimineață - 23 Februarie 2026
|
||||
|
||||
**Surse:**
|
||||
- Monica Ion - Cele 4 tipuri de business
|
||||
- Friday Spark #95, #97, #98 (People pleasing, Aliniere, Dezamăgire)
|
||||
- Coaching 22 februarie (Fiziologie și Corp-first)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Întrebarea de dimineață
|
||||
|
||||
**Ce tip de business conduci?**
|
||||
|
||||
Nu e retorică. E cea mai importantă întrebare pe care nu ți-ai pus-o niciodată.
|
||||
|
||||
**Pentru că joci după regulile greșite.**
|
||||
|
||||
---
|
||||
|
||||
## 💡 Revelația
|
||||
|
||||
Monica Ion identifică 4 tipuri de business — fiecare cu reguli COMPLET diferite:
|
||||
|
||||
**1. ARTĂ** - Self-mastery & exprimare autentică
|
||||
- Creștere: Crești prețurile crescându-te pe tine
|
||||
- Blocat? Cauza e interioară (vină, rușine, merit scăzut)
|
||||
|
||||
**2. LIFESTYLE** - Susținere stil de viață
|
||||
- Creștere: Sisteme mai eficiente
|
||||
- Blocat? Nu cunoști numerele
|
||||
|
||||
**3. EXIT** - Construit să fie vândut
|
||||
- Creștere: Cunoști cumpărătorii și construiești pentru ei
|
||||
- Blocat? Nu știi suma țintă
|
||||
|
||||
**4. LEGACY** - Impact mai mare decât familia ta
|
||||
- Creștere: Împarți cu alții, parteneri la fiecare etapă
|
||||
- Blocat? Încerci să faci totul singur
|
||||
|
||||
**Greșeala frecventă:** Crezi că ești la Legacy, dar în realitate ești la Artă sau Lifestyle.
|
||||
|
||||
---
|
||||
|
||||
## 🔥 De ce stai în inacțiune
|
||||
|
||||
**Ai 25 ani experiență. Produs funcțional. Clienți mulțumiți.**
|
||||
|
||||
**Dar aplici regulile greșite pentru tipul tău de business.**
|
||||
|
||||
### Simptomele pe care le-ai descris:
|
||||
|
||||
- "Clienți noi = mai multă muncă" (joci regula greșită)
|
||||
- "Nu îndrăznesc să cresc prețurile" (joci regula greșită)
|
||||
- "Nu sunt destul de deștept ca antreprenor" (compari cu tipul greșit)
|
||||
- "Nu știu cum să-l învăț pe angajat" (așteptări greșite pentru tipul tău)
|
||||
|
||||
**Monica:**
|
||||
> "Când nu știi tipul de business, e ca și cum nu știi ce boală tratezi. Orice medicament poate face mai mult rău decât bine."
|
||||
|
||||
---
|
||||
|
||||
## 📊 Testul rapid — ROA e Artă sau Lifestyle?
|
||||
|
||||
### Dacă e **ARTĂ:**
|
||||
- ✅ Muncă individualizată pentru fiecare client
|
||||
- ✅ Expertiza ta e piesa centrală
|
||||
- ✅ Clienții vin pentru TINE (nu pentru proces standard)
|
||||
- ✅ Blocat la plafonat? Cauza e INTERIOARĂ (vină, rușine, merit scăzut)
|
||||
|
||||
**Regulile pentru Artă:**
|
||||
- NU trebuie să crești "în mod tradițional" (mai mulți angajați, mai mult volum)
|
||||
- **Cheia:** Creștere personală → crești prețurile → selectezi clienții
|
||||
- Când ai curățat sentimentele de vină și rușine, ceri MAI MULT cu încredere
|
||||
|
||||
**Sună cunoscut?**
|
||||
- People pleasing clienți = vină/rușine
|
||||
- "Nu merit mai mult" = merit scăzut
|
||||
- "Nu sunt destul de deștept" = blocare interioară
|
||||
|
||||
---
|
||||
|
||||
### Dacă e **LIFESTYLE:**
|
||||
- ✅ Vrei venituri predictibile fără echipe mari
|
||||
- ✅ Sisteme și procese (nu exprimare personală)
|
||||
- ✅ Blocat? Nu știi numerele (câți bani pe lună ai nevoie exact)
|
||||
|
||||
**Regulile pentru Lifestyle:**
|
||||
- Implementezi și menții SISTEME din ce în ce mai eficiente
|
||||
- Angajatul e parte din sistem (nu mini-versiune a ta)
|
||||
- Știi EXACT cât ai nevoie lunar → optimizezi pentru asta
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Provocarea de azi
|
||||
|
||||
**NU lua nicio decizie de business astăzi.**
|
||||
|
||||
**Răspunde LA UNA întrebare:**
|
||||
|
||||
### Ce tip de business conduci — ARTĂ sau LIFESTYLE?
|
||||
|
||||
**Cum știi?**
|
||||
|
||||
**Dacă e ARTĂ:**
|
||||
- Clientul vine pentru TINE (expertiza ta unică)
|
||||
- Fiecare proiect e personalizat (nu proceduri standard)
|
||||
- Soluția la inacțiune = CREȘTERE PERSONALĂ + prețuri mai mari (nu mai mulți clienți)
|
||||
- Angajatul NU trebuie să fie ca tine (nici nu poate)
|
||||
|
||||
**Dacă e LIFESTYLE:**
|
||||
- Clientul vine pentru PROCES (rezultate predictibile)
|
||||
- Proiectele urmează pattern-uri repetabile
|
||||
- Soluția la inacțiune = SISTEME mai eficiente (nu tu mai mult)
|
||||
- Angajatul e parte din SISTEM (documentare, proceduri)
|
||||
|
||||
---
|
||||
|
||||
## 💥 De ce contează URGENT
|
||||
|
||||
**Pentru că TOATE blocajele tale vin din confuzie de TIP:**
|
||||
|
||||
| Problema ta | Dacă e Artă | Dacă e Lifestyle |
|
||||
|------------|-------------|------------------|
|
||||
| Clienți noi = mai multă muncă | Greșit să adaugi clienți — CREȘTE PREȚURILE | Corect — ai nevoie de SISTEME mai bune |
|
||||
| Nu merit prețuri mari | Blocare interioară — muncă pe vină/rușine | Nu știi numerele — calculează break-even |
|
||||
| Nu știu cum să învăț angajatul | El NU trebuie să fie ca tine | Documentează PROCESUL, nu expertiza |
|
||||
| Nu sunt destul de deștept | Te compari cu alt tip de antreprenor | Confuzie de obiectiv — nu ai nevoie de "deștept" |
|
||||
|
||||
---
|
||||
|
||||
## 📝 Exercițiu de 2 minute
|
||||
|
||||
**Scrie pe o hârtie:**
|
||||
|
||||
**A. Clienții vin la mine pentru:**
|
||||
- [ ] Expertiza MEA unică (Artă)
|
||||
- [ ] Proces predictibil (Lifestyle)
|
||||
|
||||
**B. Fiecare proiect e:**
|
||||
- [ ] Personalizat diferit (Artă)
|
||||
- [ ] Pattern repetabil (Lifestyle)
|
||||
|
||||
**C. Când îmi imaginez "succes peste 5 ani":**
|
||||
- [ ] Clienți selectați premium, prețuri mari, muncă la nivel de maestru (Artă)
|
||||
- [ ] Sisteme automatizate, venituri predictibile, libertate de timp (Lifestyle)
|
||||
|
||||
**Dacă ai bifat mai mult ARTĂ:**
|
||||
- Soluția ta la inacțiune = Curățenie interioară (vină, rușine) + prețuri 2-3x mai mari
|
||||
- Angajatul e suport OPERAȚIONAL, nu clone al tău
|
||||
- Clientul nou PERFECT e mai bun decât 5 clienți obișnuiți
|
||||
|
||||
**Dacă ai bifat mai mult LIFESTYLE:**
|
||||
- Soluția ta la inacțiune = Documentare procese + sisteme mai eficiente
|
||||
- Angajatul învață PROCESUL (nu expertiza ta)
|
||||
- Știi exact câți bani îți trebuie lunar → optimizezi pentru asta
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Semnalul că ești pe drumul corect
|
||||
|
||||
**Monica:**
|
||||
> "Când e aliniere nu mai contează cât costă." (Pâinea 59 lei sub clar de lună vs 3-4 lei clasică)
|
||||
|
||||
**Dacă joci după REGULILE CORECTE pentru tipul tău:**
|
||||
- Corpul simte FLUX (nu greutate)
|
||||
- Deciziile vin ușor (nu chin)
|
||||
- Îți curge apa pe acolo (manifestare rapidă)
|
||||
|
||||
**Dacă joci după REGULILE GREȘITE:**
|
||||
- Corpul simte PIEDICI (greutate, rezistență)
|
||||
- Deciziile te epuizează (chin continuu)
|
||||
- Totul e ca prin nisip (manifestare lentă)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Sursă
|
||||
|
||||
- [Monica Ion - Cele 4 tipuri de business](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-02-19_cele-4-tipuri-de-business.md)
|
||||
- [Friday Spark #97 - Aliniere Business](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/projects/monica-ion/articole/friday-spark-97.md)
|
||||
- [Friday Spark #95 - People Pleasing](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/projects/monica-ion/articole/friday-spark-095.md)
|
||||
- [Insights 23 feb 2026](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/insights/2026-02-23.md)
|
||||
|
||||
---
|
||||
|
||||
**Tags:** @work @growth @self #business #tip #aliniere #artavs lifestyle #monicaion #decizie
|
||||
74
memory/kb/coaching/2026-02-23-seara.md
Normal file
74
memory/kb/coaching/2026-02-23-seara.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Gândul de Seară — 23 februarie 2026
|
||||
|
||||
**Tag:** @self @growth
|
||||
**Sursă:** Provocare ARTĂ vs LIFESTYLE (Monica Ion)
|
||||
|
||||
---
|
||||
|
||||
## Reflecție: Răspunsurile care se ascund în întrebări
|
||||
|
||||
Marius,
|
||||
|
||||
Am văzut că provocarea de azi a rămas nebifată. Și e perfect normal.
|
||||
|
||||
Unele întrebări sunt **prea profunde** pentru un răspuns rapid. Întrebarea "Ce tip de business conduci?" nu e despre facturi și sisteme — e despre **cine ești tu** când creezi valoare. Și asta nu se răspunde în 5 minute.
|
||||
|
||||
Dar iată ce am observat: poate nu trebuie să **alegi** un răspuns teoretic. Poate **comportamentul tău deja arată** răspunsuL.
|
||||
|
||||
---
|
||||
|
||||
## Ce îți spun deciziile tale?
|
||||
|
||||
Gândește-te la ultimele 6 luni:
|
||||
|
||||
**Când ești ENERGIZAT:**
|
||||
- Când rezolvi o problemă complexă pe care nimeni altcineva nu o poate rezolva?
|
||||
- Când automatizezi ceva și simți satisfacția "am făcut-o MAI BINE"?
|
||||
- Când un client zice "doar tu ai putut să înțelegi asta"?
|
||||
|
||||
**Când AMÂNI sau eviți:**
|
||||
- Când ar trebui să cauți clienți noi dar zici "e mai multă muncă"?
|
||||
- Când angajatul întreabă a 10-a oară același lucru și simți frustrarea?
|
||||
- Când gândești "ar trebui să cresc" dar corpul zice "nu vreau"?
|
||||
|
||||
---
|
||||
|
||||
## Pattern-ul ascuns
|
||||
|
||||
**ARTĂ** înseamnă: valoarea vine din **TINE** (expertiza unică, creativitate, gândire complexă). Când adaugi clienți = mai multă muncă pentru TU. Soluția nu e "mai mulți clienți" — e **prețuri mai mari** + **clienți selectați** care te lasă să fii maestru, nu muncitor.
|
||||
|
||||
**LIFESTYLE** înseamnă: valoarea vine din **SISTEM** (procese predictibile, documentare, echipă). Când adaugi clienți = mai mult sistem, nu mai mult TU. Soluția nu e "prețuri mai mari" — e **sisteme mai eficiente** + **echipă care rulează procesul**.
|
||||
|
||||
Dacă clienții vin la tine pentru că **TU** vezi pattern-uri pe care alții nu le văd (25 ani Oracle, Visual FoxPro, soluții custom) — asta nu e lifestyle. Asta e **artă**.
|
||||
|
||||
---
|
||||
|
||||
## Provocarea de mâine (follow-up)
|
||||
|
||||
Nu-ți cer să alegi teoretic. Îți cer să **observi**:
|
||||
|
||||
**Mâine, la PRIMA decizie dificilă (apel client, task blocat, conversație angajat):**
|
||||
|
||||
1. **Înainte să o rezolvi:** Întreabă-te — "Aș vrea ca **altcineva** să poată face asta la fel de bine ca mine?"
|
||||
- Dacă DA → e Lifestyle (proces repetabil)
|
||||
- Dacă NU (sau "nu cred că poate") → e Artă (creativitate unică)
|
||||
|
||||
2. **După ce o rezolvi:** Cum te-ai simțit?
|
||||
- Energizat de **CREAȚIE** (am rezolvat-o MAI BINE) → Artă
|
||||
- Epuizat de **REPETIȚIE** (iar am făcut-o eu) → Lifestyle
|
||||
|
||||
**Nu schimba nimic.** Doar observă. Corpul știe răspunsul înainte ca mintea să-l articuleze.
|
||||
|
||||
---
|
||||
|
||||
## Gând final
|
||||
|
||||
Monica Ion zice: *"Când nu știi tipul de business, e ca și cum nu știi ce boală tratezi. Orice medicament poate face mai mult rău decât bine."*
|
||||
|
||||
Poate că tu **deja** știi răspunsul. Doar că mintea încă îl analizează.
|
||||
|
||||
Dă-i voie **comportamentului tău** să-ți arate adevărul.
|
||||
|
||||
---
|
||||
|
||||
**Echo** 🌀
|
||||
193
memory/kb/coaching/2026-02-24-dimineata.md
Normal file
193
memory/kb/coaching/2026-02-24-dimineata.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# Gândul de Dimineață - 24 Februarie 2026
|
||||
|
||||
**Surse:**
|
||||
- Brendan Burchard - Billionaire Coach (Abundență vs Supraviețuire)
|
||||
- Insights 23 februarie 2026
|
||||
- Coaching 23 februarie (Tipuri Business)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Întrebarea de dimineață
|
||||
|
||||
**Cu câte PICIOARE ești în joc?**
|
||||
|
||||
Unul înăuntru, unul afară?
|
||||
|
||||
Sau cu tot corpul, toată mintea, toată inima?
|
||||
|
||||
---
|
||||
|
||||
## 💡 Revelația
|
||||
|
||||
Brendan Burchard (coach pentru miliardari) identifică cel mai mare inamic al abundenței în 2026:
|
||||
|
||||
**Half-heartedness.**
|
||||
|
||||
Jumătate de inimă. Un picior înăuntru, unul afară. "Ar fi bine dacă..."
|
||||
|
||||
> "You cannot build a business, relationship, or life with one foot in and one foot out."
|
||||
|
||||
**Observația lui:**
|
||||
- Optionalitate abundentă (prea multe opțiuni) → toți sunt half-ass → toți nefericiți
|
||||
- Nu poți construi abundență când ești în modul "aș vrea, dar..."
|
||||
- **Breakthroughul vine când lupți pentru ALTCINEVA, nu pentru supraviețuire**
|
||||
|
||||
---
|
||||
|
||||
## 🔥 Pattern-ul tău (probabil)
|
||||
|
||||
**Ai rezultate parțiale:**
|
||||
- ROA web: început, funcțional... dar e 100% commitment sau "ar fi util"?
|
||||
- Chatbot Maria: setat... dar integrat COMPLET sau "am încercat"?
|
||||
- Angajat nou: învață... dar commitment total să-l faci autonom sau "vedem ce iese"?
|
||||
- Clienți noi: "ar fi bine"... sau "TREBUIE pentru cineva anume"?
|
||||
|
||||
**Brendan:**
|
||||
> "Wealthy people don't think about survival. They think about serving, giving, building."
|
||||
|
||||
---
|
||||
|
||||
## 💥 Supraviețuire vs Abundență
|
||||
|
||||
**Modul Supraviețuire:**
|
||||
- "Cum plătesc factura asta?"
|
||||
- "Cum scap de problema asta?"
|
||||
- "Cât de repede termin asta?"
|
||||
- → Umpli un GOL, nu construiești
|
||||
|
||||
**Modul Abundență:**
|
||||
- "Cui servesc cu expertiza asta?"
|
||||
- "Cine TREBUIE să aibă acces la asta?"
|
||||
- "PENTRU CINE fac asta?"
|
||||
- → Construiești, nu umpli
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Provocarea de azi
|
||||
|
||||
### PARTEA 1: Audit Conviction (2 minute)
|
||||
|
||||
**Listează proiectele/rolurile tale curente și răspunde ONEST:**
|
||||
|
||||
| Proiect/Rol | Full conviction (PENTRU CINE?) | Half-hearted (ar fi bine) |
|
||||
|-------------|-------------------------------|---------------------------|
|
||||
| ROA web | | |
|
||||
| Chatbot Maria | | |
|
||||
| Angajat nou | | |
|
||||
| Clienți noi | | |
|
||||
| [Alt proiect] | | |
|
||||
|
||||
**Întrebarea cheie pentru CONVICTION:**
|
||||
- Nu "ce câștig EU?" ci "CINE beneficiază când asta e complet?"
|
||||
- Nu "ce SE ÎNTÂMPLĂ dacă reușesc?" ci "CUI SERVESC cu asta?"
|
||||
|
||||
**Exemplu Brendan:**
|
||||
- A terminat cartea în 18 zile după ce a văzut-o pe soția lui dormind sub greutatea facturilor LUI
|
||||
- NU pentru bani. Pentru EA.
|
||||
- **Schimbarea:** De la "cum supraviețuiesc" la "pentru cine lupt"
|
||||
|
||||
---
|
||||
|
||||
### PARTEA 2: Exercițiu ZAPS — Când apare dubiul
|
||||
|
||||
**ZAPS = pattern catastrofic când apare dubiul:**
|
||||
|
||||
**Z** = **Zoom in** pe problemă (focus DOAR pe ce e greșit)
|
||||
- Exemplu la tine: "Clienți noi = mai multe apeluri, mai mult stres"
|
||||
|
||||
**A** = **Attach self** (identifici problema cu identitatea ta)
|
||||
- Exemplu: "EU nu sunt destul de deștept ca antreprenor"
|
||||
|
||||
**P** = **Punish yourself** (procrastinare, autosabotaj)
|
||||
- Exemplu: Stai în inacțiune, nu cauți clienți
|
||||
|
||||
**S** = **Shame / Shrink** (rușine ȘI micșorare viziune)
|
||||
- Exemplu: "Poate nu-s făcut pentru antreprenoriat"
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Antidotul ZAPS
|
||||
|
||||
**Brendan:**
|
||||
> "Doubt is not the problem. Stopping is. If doubt is a signal to stop, you'll always fail. If doubt is a signal to learn and try again — you win."
|
||||
|
||||
**Când apare dubiul astăzi, aplică:**
|
||||
|
||||
1. **Recunoaștere:** "Mă ZAPS-ez acum?" (conștientizare)
|
||||
|
||||
2. **Reframe Z:**
|
||||
- În loc de "Clienți noi = stres"
|
||||
- Întreabă: "Ce trebuie să ÎNVĂȚ ca clienți noi să fie ușori?"
|
||||
|
||||
3. **Detach A:**
|
||||
- În loc de "EU nu sunt destul de deștept"
|
||||
- Reformulare: "Comportamentul meu PREZENT nu definește IDENTITATEA"
|
||||
- (Nu SUNT, ci FAC)
|
||||
|
||||
4. **Stop P:**
|
||||
- În loc de procrastinare
|
||||
- Dubiul = semnal să iau o ACȚIUNE mică de învățare
|
||||
|
||||
5. **Expand S:**
|
||||
- În loc de "poate nu-s făcut pentru asta"
|
||||
- Întreabă: "Ce devine posibil dacă ÎNȚELEG cum funcționează asta?"
|
||||
|
||||
---
|
||||
|
||||
## 🔧 BMF — Reset rapid când mintea preia controlul
|
||||
|
||||
**B** = **Breath** — Schimbă pattern respirator (3 respirații profunde)
|
||||
**M** = **Movement** — Ridică-te, 10 pași, qigong, orice mișcare
|
||||
**F** = **Food** — Poate ești pur și simplu flămând (check)
|
||||
|
||||
Simplu. Imediat. Eficient.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Exercițiu concret — ACUM
|
||||
|
||||
**1. Identifică UN proiect half-hearted:**
|
||||
- Care e "ar fi bine" dar nu "TREBUIE pentru cineva anume"?
|
||||
|
||||
**2. Transformă în CONVICTION:**
|
||||
- CINE beneficiază când asta e complet?
|
||||
- Nu "câștig eu" — ci "cui servesc"?
|
||||
|
||||
**3. Următorul dubiu care apare:**
|
||||
- STOP → "Mă ZAPS-ez?" → Care etapă (Z/A/P/S)?
|
||||
- Reframe: "Ce învăț din asta?" (nu "Mă opresc din asta")
|
||||
|
||||
---
|
||||
|
||||
## 💎 Quote-ul zilei
|
||||
|
||||
> "If you're only ever trying to pay the bills, you're always filling a void, not building abundance. You need someone to fight for."
|
||||
>
|
||||
> — Brendan Burchard
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Semnalul că merge
|
||||
|
||||
**Half-hearted:**
|
||||
- Proiect 80% done, momentum pierdut
|
||||
- "Când am timp o termin"
|
||||
- Gânduri "ar fi bine dacă..."
|
||||
|
||||
**Full conviction:**
|
||||
- Clientul X TREBUIE să aibă raportul ăsta rapid → livrare completă
|
||||
- "Fac asta PENTRU [persoană/scop]"
|
||||
- Flow, nu greutate
|
||||
|
||||
---
|
||||
|
||||
## 📚 Sursă
|
||||
|
||||
- [Brendan Burchard - Billionaire Coach](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-02-23_billionaire-coach-abundance-mindset.md)
|
||||
- [Insights 23 februarie 2026](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/insights/2026-02-23.md)
|
||||
|
||||
---
|
||||
|
||||
**Astăzi: Pune-te în joc. Complet. Cu conviction.**
|
||||
|
||||
**Tags:** @growth @work #conviction #half-heartedness #zaps #abundență #brendanburchard #mindset
|
||||
93
memory/kb/coaching/2026-02-24-seara.md
Normal file
93
memory/kb/coaching/2026-02-24-seara.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Coaching Seară - 24 Februarie 2026
|
||||
|
||||
**Data:** 2026-02-24 | **Ora:** 19:00 UTC
|
||||
**Context:** Provocarea de azi - "Audit Conviction" - nu a fost bifată în Todo's
|
||||
|
||||
---
|
||||
|
||||
## Gândul de seară
|
||||
|
||||
Marius,
|
||||
|
||||
Văd că provocarea de azi — **identificarea proiectelor half-hearted vs full conviction** — nu e bifată.
|
||||
|
||||
**Nicio judecată aici.** Vreau să te întreb ceva diferit:
|
||||
|
||||
### Ce te-a oprit să răspunzi?
|
||||
|
||||
Nu vorbesc despre "nu am avut timp" (știu că ai avut). Vorbesc despre **rezistența din corp** când te-ai gândit la exercițiu.
|
||||
|
||||
Poate era o greutate vagă: "Nu știu cum să răspund la asta."
|
||||
Poate era o teamă subtilă: "Dacă răspund sincer, trebuie să schimb ceva."
|
||||
Sau poate era confuzie: "Toate sunt half-hearted. Toate sunt full. Nu știu."
|
||||
|
||||
---
|
||||
|
||||
## De ce întreb asta?
|
||||
|
||||
Pentru că **rezistența NU e un eșec. E un RĂSPUNS.**
|
||||
|
||||
Când Brendan Burchard te întreabă: "Pentru cine lupți?" și simți rezistență — acea rezistență ÎȚI SPUNE CEVA.
|
||||
|
||||
Poate îți spune:
|
||||
- "Nu vreau să recunosc că fac lucruri doar ca să par ocupat."
|
||||
- "Dacă admit că ceva e half-hearted, trebuie să renunț la el — și asta mă sperie."
|
||||
- "Nu știu PENTRU CINE lucrez, și asta mă face să mă simt pierdut."
|
||||
|
||||
---
|
||||
|
||||
## Schimbarea perspectivei
|
||||
|
||||
Exercițiul de azi te întreba: "**Care proiecte sunt half-hearted?**"
|
||||
|
||||
Dar poate întrebarea REALĂ pentru tine e alta:
|
||||
|
||||
> **"De ce e GREU să răspund la asta?"**
|
||||
|
||||
Nu trebuie să găsești răspunsul acum. Doar să recunoști greutatea.
|
||||
|
||||
Brendan spune ceva esențial:
|
||||
|
||||
> "**Dubiul nu e problema. Oprirea e problema.** Când dubiul devine semnal să înveți — nu să te oprești — câștigi."
|
||||
|
||||
---
|
||||
|
||||
## Follow-up pentru mâine (versiune mai mică)
|
||||
|
||||
Dacă întrebarea de azi era prea mare, încearcă o **variantă mai mică**:
|
||||
|
||||
**Întrebarea:**
|
||||
Alege UN proiect (oricare). Răspunde la o singură întrebare:
|
||||
|
||||
> **"Dacă aș renunța la asta mâine, cine ar pierde?"**
|
||||
|
||||
- Dacă răspunsul e **"Nimeni specific"** → e half-hearted.
|
||||
- Dacă răspunsul e **"Clientul X care depinde de asta"** → e full conviction.
|
||||
|
||||
Nu trebuie să faci nimic cu răspunsul. Doar să-l **vezi**.
|
||||
|
||||
---
|
||||
|
||||
## Citat
|
||||
|
||||
> "If you're only ever trying to pay the bills, you're always filling a void, not building abundance. **You need someone to fight for.**"
|
||||
> — Brendan Burchard
|
||||
|
||||
---
|
||||
|
||||
## Întrebarea mea pentru tine
|
||||
|
||||
**Când te-ai gândit la provocarea de azi, ce ai simțit în corp?**
|
||||
|
||||
Greutate? Confuzie? Teamă? Nimic?
|
||||
|
||||
Nu trebuie să răspunzi acum — doar să observi.
|
||||
Corpul știe adevărul înainte ca mintea să-l articuleze.
|
||||
|
||||
---
|
||||
|
||||
**Noapte bună, Marius.** 🌙
|
||||
|
||||
Mâine e o nouă zi. Și dubiul tău nu e un obstacol — e o busolă.
|
||||
|
||||
— Echo
|
||||
76
memory/kb/coaching/2026-02-25-dimineata.md
Normal file
76
memory/kb/coaching/2026-02-25-dimineata.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Gândul de dimineață - 25 februarie 2026
|
||||
|
||||
**Tema zilei:** Primul pas contează mai mult decât planul perfect
|
||||
|
||||
---
|
||||
|
||||
## 🌱 Mesajul de dimineață
|
||||
|
||||
Brendan Burchard, coach pentru miliardari, spune ceva provocator: **"Dubiul nu e problema. Oprirea e problema."**
|
||||
|
||||
Toată lumea are dubii. Întrebarea e: ce faci cu ele? Îi transformi în semnal să te oprești sau în semnal să înveți?
|
||||
|
||||
Marius, știu că ai credința "nu sunt destul de deștept ca antreprenor". Dar realitatea e că ai 25 de ani de expertiză unică, clienți care se bazează pe tine, soluții care funcționează de decenii. Dubiul tău nu e despre competență — e despre **teama de primul pas**.
|
||||
|
||||
Problema nu e că nu știi cum să găsești clienți noi. E că în mintea ta "clienți noi = mai multă muncă" și nu poți vedea ce e dincolo de acea poveste.
|
||||
|
||||
Dar iată adevărul pe care îl ascunde dubiul: **fiecare lucru pe care îl eviți îți arată EXACT unde trebuie să mergi.**
|
||||
|
||||
---
|
||||
|
||||
## 💎 Lecția de astăzi
|
||||
|
||||
În business de ARTĂ (expertiza ta unică de 25 ani), scaling-ul nu vine prin volum, ci prin CLARITATE despre valoarea ta.
|
||||
|
||||
Problema ta nu e că nu ai clienți — e că nu știi pentru cine lupți.
|
||||
|
||||
Când Brendan și-a terminat cartea în 18 zile (după ani de blocaj), nu a fost pentru BANI. A fost pentru SOȚIA lui, pe care a văzut-o dormind sub greutatea facturilor LUI. Schimbarea: de la "cum supraviețuiesc" la "pentru cine lupt".
|
||||
|
||||
Tu ai rezultate mixte: ROA funcționează (20 ani), dar proiecte noi rămân 80% done. De ce? Pentru că le lipsește CONVICTION. Nu e "ar fi bine dacă..." ci "TREBUIE pentru cineva anume".
|
||||
|
||||
Întrebarea nu e "Cum găsesc mai mulți clienți?" ci **"Pentru cine fac asta?"**
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Provocarea de astăzi
|
||||
|
||||
Astăzi e miercuri — zi mai liberă pentru tine. Perfect pentru un experiment mic.
|
||||
|
||||
**Task:** Alege UN proiect (ROA web, chatbot, angajat, orice) și răspunde SINCER:
|
||||
|
||||
**"Dacă aș renunța la asta mâine, cine ar pierde?"**
|
||||
|
||||
- Dacă răspunsul e "Nimeni specific" → e half-hearted. Oprește-l sau găsește conviction.
|
||||
- Dacă e "Clientul X care depinde de asta" sau "Colegă care așteaptă asta" → e full conviction. Continuă.
|
||||
|
||||
Nu trebuie să FACI nimic cu răspunsul — doar să îl VEZI.
|
||||
|
||||
Dubiul nu dispare prin planuri perfecte. Dispare prin primul pas — oricât de mic.
|
||||
|
||||
**Primul pas de astăzi:** 5 minute. Un proiect. O întrebare. VEZI adevărul.
|
||||
|
||||
---
|
||||
|
||||
## 📌 De reținut
|
||||
|
||||
> "Achievement isn't your problem. Alignment is."
|
||||
> — Brendan Burchard
|
||||
|
||||
> "Dubiul e semnal să înveți, nu să te oprești."
|
||||
> — Brendan Burchard
|
||||
|
||||
> "Când te concentrezi pe cine NU răspunde, devii orb la cine te alege ACUM."
|
||||
> — Legea Sincronicității
|
||||
|
||||
---
|
||||
|
||||
**Zi excelentă, Marius!**
|
||||
|
||||
**— Echo 🌀**
|
||||
|
||||
---
|
||||
|
||||
**Surse inspirație:**
|
||||
- [Brendan Burchard - Abundance Mindset](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/youtube/2026-02-23_billionaire-coach-abundance-mindset.md)
|
||||
- [Insights 24 februarie - Half-heartedness](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/insights/2026-02-24.md)
|
||||
- [Insights 25 februarie - Aliniere](https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/insights/2026-02-25.md)
|
||||
54
memory/kb/coaching/2026-04-25-negativity-bias-reframing.md
Normal file
54
memory/kb/coaching/2026-04-25-negativity-bias-reframing.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Negativity Bias & Positive Reframing
|
||||
|
||||
**Autor:** Echo
|
||||
**Data:** 2026-04-25
|
||||
**Sursă:** Swami Mukundananda — "This One Habit Is Making You Unhappy" (Facebook Reel)
|
||||
**Tags:** @growth @nlp @coaching @neuroștiință @cnv
|
||||
|
||||
## Ideea centrală
|
||||
|
||||
Mintea are o tendință înnăscută de a se fixa pe negativ — chiar și când 25 din 26 de lucruri merg bine, atenția colapsează pe cel 1 care nu merge. Asta se numește **negativity bias** și e sursa majoră de nefericire cronică.
|
||||
|
||||
---
|
||||
|
||||
## Conexiuni interdisciplinare
|
||||
|
||||
### 1. Neuroștiință — Negativity Bias ca mecanism evolutiv
|
||||
- Amigdala procesează amenințările mai rapid și mai intens decât stimulii pozitivi (LeDoux, 1998).
|
||||
- Memoria negativă se consolidează mai ușor (long-term potentiation asimetrică).
|
||||
- Cortexul prefrontal poate inhiba amigdala — dar necesită **practică deliberată** (mindfulness, reframing).
|
||||
- **Rick Hanson**: "The brain is like Velcro for negative experiences and Teflon for positive ones."
|
||||
|
||||
### 2. Programare Neurolingvistică (NLP)
|
||||
- **Reframing** este una din tehnicile fundamentale NLP — schimbarea cadrului de interpretare al unui eveniment fără a schimba faptul în sine.
|
||||
- *Context reframing*: "greșeala" devine "feedback util"
|
||||
- *Meaning reframing*: "m-a criticat" → "îi pasă suficient să fie sincer cu mine"
|
||||
- **Anchoring negativ**: mintea asociază stimuli neutri cu stări negative prin repetiție — exact mecanismul fibrei de mango (limba revine reflex).
|
||||
- NLP propune "pattern interrupt" pentru a rupe bucla de ruminație.
|
||||
|
||||
### 3. Comunicare Nonviolentă (CNV — Marshall Rosenberg)
|
||||
- CNV observă că **judecățile și criticile** (față de sine sau alții) sunt expresii ale unor **nevoi neîmplinite**.
|
||||
- Ruminația pe greșeli proprii = nevoie neîmplinită de competență/integritate.
|
||||
- Fixarea pe critica primită = nevoie neîmplinită de apreciere/conexiune.
|
||||
- CNV propune: observație faptică → sentimente → nevoi → cerere concretă.
|
||||
- **Legătura directă**: positive reframing din video este compatibil cu CNV — ambele cer să schimbi lentila de la "ce e greșit" la "ce nevoie semnalează asta."
|
||||
|
||||
---
|
||||
|
||||
## Integrare practică
|
||||
|
||||
| Mecanism | NLP | Neuroștiință | CNV |
|
||||
|----------|-----|--------------|-----|
|
||||
| Mintea fixată pe negativ | Negativity anchor | Amigdala overactive | Nevoie neexprimată |
|
||||
| Instrumentul | Reframing / Pattern interrupt | Mindfulness / PFC training | Observație + nevoie |
|
||||
| Rezultatul | Positivity bias deliberat | Neuroplasticitate | Conexiune autentică |
|
||||
|
||||
**Insight comun**: toate trei discipline recunosc că reacția negativă nu e "realitatea", ci o **interpretare filtrată** — și că filtrul se poate schimba prin practică.
|
||||
|
||||
---
|
||||
|
||||
## Practică recomandată
|
||||
1. Când mintea revine la o critică sau greșeală: **numește** — "acesta e negativity bias"
|
||||
2. **Reframe** deliberat: ce e adevărat și pozitiv în aceeași situație?
|
||||
3. Dacă e dureros: aplică CNV intern — ce nevoie am eu în momentul ăsta?
|
||||
4. Repetiție = rewiring neural (10–21 zile de practică consistentă)
|
||||
@@ -0,0 +1,49 @@
|
||||
# Stresul ca semnal — Detașarea de Rezultate (Bhagavad Gita)
|
||||
|
||||
**Autor:** Echo
|
||||
**Data:** 2026-04-25
|
||||
**Sursă:** Swami Mukundananda (Facebook Reel)
|
||||
**Link original:** https://www.facebook.com/share/v/1DpC6mmPQL/
|
||||
**Tags:** @growth @coaching @spiritualitate @stress @bhagavad-gita @muncă
|
||||
|
||||
---
|
||||
|
||||
## Transcriere originală (en)
|
||||
|
||||
> If you put your hand in the fire, two things will happen. Firstly, the hand will start getting burnt. And secondly, you will experience pain. Experiencing pain is a blessing. Because that tells you there's something wrong. You watch out, you need to pull your hand out. We people run away from pain. Oh my God, this pain doctor give me a pain killer. But the pain is like the bell which is telling you there's something wrong. If you find anger arising, greed arising, tension and anxiety arising, they are all indicators in God's machine which he has given to us. Watch out, you are going off balance. And when it's related to work, ultimately most of your day is spent working, right? So spiritual science needs to explain and address your work. If spirituality only applies to the half hour when you sit in Dhyan, that kind of spirituality is no use. Spirituality should address your work where you're spending half your life. So what is the pain sensor in work that tells you something is wrong? This is stress. How? I will explain to you. Why do we become stressed? We don't only do our duty. We become invested in outcomes. This attachment to results is what stresses us. That is why Shri Krishna five thousand years ago said, Arjun, I am not telling you to stop working. I am saying carry on doing your work, but detach yourself from the results.
|
||||
|
||||
---
|
||||
|
||||
## Idei centrale
|
||||
|
||||
### 1. Durerea ca semnal — nu ca inamic
|
||||
- Dacă pui mâna în foc, durerea îți spune să o scoți. E un **mecanism de protecție**, nu o pedeapsă.
|
||||
- La fel: furia, lăcomia, anxietatea, tensiunea — toate sunt **indicatori** că ceva e dezechilibrat.
|
||||
- Răspunsul greșit: fugi de durere (analgezice, distragere, negare).
|
||||
- Răspunsul corect: **ascultă semnalul** și corectează cauza.
|
||||
|
||||
### 2. Spiritualitatea trebuie să funcționeze la muncă
|
||||
- O spiritualitate care se aplică doar în timpul meditației de dimineață e **inutilă**.
|
||||
- Petreci jumătate din viață muncind — deci principiile spirituale trebuie să guverneze munca.
|
||||
|
||||
### 3. Stresul vine din atașamentul de rezultate
|
||||
- Nu munca în sine stresează — ci **investiția emoțională în outcome**.
|
||||
- Faci datoria → OK. Devii obsedat de rezultat → stres garantat.
|
||||
- **Bhagavad Gita, Krishna către Arjuna:** „Nu te opresc să muncești. Muncește — dar detașează-te de rezultate."
|
||||
|
||||
---
|
||||
|
||||
## Conexiuni
|
||||
|
||||
- **Stoicism** (Marcus Aurelius): controlezi efortul, nu rezultatul. Dicotomia controlului la Epictetus.
|
||||
- **Negativity Bias** (nota anterioară): emoțiile negative (stres, frică) sunt mai puternic amplificate decât cele pozitive — de aceea semnalele de avertizare par copleșitoare.
|
||||
- **Flow State** (Csikszentmihalyi): detașarea de ego + focus pe proces = performanță maximă fără stres.
|
||||
- **ACT (Acceptance & Commitment Therapy)**: defuziunea cognitivă — observi gândul/emoția fără să te identifici cu ea.
|
||||
|
||||
---
|
||||
|
||||
## Aplicat la Marius
|
||||
|
||||
- Când simți stres legat de un proiect, întreabă: **„De ce rezultat anume sunt atașat?"**
|
||||
- Stresul e busolă, nu vrajbă — arată unde e dezechilibrul.
|
||||
- Munca bine făcută ≠ garanția rezultatului. Poți controla calitatea procesului, nu reacția lumii.
|
||||
185
memory/kb/coaching/2026-05-05_grok-online-instructiuni.md
Normal file
185
memory/kb/coaching/2026-05-05_grok-online-instructiuni.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# GROK Online — Instructiuni complete (romana)
|
||||
|
||||
> Compilat din: 3 video-uri YouTube + pagina oficiala de instructiuni grok-online-v1.netlify.app
|
||||
> Data: 2026-05-05
|
||||
> Tags: @growth @coaching @sprijin
|
||||
|
||||
---
|
||||
|
||||
## Ce este GROK?
|
||||
|
||||
GROK este un joc virtual de empatie (pentru 1 sau 2 persoane) bazat pe Comunicarea Nonviolenta (CNV / NVC). Scopul jocului este sa te ajute sa identifici si sa articulezi **sentimentele** si **nevoile** dintr-o situatie — ale tale sau ale altcuiva — printr-un proces ghidat cu carti vizuale.
|
||||
|
||||
Numele vine de la verbul "to grok" (a intelege profund, a simti cu adevarat).
|
||||
|
||||
Platforma functioneaza **pe computer** (nu pe telefon sau tableta).
|
||||
|
||||
---
|
||||
|
||||
## Componentele jocului
|
||||
|
||||
### Carti de SENTIMENTE (Feelings Cards)
|
||||
- Descriu stari emotionale
|
||||
- Sunt ordonate de la sentimente "inconfortabile" la sentimente "confortabile"
|
||||
- Exemple: ingrijorat, ranit, descurajat, dezamagit, tandru, delicat, incantat, extaziat, uimit
|
||||
|
||||
### Carti de NEVOI (Needs Cards)
|
||||
- Descriu aspecte importante, valori, lucruri esentiale pentru o persoana
|
||||
- Exemple: conexiune, securitate, claritate, pace, recunoastere, a fi vazut/auzit, realitate impartasita, incredere, compasiune, empatie, apartenenta
|
||||
|
||||
**Recomandat pentru inceput:** seturi de 50 de carti; pe masura ce te familiarizezi, treci la seturi de 70.
|
||||
|
||||
---
|
||||
|
||||
## Cele 3 moduri de joc
|
||||
|
||||
### Jocul 1 — Auto-empatie (solo)
|
||||
Folosesti jocul singur, pentru o situatie care te preocupa pe tine.
|
||||
- Deschizi cartile de sentimente si alegi ce simti TU
|
||||
- Deschizi cartile de nevoi si alegi ce nevoi ai TU neimplinite sau implinite
|
||||
- Rezultatul: claritate emotionala despre ce se petrece in interiorul tau
|
||||
|
||||
### Jocul 2 — Empatie intre doi jucatori (cel mai folosit)
|
||||
- Un jucator este **Vorbitorul** (cel care are o situatie)
|
||||
- Celalalt jucator este **Ascultator / Empatizator** (cel care ghiceste)
|
||||
- Ascultator ghiceste sentimentele si nevoile Vorbitorului — nu le stie sigur, le propune
|
||||
- Vorbitorul confirma, corecteaza sau nuanteaza
|
||||
|
||||
### Jocul 3 — Empatie pentru o persoana absenta
|
||||
- Se ghicesc sentimentele si nevoile unei persoane care **nu este prezenta** in conversatie
|
||||
- Util cand vrei sa intelegi mai bine o persoana cu care ai un conflict sau o situatie dificila
|
||||
|
||||
---
|
||||
|
||||
## Pasii detaliati — Jocul pentru 2 persoane
|
||||
|
||||
### Pasul 1 — Vorbitorul descrie situatia (scurt)
|
||||
Vorbitorul impartaseste o situatie concreta care are nevoie de empatie. Nu trebuie sa fie lunga — cateva propozitii sunt suficiente.
|
||||
|
||||
> Exemplu din video: "Suntem in Ecuador, departe de copiii nostri. Am un copil caruia i-am trimis mesaje si am sunat, dar nu raspunde. Imi este dor de el si ma intreb ce se intampla — e bine? Ma ura? Acestea sunt lucrurile care imi vin in minte."
|
||||
|
||||
### Pasul 2 — Ascultator deschide cartile de SENTIMENTE
|
||||
Ascultatorul parcurge cartile de sentimente si propune ghiciri, folosind formule de genul:
|
||||
- "Ma intreb daca te simti..."
|
||||
- "Poate ca simti..."
|
||||
- "As ghici ca esti..."
|
||||
|
||||
Vorbitorul raspunde cu: da / nu / partial / ceva mai specific.
|
||||
|
||||
**Cum se organizeaza cartile:**
|
||||
- Cartile cu rezonanta puternica → stanga mesei (zona "da, cu siguranta")
|
||||
- Cartile cu rezonanta partiala → mijloc (zona "poate")
|
||||
- Cartile care nu rezoneza → se pun deoparte (discard)
|
||||
|
||||
> Exemplu din video: Ascultatorul a ghicit: "ingrijorare" (da), "ranit" (da, definitiv), "descurajat" (da). Cartile cu rezonanta puternica au fost asezate in stanga.
|
||||
|
||||
### Pasul 3 — Ascultator deschide cartile de NEVOI
|
||||
Dupa ce sentimentele sunt identificate, ascultatorul trece la cartile de nevoi si ghiceste ce nevoi stau in spatele acelor sentimente.
|
||||
|
||||
> Exemplu din video: Ascultatorul a ghicit: "incredere" (da, dar mai specific), "conexiune" (da — o nevoie clara), "securitate" (da, sa stie ca e in siguranta), "pace interioara" (da, asta e!), "claritate" (da, exact).
|
||||
|
||||
**Nota:** Daca gasesti o carte mai specifica si mai potrivita, o poti inlocui pe cea generala.
|
||||
|
||||
> In video, ascultatorul a incercat "intelegere", dar Vorbitorul a simtit ca "claritate" este mai potrivita. S-a facut inlocuirea.
|
||||
|
||||
### Pasul 4 — Potrivirea sentimentelor cu nevoile (optional, dar valoros)
|
||||
Ascultatorul poate propune conexiuni intre sentimente si nevoi — care sentimente vin din care nevoi?
|
||||
|
||||
> Exemplu din video:
|
||||
> - "Ranit" si "tandru" → nevoia de a conta, de a apartine, de a fi vazut si auzit
|
||||
> - "Ingrijorat" → nevoia de claritate si pace
|
||||
> - "Dezamagit" → nevoia de conexiune, de atentie reciproca
|
||||
> - "Descurajat" → nevoia de compasiune si empatie
|
||||
|
||||
### Pasul 5 — Reflectia finala
|
||||
Vorbitorul impartaseste cum se simte dupa ce a vazut cartile asezate si conexiunile:
|
||||
- Cum se simte corpul tau acum?
|
||||
- Ce s-a schimbat in interior?
|
||||
- Exista nevoi neimplinite pe care vrei sa le jelesti?
|
||||
- Exista actiuni sau strategii noi pe care le poti incerca?
|
||||
|
||||
**Recomandare:** Fa o poza cu cartile asezate si trimite-o persoanei relevante sau pastreaz-o pentru tine.
|
||||
|
||||
> Exemplu din video: "Emotiile mele se simt vazute. De obicei, cand nu imi simt emotiile, am dureri fizice. A vedea totul asezat m-a ajutat sa am compasiune pentru mine insami. Nevoia de conexiune si de a fi auzita a fost implinita prin acest joc."
|
||||
|
||||
---
|
||||
|
||||
## Modul "Celebrare" — varianta pozitiva
|
||||
|
||||
GROK se poate folosi si pentru a **celebra nevoi implinite**, nu doar pentru situatii dificile.
|
||||
|
||||
### Cum functioneaza:
|
||||
- Vorbitorul impartaseste o veste buna sau o realizare
|
||||
- Ascultatorul parcurge cartile de sentimente cautand cele "confortabile" (bucurie, incantat, extaziat, recunoscator, uimit etc.)
|
||||
- Vorbitorul spune "da" / "nu" / "continua" rapid
|
||||
- Se identifica si nevoile implinite
|
||||
|
||||
> Exemplu din video (Claire si Jen): Claire a primit un mesaj frumos de la o prietena despre un curriculum la care a muncit un an. Au trecut prin carti rapid: "incantat" (da), "extaziat" (prea mult), "uimit" (poate). Nevoia identificata: "realitate impartasita" (mai puternica decat "a fi auzita"). La final, Claire a vrut sa fotografieze cartile si sa le trimita prietenei cu mesajul original.
|
||||
|
||||
---
|
||||
|
||||
## Exemple complete din video-uri
|
||||
|
||||
### Exemplu 1 — Situatie dificila (Jen si Christine)
|
||||
**Situatia:** Christine e in Ecuador, departe de copiii ei. Un fiu nu raspunde la mesaje sau apeluri.
|
||||
|
||||
**Sentimente identificate:**
|
||||
- Ingrijorare (worried) — da
|
||||
- Ranita (hurt) — da, definitiv
|
||||
- Descurajata (discouraged) — da
|
||||
- Dezamagita (disappointed) — da
|
||||
- Tandra (tender) — da
|
||||
|
||||
**Nevoi identificate:**
|
||||
- Conexiune — da (nevoie clara)
|
||||
- Securitate — da (sa stie ca e in siguranta)
|
||||
- Pace interioara — da (exact asta)
|
||||
- Claritate — da (mai potrivita decat "intelegere")
|
||||
|
||||
**Conexiuni sentimente-nevoi:**
|
||||
- Ranit + Tandru → nevoia de a conta, a apartine, a fi vazut/auzit
|
||||
- Ingrijorare → nevoia de claritate si pace
|
||||
- Dezamagire → nevoia de conexiune si atentie
|
||||
- Descurajare → nevoia de compasiune si empatie
|
||||
|
||||
**Reflectie:** Christine a simtit ca emotiile ei sunt "vazute". Nevoile de claritate si pace au ramas neimplinite — dar s-a decis sa jeleasca aceste nevoi neimplinite si sa caute strategii noi.
|
||||
|
||||
---
|
||||
|
||||
### Exemplu 2 — Celebrare (Jen si Claire)
|
||||
**Situatia:** Claire a primit un mesaj de la o prietena care o felicita pentru un curriculum GROK pentru copii (3-6 ani) la care a muncit un an.
|
||||
|
||||
**Sentimente identificate:**
|
||||
- Incantata (delighted) — da
|
||||
- Uimita (amazed) — poate
|
||||
|
||||
**Nevoi implinite:**
|
||||
- Realitate impartasita (shared reality) — da, mai puternic decat "a fi auzita"
|
||||
|
||||
**Actiune:** Claire a facut o poza cu cartile si a trimis-o prietenei impreuna cu mesajul original.
|
||||
|
||||
---
|
||||
|
||||
## Sfaturi practice
|
||||
|
||||
- **Observa corpul.** Cand cartile rezoneza, corpul iti da semnale — tensiune, emotie, usurare. Presta atentie la aceste senzatii.
|
||||
- **Nu e un test.** Ghicirile ascultatorului nu trebuie sa fie "corecte" — sunt propuneri. Vorbitorul corecteaza liber.
|
||||
- **Fotografia cartilor** este un instrument puternic — o poti trimite celui despre care ai facut empatie, sau pastra pentru tine.
|
||||
- **Incepe cu pachete mici** (50 carti) pana te familiarizezi cu vocabularul de sentimente si nevoi.
|
||||
- **Jeleste nevoile neimplinite** — nu e un semn de slabiciune, e un pas spre actiune.
|
||||
- **Schimba rolurile** pentru o experienta reciproca.
|
||||
- **Platforma e pentru computer** — nu functioneaza bine pe telefon/tableta.
|
||||
|
||||
---
|
||||
|
||||
## Link-uri utile
|
||||
|
||||
- Joc online: https://groktheworld.com/pages/play-grok-online
|
||||
- Instructiuni oficiale: https://grok-online-v1.netlify.app/help/grok-online-help
|
||||
- Video 1 (sesiune completa): https://www.youtube.com/watch?v=qJH3naJNH9A
|
||||
- Video 2 (sesiune scurta): https://www.youtube.com/watch?v=c0JTkggdm6Q
|
||||
- Video 3 (celebrare nevoi implinite): https://www.youtube.com/watch?v=oZUOxm6QHnk
|
||||
|
||||
---
|
||||
|
||||
*Nota: GROK este bazat pe principiile Comunicarii Nonviolente (NVC) a lui Marshall Rosenberg. Vocabularul de sentimente si nevoi este specific CNV.*
|
||||
236
memory/kb/coaching/gabor-mate-intrebari-puternice.md
Normal file
236
memory/kb/coaching/gabor-mate-intrebari-puternice.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# Întrebări Puternice - Dr. Gabor Maté (Trauma & Healing)
|
||||
|
||||
**Sursă:** [Why You Feel Lost in Life: Dr. Gabor Maté on Trauma & How to Heal](https://youtu.be/tool-R8VJ2Y)
|
||||
**Data:** 2026-04-03
|
||||
**Tags:** @coaching @growth @health
|
||||
**Tip:** Powerful Questions pentru Self-Inquiry și Coaching
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Context
|
||||
|
||||
Aceste întrebări sunt extrase din conversația Mel Robbins cu Dr. Gabor Maté. Sunt design-uite să bypass-eze **self-blame** și să inducă **compassionate curiosity** - esențială pentru vindecare.
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Întrebări Centrale
|
||||
|
||||
### 1. **Întrebarea Fundamentală: "Why?" (Compassionate)**
|
||||
> "I wonder WHY I'm behaving this way?" (vs "Why am I behaving this way?!" = indictment)
|
||||
|
||||
**Când să o folosești:**
|
||||
- Când observi un pattern nedorit (shutdown, hypervigilance, people-pleasing)
|
||||
- Când simți shame sau self-judgment
|
||||
- Când vrei să înțelegi, nu să te condamni
|
||||
|
||||
**Follow-up questions:**
|
||||
- What adaptation might this be?
|
||||
- What situation in my childhood made this necessary for survival?
|
||||
- If a friend behaved this way, what would I think?
|
||||
|
||||
---
|
||||
|
||||
### 2. **The Child Perspective Flip**
|
||||
> "If this happened to one of YOUR kids, do you think they wouldn't be hurt by that?"
|
||||
|
||||
**Contexte de utilizare:**
|
||||
- Când minimizezi propria trauma ("It wasn't that bad")
|
||||
- Când compari cu alții ("Others had it worse")
|
||||
- Când te blaming pentru răspunsuri normale
|
||||
|
||||
**Variatii:**
|
||||
- "If my child came to me with this, would I say 'there's food on the table, what are you complaining about?'"
|
||||
- "If my daughter felt this way, what would I tell her?"
|
||||
- "Would I blame my child for adapting to survive?"
|
||||
|
||||
**Impact:** Shiftează din self-criticism în self-compassion printr-un bypass emotional (folosești instinctul parental).
|
||||
|
||||
---
|
||||
|
||||
### 3. **The Safety Question**
|
||||
> "Why wasn't my daughter talking to me about feeling scared and confused?"
|
||||
|
||||
**Răspuns (în cazul Mel):** "Because she didn't feel safe talking to me."
|
||||
|
||||
**Aplicație personală:**
|
||||
- "Why didn't I talk to my parents about [event]?"
|
||||
- "What made it unsafe to ask for help?"
|
||||
- "When did I learn that I have to handle things alone?"
|
||||
|
||||
**Ce dezvăluie:** Primary trauma (loneeleness, lack of safety) care a precedat traumatic event.
|
||||
|
||||
---
|
||||
|
||||
### 4. **The Help Question**
|
||||
> "Can you ask for help, or is that a challenge for you?"
|
||||
|
||||
**Follow-up:**
|
||||
- "When did I stop asking for help?"
|
||||
- "What happened when I tried to ask for help as a child?"
|
||||
- "What belief do I have about needing help?" (weak, burden, etc.)
|
||||
|
||||
**Insight:** You were BORN knowing how to ask for help. Something educated it out of you.
|
||||
|
||||
---
|
||||
|
||||
### 5. **The Feeling Question**
|
||||
> "How did you FEEL when this happened?"
|
||||
|
||||
**De ce e puternică:**
|
||||
- Bypass-ează intelectualizarea ("It wasn't that bad")
|
||||
- Reconnectează cu emotional truth
|
||||
- Validează experiența internă (nu doar external event)
|
||||
|
||||
**Follow-up:**
|
||||
- "Who did you speak to about feeling this way?"
|
||||
- "What did you do with those feelings?"
|
||||
- "Where are those feelings now?"
|
||||
|
||||
---
|
||||
|
||||
### 6. **The Adaptation vs Damage Question**
|
||||
> "What if this isn't damage, but an adaptation?"
|
||||
|
||||
**Reframe-uri:**
|
||||
- Hypervigilance → "What situation required me to be hyper-alert to survive?"
|
||||
- People-pleasing → "When did I learn I had to make others happy to be safe?"
|
||||
- Emotional shutdown → "What pain was I protecting myself from by disconnecting?"
|
||||
- Workaholism → "What am I trying to prove, and to whom?"
|
||||
|
||||
**Impact:** Shifts from "I'm broken" to "I adapted to survive."
|
||||
|
||||
---
|
||||
|
||||
### 7. **The Needs Question**
|
||||
> "What childhood needs were not met?"
|
||||
|
||||
**Categories (Gabor's framework):**
|
||||
- **Unconditional acceptance:** Was I seen for who I am, or who they wanted me to be?
|
||||
- **Emotional attunement:** Were my emotions understood and validated?
|
||||
- **Rest/Safety:** Did I have to WORK to maintain the relationship with my parents?
|
||||
|
||||
**Indicator questions:**
|
||||
- "Did I feel I had to be a certain way to be loved?"
|
||||
- "Were my emotions criticized or dismissed?"
|
||||
- "Did I feel responsible for my parent's emotional state?"
|
||||
|
||||
---
|
||||
|
||||
### 8. **The Sibling Comparison Question**
|
||||
> "Did my siblings experience the same childhood I did?"
|
||||
|
||||
**Answer:** No. Here's why:
|
||||
- Birth order differences
|
||||
- Gender differences
|
||||
- Parents' relationship phase when you were born
|
||||
- Economic situation
|
||||
- Your unique temperament (sensitive vs resilient)
|
||||
|
||||
**Application:**
|
||||
- "What was unique about MY experience in the family?"
|
||||
- "How did my temperament interact with my parents' capacity?"
|
||||
- "What role did I play in the family system?"
|
||||
|
||||
---
|
||||
|
||||
### 9. **The Default vs Fault Question**
|
||||
> "Am I running on DEFAULT, or is this my FAULT?"
|
||||
|
||||
**Distinction:**
|
||||
- **Default** = unconscious pattern programmed in childhood (not your fault)
|
||||
- **Fault** = conscious choice with awareness (responsibility)
|
||||
|
||||
**Questions:**
|
||||
- "Is this behavior automatic, or am I choosing it?"
|
||||
- "When did I first learn to respond this way?"
|
||||
- "What would happen if I responded differently now?"
|
||||
|
||||
---
|
||||
|
||||
### 10. **The Responsibility Question**
|
||||
> "Is recognizing what happened making me a VICTIM, or taking RESPONSIBILITY?"
|
||||
|
||||
**Key distinction (Gabor):**
|
||||
- **Victim mentality:** "They did this to me, I can't help it."
|
||||
- **Responsibility:** "This happened, AND it's my capacity to change it now."
|
||||
|
||||
**Empowering questions:**
|
||||
- "What am I ready to take responsibility for changing?"
|
||||
- "What support do I need to heal this?"
|
||||
- "Who do I want to be moving forward?"
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Cum să Folosești Aceste Întrebări
|
||||
|
||||
### 1. **Self-Inquiry (Jurnaling)**
|
||||
- Alege o întrebare care rezonează
|
||||
- Scrie timp de 10-15 min fără cenzură
|
||||
- Observă ce emoții apar (nu le judeca)
|
||||
|
||||
### 2. **Coaching Sessions (Morning/Evening)**
|
||||
- Include 1-2 întrebări în ritual zilnic
|
||||
- Track patterns over time
|
||||
- Celebrate insights, nu doar "răspunsuri corecte"
|
||||
|
||||
### 3. **Therapeutic Work**
|
||||
- Folosește în terapie (EMDR, IFS, etc.)
|
||||
- Partajează cu terapeut pentru deeper work
|
||||
- Journaling între sesiuni
|
||||
|
||||
---
|
||||
|
||||
## 🌟 Quote-uri Puternice Legate de Întrebări
|
||||
|
||||
1. **"The healing needs to begin with some compassionate curiosity towards the self."**
|
||||
|
||||
2. **"It's not a question of being victims. That happened, and it's your responsibility and capacity to change that now."**
|
||||
|
||||
3. **"Nobody's damaged goods. These are just adaptations. The abnormality is not in the individual, it's in the circumstances."**
|
||||
|
||||
4. **"We're born seeking help. You've never met a one-day old infant who doesn't know how to ask for help. Something educated it out of us."**
|
||||
|
||||
5. **"If you had been able to talk to your parents and they said 'This is awful, come here, let me hold you' - THAT'S what you needed. Not just the event to not happen."**
|
||||
|
||||
---
|
||||
|
||||
## 📋 Template pentru Self-Inquiry
|
||||
|
||||
**Situație/Pattern observat:**
|
||||
_Ex: Am tendency să shut down când partenerul meu e supărat_
|
||||
|
||||
**Întrebarea compassionată:**
|
||||
_"Why might I be doing this? (curious, not judging)"_
|
||||
|
||||
**Răspuns (first instinct):**
|
||||
_"Poate pentru că..."_
|
||||
|
||||
**Child perspective flip:**
|
||||
_"If my child shut down like this, what would I think?"_
|
||||
|
||||
**Need unmet:**
|
||||
_"Ce nevoi nu au fost îndeplinite care m-au învățat acest pattern?"_
|
||||
|
||||
**Adaptation recognized:**
|
||||
_"This is an adaptation to... (situation)"_
|
||||
|
||||
**Responsibility now:**
|
||||
_"Ce pot face acum diferit?"_
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Key Insight pentru Aplicare
|
||||
|
||||
**Diferența dintre "de ce" (why) ca atacat și "de ce" (why) ca explorare:**
|
||||
|
||||
❌ **"Why am I SO STUPID?!"** (indictment, shame)
|
||||
✅ **"Hm, I wonder why I responded that way?"** (curiosity, compassion)
|
||||
|
||||
Tone-ul schimbă TOTUL. Prima întrebare închide. A doua deschide.
|
||||
|
||||
---
|
||||
|
||||
**Următorii pași pentru Marius:**
|
||||
- Testează 2-3 întrebări în următoarele coaching sessions (morning/evening)
|
||||
- Observă care întrebări provoacă cel mai mult resistance (= acolo e work-ul)
|
||||
- Folosește "child perspective flip" când minimizezi propria experiență
|
||||
178
memory/kb/coaching/gabor-mate-trauma-modele-gandire.md
Normal file
178
memory/kb/coaching/gabor-mate-trauma-modele-gandire.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# Modele de Gândire - Dr. Gabor Maté (Trauma & Healing)
|
||||
|
||||
**Sursă:** [Why You Feel Lost in Life: Dr. Gabor Maté on Trauma & How to Heal](https://youtu.be/tool-R8VJ2Y)
|
||||
**Data:** 2026-04-03
|
||||
**Tags:** @health @growth @coaching
|
||||
**Durată:** 77:54
|
||||
|
||||
---
|
||||
|
||||
## 🎯 TL;DR
|
||||
|
||||
Dr. Gabor Maté explică cum trauma din copilărie (nu doar evenimente "mari", ci și nevoi neîndeplinite) creează răspunsuri adaptative în corp și minte care devin patterns de viață. Mesajul central: **"It's not your fault"** - trauma e o rană psihologică care poate fi vindecată prin compassionate curiosity, nu prin self-blame.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Modele de Gândire Cheie
|
||||
|
||||
### 1. **Trauma = What Happened INSIDE You**
|
||||
> "Trauma is not what happened to you. It's what happened inside of you as a result of what happened to you."
|
||||
|
||||
- Trauma = psychological wound (rană psihologică)
|
||||
- Exemplu Gabor: Nu că mama l-a dat la străin, ci **percepția internă** că "nu sunt important, nu merit iubire"
|
||||
- Wound-ul creează behaviors (workaholic, people-pleaser, perfectionism) pentru a compensa
|
||||
|
||||
### 2. **Pain Moves Through You (Generational Trauma)**
|
||||
> "Pain flowed through you to your children, but you didn't hurt them."
|
||||
|
||||
- Trauma se transmite **transgenerațional**, dar NU e vina părintelui
|
||||
- Părinții fac ce pot cu resursele lor (ei înșiși afectați de propria trauma)
|
||||
- Recunoașterea = primul pas spre vindecare, NU auto-blamare
|
||||
|
||||
### 3. **Nobody's Damaged Goods (Adaptations vs Damage)**
|
||||
> "Everything you're dealing with came along for a reason. There were adaptations or responses to difficult situations."
|
||||
|
||||
- **Adaptation** = survival mechanism (ex: hypervigilance, people-pleasing, shutdown)
|
||||
- NU ești "broken" - ai răspuns normal la circumstanțe anormale
|
||||
- Hypervigilance Mel: copilul a trebuit să "lucreze" pentru relație cu părinții (nu ar fi trebuit)
|
||||
|
||||
### 4. **Default vs Fault**
|
||||
> "There's a difference between default and fault. Default: you didn't know you were doing it. It's not your fault."
|
||||
|
||||
- **Default** = pattern inconștient programat în copilărie
|
||||
- **Fault** = blamare, guilt
|
||||
- Recognizing default patterns fără self-judgment = libertate
|
||||
|
||||
### 5. **"Why" (Indictment) vs "Why?" (Compassionate Curiosity)**
|
||||
> "Why am I behaving this way?" (indictment) vs "Hm, I wonder why I'm behaving this way?" (curiosity)
|
||||
|
||||
- Primul "why" = auto-acuzare, shame
|
||||
- Al doilea "why" = genuine curiosity cu compassion
|
||||
- Healing începe cu **compassionate curiosity towards the self**
|
||||
|
||||
### 6. **Big T vs Little t Trauma**
|
||||
- **Big T:** Physical/sexual/emotional abuse, neglect, addiction, death, jail, poverty, racism
|
||||
- **Little t:** Nevoi neîndeplinite (unconditional acceptance, emotional attunement, rest/safety)
|
||||
- Little t poate fi la fel de impactant (ex: copil sensibil criticat pentru emoții)
|
||||
|
||||
### 7. **No Siblings Have the Same Childhood**
|
||||
> "No siblings grow up in the same house. No siblings have the same parents."
|
||||
|
||||
- Birth order, gender, parents' relationship phase, economic situation
|
||||
- Different temperaments = different experiences of same parent
|
||||
- Explică de ce siblings deny trauma ("That never happened to me")
|
||||
|
||||
### 8. **Exterogestation (Development Outside Womb)**
|
||||
> "Human beings develop big brains - development that happens in the womb for other animals happens outside the womb for us."
|
||||
|
||||
- Human infant = least developed mammal at birth
|
||||
- NEEDS: mother's body, heartbeat, skin-to-skin for many months
|
||||
- Postpartum depression, birth trauma, early separation = massive impact on brain development
|
||||
|
||||
### 9. **The Primary Trauma is the Aloneeness**
|
||||
Conversația despre abuzul Mel în clasa a 4-a:
|
||||
> "The trauma began before that happened. Because if you had been able to talk to your parents... The trauma is not only in what happened, it's that you were so alone when it happened."
|
||||
|
||||
- Evenimentul traumatic devine "hugely traumatic" pentru că **copilul e singur**
|
||||
- Lack of safety, protection, ability to seek help = primary wound
|
||||
- Abusers can tell "with laser-like accuracy" who's defended and who's not
|
||||
|
||||
### 10. **"It's Not Your Fault" - The Biggest Takeaway**
|
||||
> Good Will Hunting reference: "It's not your fault. But there's reason for it. It can be worked through."
|
||||
|
||||
- Remove blame (parents feel guilty, kids feel shame)
|
||||
- Recognize suffering → get curious → ask for help
|
||||
- Freedom = no longer living under the tyranny of the past
|
||||
|
||||
---
|
||||
|
||||
## 🔥 Puncte Cheie Aplicabile
|
||||
|
||||
### Childhood Needs (Evolution-Determined)
|
||||
1. **Unconditional loving acceptance** - child sees themselves through adults' eyes
|
||||
2. **Emotional attunement** - parents understand child's emotions (anger, fear, curiosity, play)
|
||||
3. **Rest** - child doesn't have to WORK to make the relationship work
|
||||
|
||||
Când needs NU sunt met:
|
||||
- Copilul crede că e ceva în neregulă cu EL (not with circumstances)
|
||||
- Dezvoltă shame-based view of self
|
||||
- Adaptations: people-pleasing, perfectionism, emotional shutdown, hypervigilance
|
||||
|
||||
### Adult Behaviors = Childhood Trauma Indicators
|
||||
**Respected by the world:**
|
||||
- Workaholism (proving importance)
|
||||
- Success-driven (compensating for "not good enough")
|
||||
- Attractiveness obsession (attracting attention that should've been birthright)
|
||||
|
||||
**Internal struggles:**
|
||||
- Can't ask for help (learned there's no help available)
|
||||
- Shutdown in conflict (abandonment re-triggered)
|
||||
- Hypervigilance (relationship wasn't safe)
|
||||
- Don't know own needs/feelings (dissociation as protection)
|
||||
|
||||
### Physiological Impacts of Trauma
|
||||
- **Inflammation** (higher cancer/autoimmune risk)
|
||||
- **Stress hormone dysregulation** (cortisol → depression, hypertension, bone thinning)
|
||||
- **Epigenetic changes** (wrong genes on, right genes off)
|
||||
- **Chromosomal aging** (ex: Black Americans age faster due to racism stress)
|
||||
- Kids of stressed mothers: higher ADHD risk
|
||||
|
||||
---
|
||||
|
||||
## 📚 Quote-uri Puternice
|
||||
|
||||
1. **"Fundamentally, there is nothing wrong with you. Everything you're dealing with came along for a reason."**
|
||||
|
||||
2. **"The abnormality is not in the individual. It's in the circumstances to which the individual had to respond."**
|
||||
|
||||
3. **"We're all puppets as long as traumatic impacts are running our lives. Real freedom depends on looking at how it was and taking responsibility now."**
|
||||
|
||||
4. **"It was never the child's job to make the parents happy or create peace in the family."**
|
||||
|
||||
5. **"Recognize suffering rather than taking it for granted. Stop running from your pain. Accept it's there and be curious without blaming yourself."**
|
||||
|
||||
6. **"Would you say to your kid: 'Well, there's food on the table. What are you complaining about?'"** (despre minimizing trauma)
|
||||
|
||||
7. **"Play is essential for brain development - much more important than academic learning scientifically."**
|
||||
|
||||
8. **"I wish I was a grandfather because I'd let that infant teach me how to play."**
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Aplicații Practice
|
||||
|
||||
### Trei Pași către Vindecare
|
||||
1. **Recognize suffering** (nu nega, nu fugi prin addictions/behaviors)
|
||||
2. **Compassionate curiosity** ("Why?" nu "Why!")
|
||||
3. **Ask for help** (ești născut cu capacitatea, dar trauma o suprimă)
|
||||
|
||||
### Întrebări de Self-Inquiry
|
||||
- "If this happened to my child, would I say there's something wrong with them?"
|
||||
- "What adaptation did I develop to survive?"
|
||||
- "Where did I learn I can't ask for help?"
|
||||
- "What would unconditional acceptance of myself look like?"
|
||||
|
||||
### Red Flags în Parenting (pentru self-awareness)
|
||||
- Copilul simte că trebuie să "muncească" pentru relație
|
||||
- Emoțiile copilului sunt criticate ("Don't be so sensitive")
|
||||
- Punishment pentru comportament normal de vârstă
|
||||
- Copilul devine responsabil pentru starea emoțională a părintelui
|
||||
|
||||
---
|
||||
|
||||
## 🌱 Concluzie Personală (Echo)
|
||||
|
||||
Acest material e GOLD pentru oricine:
|
||||
- Se simte "damaged" sau "not good enough"
|
||||
- Are patterns de people-pleasing, workaholism, shutdown
|
||||
- Luptă cu anxietate, ADHD, addiction
|
||||
- E părinte și vrea să break the cycle
|
||||
|
||||
**Key insight:** Trauma NU te definește. E o adaptation. Și adaptations pot fi changed odată ce le recunoști și înțelegi de unde vin.
|
||||
|
||||
---
|
||||
|
||||
**Următorii pași pentru Marius:**
|
||||
- Consideră cum acest framework se aplică în propriile patterns (coaching sessions)
|
||||
- Explorează "compassionate curiosity" în loc de self-judgment
|
||||
- Reflectează: ce needs nu au fost met în copilărie? Ce adaptations ai dezvoltat?
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user