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
9.5 KiB
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
- serviciul
claude-discord(stare + numar de restarturi) state.jsoncitibil, cate fire contine- costul zilei fata de
COST_CAP_USD_DAY - spatiu liber pe disc (prag 10%)
- dimensiunea
bot.log(prag 100 MB) - regulile
denydinbot-settings.json— pica daca reapareBash(ssh:*)sauBash(scp:*). Sunt regulile care pe 2026-08-30 au taiat complet accesul puntii la infrastructura:denyare precedenta pestebypassPermissionssi opreste turul inainte de hook (vezi../security/README.md). - prezenta hook-ului de confirmare
- CLI-ul
claudein 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 doartailscaled; tailscale servepublica panoultainet 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-pathtaie 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, dinDASHBOARD_PREFIX. Prefixul e acceptat si intact pe intrare, decicurldirect 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/statuss-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).