# Dashboard de control pentru puntea Discord (LXC 171) Panou web pentru starea si repornirea puntii, modelat dupa dashboard-ul agentului **echo** de pe LXC 110 (`/home/moltbot/echo-core/dashboard`, port 8088): acelasi tip de server (stdlib `http.server`, zero dependinte), aceiasi tokeni de design, acelasi tipar de endpoint-uri ca in `handlers/eco.py`. ``` http://127.0.0.1:18790 ``` ## Ce arata si ce poate face | Zona | Continut | |---|---| | **Serviciu** | stare `active/running`, uptime, PID, memoria cgroup-ului, numarul de restarturi, firele active, costul zilei fata de plafon | | **Butoane** | Pornește / Oprește / Repornește puntea, cauta si curata procese orfane (`cleanup.py`), reporneste dashboard-ul insusi | | **Fire active** | ce e in `state.json`: fir, model, `cwd`, daca procesul `claude` traieste, daca are tur in desfasurare, cost | | **Diagnostic** | 8 verificari (vezi mai jos), reimprospatate la 30s | | **Confirmari in asteptare** | cererile hook-ului `PreToolUse` — se pot aproba/refuza direct din pagina, nu doar din Discord | | **Jurnal** | ultimele 300 de linii din `bot.log` sau `infra.log` | Starea se reimprospateaza automat la 5 secunde. ## Decizii de proiectare **O singura unitate controlata.** `/api/service` actioneaza intotdeauna pe `claude-discord.service`; numele unitatii nu vine niciodata din cerere. Altfel panoul ar fi un `systemctl` remote fara parola pentru tot ce ruleaza sub `claude`. Exista un test care trimite `{"unit": "ssh.service"}` si verifica faptul ca tot puntea e repornita. **Bind pe 127.0.0.1.** Butonul de restart opreste un agent care ruleaza cu `bypassPermissions` si are chei SSH catre tot clusterul. Se ajunge la el prin tunel SSH (mai jos), nu expus in LAN. `DASHBOARD_BIND` poate schimba asta, dar atunci tokenul ramane singura bariera. **Restart protejat de tururi in zbor.** `stop`/`restart` intorc **409** cu lista firelor care au un tur in desfasurare; interfata intreaba si retrimite cu `force: true` doar dupa confirmare. `KillMode=control-group` din unitul puntii omoara tot cgroup-ul, deci un restart neatent taie raspunsuri pe jumatate scrise. **Dashboard-ul e un unit separat** (`claude-discord-dashboard.service`), tocmai ca o repornire a puntii sa nu ia si panoul din care ai apasat butonul. Invers, `/api/restart-self` iese cu cod 0 si lasa `Restart=always` sa-l reporneasca. **Citire fara lock.** `state.json` e citit direct, fara `flock`: panoul nu are voie sa blocheze botul. Un JSON prins la mijlocul unei scrieri se ignora si se reia la urmatorul poll (test: `test_state_corupt_nu_arunca`). ## Verificarile din Diagnostic 1. serviciul `claude-discord` (stare + numar de restarturi) 2. `state.json` citibil, cate fire contine 3. costul zilei fata de `COST_CAP_USD_DAY` 4. spatiu liber pe disc (prag 10%) 5. dimensiunea `bot.log` (prag 100 MB) 6. **regulile `deny` din `bot-settings.json`** — pica daca reapare `Bash(ssh:*)` sau `Bash(scp:*)`. Sunt regulile care pe 2026-08-30 au taiat complet accesul puntii la infrastructura: `deny` are precedenta peste `bypassPermissions` si opreste turul inainte de hook (vezi `../security/README.md`). 7. prezenta hook-ului de confirmare 8. CLI-ul `claude` in PATH ## Endpoint-uri Toate cer cookie-ul de sesiune, obtinut cu `POST /api/auth/login`. | Metoda | Ruta | Ce face | |---|---|---| | GET | `/api/status` | serviciu + dashboard + fire + cost + numar de confirmari | | GET | `/api/logs?lines=N&file=bot\|infra` | ultimele N linii (plafon 2000) | | GET | `/api/doctor` | verificarile de mai sus | | GET | `/api/approvals` | cererile `pending` | | GET/POST | `/api/cleanup` | GET = doar cauta; POST `{"dry_run": false}` = omoara orfanii | | POST | `/api/service` | `{"action": "start\|stop\|restart", "force": bool}` | | POST | `/api/approvals/decide` | `{"request_id": "...", "decision": "allow\|deny"}` | | POST | `/api/restart-self` | reporneste dashboard-ul | | POST | `/api/auth/login` / `/api/auth/logout` | `{"token": "..."}` / sterge cookie-ul | Autentificarea e un token din `~/.claude-discord/env` schimbat pe un cookie `HttpOnly; SameSite=Strict` valabil 30 de zile, comparat cu `secrets.compare_digest`. Fara `DASHBOARD_TOKEN` in env, procesul isi genereaza unul aleator si il scrie in `logs/dashboard.log` — **nu** ramane deschis. ## Instalare `ops/install.sh` face totul (leaga unitul, genereaza `DASHBOARD_TOKEN` daca lipseste, porneste serviciul). Manual: ```bash ln -sfn /workspace/romfastsql/proxmox/lxc171-claude-agent/discord-bridge/dashboard/claude-discord-dashboard.service \ ~/.config/systemd/user/claude-discord-dashboard.service printf 'DASHBOARD_TOKEN=%s\n' "$(python3 -c 'import secrets;print(secrets.token_urlsafe(24))')" >> ~/.claude-discord/env systemctl --user daemon-reload systemctl --user enable --now claude-discord-dashboard ``` Setari optionale in `~/.claude-discord/env`: `DASHBOARD_BIND` (implicit `127.0.0.1`), `DASHBOARD_PORT` (implicit `18790`). ## Acces Fiind legat de localhost, se ajunge la el prin tunel SSH — la fel ca la dashboard-ul lui echo: ```bash # de pe statia de lucru (direct sau prin Tailscale: 100.95.55.51) ssh -L 18790:127.0.0.1:18790 -N claude@10.0.20.171 & # apoi http://localhost:18790 ``` Tokenul se citeste cu `grep DASHBOARD_TOKEN ~/.claude-discord/env`. ## Teste ```bash cd proxmox/lxc171-claude-agent/discord-bridge python3 -m pytest tests/test_dashboard.py -q ``` 25 de teste, fara retea si fara `systemctl` real (dublura inregistreaza apelurile). Acopera autentificarea, faptul ca unitatea nu poate fi aleasa din cerere, blocajul pe tur in zbor si trecerea cu `force`, traversarea de cale in `request_id`, `state.json` corupt si verificarea de regresie pentru `deny(ssh)`.