Lantul de pornire nou: JuiceSSH -> 10.0.20.36 -> C:\wolcluster.bat. Cheia moltbot in docs/chei-publice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KddsXCqEbKMhdFJDYbAsx8
13 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.
Tab Infrastructură
Al treilea tab (lângă Punte și Maria), aceeași logică folosită și de comanda
Discord /infra — un singur registru de acțiuni, infra_actions.py, cu doi
consumatori.
Registrul de acțiuni — infra_actions.py
ACTIONS: dict[str, dict] e sursa unică de adevăr: id, label/desc, host
(local/pvemini/pveelite/vm109/oracle-prod-admin), cmd/dry (comanda
reală și, dacă există, varianta --dry-run), job (True → lansată prin
systemd-run, detașată; False → ssh sincron, timeout 120s), confirm (cere
confirmare explicită în UI/Discord) și pre (text descriptiv al precondiției).
Adăugarea unei acțiuni noi înseamnă o singură intrare aici — dashboard-ul și
botul o preiau automat (dashboard prin id, bot prin app_commands.choices
generat din ACTIONS).
Funcțiile publice: status(fresh=False) (stare cluster, cache 30s), check(id, st, dry_run) (precondiții, funcție pură), run(id, dry_run), stop(id),
log_tail(id, n=60), maintenance_pending(st), post_startup_instructions().
Rute dashboard
| Metodă | Rută | Ce face |
|---|---|---|
| GET | /api/infra/status?fresh=1 |
infra_actions.status(); fresh=1 ocolește cache-ul de 30s |
| GET | /api/infra/log?id=<action>&n=<N> |
ultimele linii din jurnalul acțiunii (log_tail) |
| POST | /api/infra/run |
{"id": "...", "dry_run": bool} → infra_actions.run() |
| POST | /api/infra/stop |
{"id": "..."} → oprește job-ul systemd-run al acțiunii |
Fiecare run/stop scrie o linie în infra.log ([actiune] infra <id> dry=<bool>, cerut de <who>) — identitatea vine din antetul Tailscale, ca la
restul dashboard-ului. Citirea de stare ocolește security/infra (ssh
direct, read-only, ca să nu umple jurnalul de acțiuni la fiecare 30 de
secunde); acțiunile trec toate prin security/infra <host> "<cmd>", deci
apar în infra.log cu jurnalizarea obișnuită a wrapper-ului.
Joburi lungi — systemd-run pe nod, nu tmux
tmux nu e instalat pe niciun nod Proxmox, așa că o acțiune cu job: True
pornește prin security/infra <host> "systemd-run --unit=infra-<id> --collect ... /bin/bash -c '<cmd>'" — detașată de conexiunea ssh, cu stdout/stderr
redirectate în /var/log/infra-actions/<id>-<timestamp>.log. status()
listează job-urile active (systemctl list-units 'infra-*') și jurnalele
recente din acel director; tab-ul Infra le arată într-un banner cu buton
Jurnal (poll la 3s) și Oprește (systemctl stop infra-<id>).
Precondiții
Fiecare acțiune reală (nu stare/sarcini) are o precondiție verificată de
check() înainte de executare — de exemplu oprire cere cele 3 noduri
online + cvorum + niciun job activ; backup-pe-pvemini cere pveelite offline
și failover-ul încă neactiv. Un run() cu precondiția nefăcută întoarce
412 fără să atingă rețeaua. status() include deja rezultatul check()
pentru fiecare id (actions: {id: {ok, reason}}), ca butoanele să se
dezactiveze direct din prima cerere de stare.
Fără oprire UPS reală
Acțiunea ups-simulare rulează scriptul de shutdown la panică UPS cu
--dry-run --force — nimic nu se oprește cu adevărat. Nu există în registru
nicio acțiune care să declanșeze un test de baterie cu oprire efectivă a
clusterului; ups-test (testul lunar) descarcă bateria controlat, dar nu
oprește noduri.
Permisiunea Discord „Pin Messages" — momentan lipsă
La oprire (nu în dry-run), post_startup_instructions() postează mesajul cu
instrucțiunile de pornire în primul canal configurat și încearcă să-l fixeze
(pin). Botul nu are azi permisiunea Pin Messages pe canal: mesajul se
postează oricum, dar PUT .../pins/<id> întoarce 403, iar eșecul devine doar
un warning în răspunsul acțiunii (nu blochează oprirea). De dat botului
permisiunea Pin Messages pe canalul respectiv ca fixarea să funcționeze.
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).