Files
ROMFASTSQL/proxmox/lxc171-claude-agent/discord-bridge/dashboard/README.md
Claude Agent ab9dbef998 docs: infrastructura din dashboard si /infra, acces agenti pe statia de birou si 10.0.20.36
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
2026-09-13 10:19:06 +00:00

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

  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.

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