# 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`. ``` https://claude-agent.tailf7372d.ts.net/punte <- din tailnet, ca /echo la moltbot http://127.0.0.1:18790 <- local / prin tunel SSH ``` ## 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`), `DASHBOARD_PREFIX` (implicit gol; `/punte` cand e publicat prin `tailscale serve --set-path`). ## Acces ### Prin Tailscale (recomandat) Exact tiparul de la echo (`https://moltbot.tailf7372d.ts.net/echo/`): procesul ramane legat de `127.0.0.1`, iar `tailscaled` e singurul care ajunge la el si il publica in tailnet, cu HTTPS si certificat de la Tailscale. ```bash sudo tailscale serve --bg --set-path /punte http://127.0.0.1:18790 tailscale serve status ``` ``` https://claude-agent.tailf7372d.ts.net/punte ``` Vizibil doar in tailnet (`tailnet only`) — nu e `funnel`, deci nu iese in internet. Configuratia e persistata de `tailscaled`, deci supravietuieste repornirilor. **Montarea sub prefix**, cele doua capcane si cum sunt rezolvate: - `tailscale serve --set-path` **taie** prefixul inainte de a proxa, deci serverul vede `/api/status`, nu `/punte/api/status`. Toate URL-urile din pagini sunt **relative**, deci merg la orice prefix, fara sa stie de el. `DASHBOARD_PREFIX` e nevoie doar pentru redirecturi si e acceptat si intact pe intrare (`curl` direct pe localhost cu `/punte/...` functioneaza). - adresa **fara slash final** (`/punte`) ar rezolva `api/status` la radacina hostului, unde proxy-ul nu mai trimite nimic incoace; de aceea forma fara slash primeste un 301 catre `/punte/`. ### Prin tunel SSH ```bash 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 ``` 31 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, montarea sub prefix (cu si fara slash final) si verificarea de regresie pentru `deny(ssh)`.