Files
ROMFASTSQL/proxmox/lxc171-claude-agent/discord-bridge/dashboard
Claude Agent 1cc1a572d9 feat(discord-bridge): aprobari valabile pe tot firul (buton "Allow (tot firul)")
Confirmarea per comanda devenea obositoare intr-o sesiune care lucreaza pe
acelasi host: `ssh pvemini ...` de zece ori la rand insemna zece butoane.

Butonul de confirmare are acum trei variante: Allow / Allow (tot firul) / Deny.
"Allow (tot firul)" memoreaza tiparul `(rule, reason)` produs de clasificator,
nu comanda: dupa o aprobare pe `ssh pvemini uptime`, orice comanda catre ACEL
host trece singura, dar `ssh 10.0.20.36` sau un `rm -rf` cer din nou
confirmare. Aprobarile stau in ~/.claude-discord/approvals/grants/<fir>.json.

Domeniul e firul Discord, nu `session_id`: acela se schimba la `--resume`, iar
aprobarile ar disparea exact cand omul se astepta sa tina.

Expirare: `/new` le sterge (sesiune noua = permisiuni noi), `/permisiuni
revoca:True` la cerere, TTL implicit 12h (CLAUDE_DISCORD_GRANT_TTL), iar
CLAUDE_DISCORD_SESSION_GRANTS=off dezactiveaza complet mecanismul.

Fail-closed peste tot, ca restul hook-ului: fara CLAUDE_DISCORD_THREAD_ID (hook
rulat in afara puntii), cu fisierul de aprobari corupt, cu un thread_id care nu
arata a id (`../`, punct la inceput, peste 128 de caractere) sau la orice
exceptie, has_grant() raspunde False si se cere confirmare in Discord.

Adaugat si `/permisiuni [revoca:True]` (listare/revocare) plus butonul
echivalent in dashboard (`decision: "allow_session"`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
2026-08-31 07:36:34 +00:00
..

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/claude      <- 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. Permite (tot firul) memoreaza tiparul si nu mai intreaba in firul respectiv (vezi ../security/README.md)
Jurnal ultimele 300 de linii din bot.log sau infra.log

Starea se reimprospateaza automat la 5 secunde.

Pe telefon

Panoul e gandit sa fie folosit de pe telefon (de acolo vine si comanda in Discord), deci sub 640px se schimba trei lucruri:

  • tabelul de fire devine carduri stivuite — capul de tabel dispare, iar eticheta fiecarei celule vine din data-label; un tabel de 5 coloane pe 390px ar fi insemnat derulare orizontala la fiecare rand;
  • butoanele stau doua pe rand, cu tinta de atins de 44px (sub 380px, unul pe rand);
  • verificarile si confirmarile se aseaza pe verticala, iar comenzile lungi se rup in loc sa impinga pagina.

Nimic nu iese din viewport pe orizontala: jurnalul si tabelele deruleaza in propriul container. Capcana care rupsese pagina (2621px latime pe un ecran de 390px) era min-width: auto pe copiii de grid — liniile lungi din pre.log dilatau coloana, si odata cu ea toata pagina; de aceea exista min-width: 0 explicit in static/app.css.

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 — sau nimic, cand DASHBOARD_AUTH=off (vezi mai jos).

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|allow_session|deny"}
POST /api/restart-self reporneste dashboard-ul
POST /api/auth/login / /api/auth/logout {"token": "..."} / sterge cookie-ul (inutile cu DASHBOARD_AUTH=off)

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 din neatentie.

Fara token (DASHBOARD_AUTH=off)

Configuratia curenta pe LXC 171. Nu mai exista login: /login.html duce inapoi la panou, iar butonul „Ieși" dispare.

Se sprijina pe doua lucruri, si nu are sens fara ele:

  • serviciul e legat de 127.0.0.1, deci din retea ajunge la el doar tailscaled;
  • tailscale serve publica panoul tainet only, iar accesul in tailnet e deja autentificat de Tailscale.

Ce se pierde: orice dispozitiv din tailnet, si orice proces din container care ajunge la port, poate opri sau reporni puntea. Ce ramane: fiecare start/stop/restart se scrie in logs/dashboard.log cu identitatea din antetul Tailscale-User-Login pus de tailscale serve (local cand cererea vine de pe 127.0.0.1). Antetul e doar pentru jurnal — nu decide accesul, fiindca un proces local l-ar putea fabrica.

Inapoi la token: scoate DASHBOARD_AUTH=off din ~/.claude-discord/env si reporneste serviciul. Doar off, none, 0 si false scot login-ul; orice alta valoare il pastreaza.

Instalare

ops/install.sh face totul (leaga unitul, genereaza DASHBOARD_TOKEN daca lipseste, porneste serviciul). Manual:

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; /claude cand e publicat prin tailscale serve --set-path), DASHBOARD_AUTH (off scoate login-ul).

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.

sudo tailscale serve --bg --set-path /claude http://127.0.0.1:18790
tailscale serve status
https://claude-agent.tailf7372d.ts.net/claude

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 /claude/api/status. URL-urile din pagini sunt relative, iar <base href="/claude/"> se pune la servire, din DASHBOARD_PREFIX. Prefixul e acceptat si intact pe intrare, deci curl direct pe localhost cu /claude/... functioneaza la fel.
  • adresa fara slash final (/claude) ajunge la server tot ca /, exact ca /claude/: proxy-ul taie prefixul si nu lasa nicio urma a formei originale. Fara <base href>, api/status s-ar rezolva atunci la radacina hostului, unde proxy-ul nu mai trimite nimic incoace — pagina se incarca si ramane goala, fara nicio eroare vizibila. Exact asta s-a intamplat la prima folosire reala.
  • paginile se servesc cu Cache-Control: no-store: poarta de acum configuratie (base href), iar o copie veche din cache ar trimite cererile in alta parte.

Prin tunel SSH

ssh -L 18790:127.0.0.1:18790 -N claude@10.0.20.171 &
# apoi http://localhost:18790

Cu DASHBOARD_AUTH=off (configuratia curenta) nu se cere nimic la intrare. Daca tokenul e reactivat, se citeste cu grep DASHBOARD_TOKEN ~/.claude-discord/env.

Teste

cd proxmox/lxc171-claude-agent/discord-bridge
python3 -m pytest tests/test_dashboard.py -q

41 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).