# Punte Discord -> Claude Code (LXC 171) > **Care chatbot e care?** Exista trei lucruri numite „Maria", doi boti pe Discord si > doua punti WhatsApp legate la acelasi numar. Inainte de a depana, citeste > [`docs/chatboti-si-punti.md`](../../../docs/chatboti-si-punti.md) — spune care instanta raspunde de fapt. Un bot Discord subtire care duce mesajele dintr-un guild privat catre CLI-ul `claude` care ruleaza pe containerul de dezvoltare **LXC 171 (claude-agent, 10.0.20.171)**, si aduce raspunsurile inapoi. Practic: acelasi Claude Code cu care lucrezi in terminal, comandat de pe telefon. Nu e un chatbot separat. Nu are memorie proprie, nu are baza de date proprie: sesiunile sunt chiar sesiunile Claude Code din `~/.claude/projects/`, iar un fir de Discord este o sesiune. > Nu confunda cu **MoltBot** (LXC 110) sau cu OpenClaw — acelea sunt alti agenti, cu > alt scop. Puntea asta ruleaza pe masina de dezvoltare si are accesul ei. --- ## Arhitectura ``` Discord (guild privat) | on_message v bot.py -- allowlist (guild / canal / utilizator) [doar adaptor Discord] | +-- session_store.py state.json {thread_id: {sid, cwd, model, inflight, pid}} +-- runner.py proces persistent per fir, alimentat pe stdin +-- stream.py parser tolerant de JSONL +-- render.py chunker + un loop de editare per canal +-- limits.py max procese, timeout tur, rate limit, plafon de cost +-- security/ hook PreToolUse (confirmari) + wrapper `infra` +-- alerts.py alerte email [ops] +-- cleanup.py procese lasate in urma (`/cleanup`) [ops] | v claude -p --input-format stream-json --output-format stream-json --verbose --resume --permission-mode bypassPermissions --settings ~/.claude-discord/bot-settings.json --model sonnet --autocompact auto ``` Cateva alegeri care nu se vad din diagrama: - **Proces persistent per fir**, nu unul per mesaj. Asta permite *steering* la mijlocul turului: un mesaj trimis in timp ce Claude lucreaza ajunge la el si schimba raspunsul (verificat: mesaj la 8s intr-un tur de 34.5s). Reaper la 20 min de inactivitate; repornirea se face cu `--resume `, deci firul nu-si pierde contextul. - **Un proces `claude` = ~406 MB RSS** (masurat). De aici toate limitele: maxim 4 procese vii, `MemoryMax=6G` pe unit, si comanda `/cleanup`. - **Model implicit `sonnet`.** Un tur banal pe opus a costat $0.1547 (masurat), deci opus e optional, per fir, prin `/model model:opus`. --- ## Comenzi | Comanda | Ce face | |---------|---------| | `/new` | Sesiune noua, curata, in firul curent | | `/new fork:True` | Sesiune noua care porneste din contextul celei curente | | `/cd cale:` | Schimba directorul de lucru al firului (ex. `/cd cale:/workspace/romfastsql`) | | `/model model:` | Schimba modelul pentru firul curent | | `/status` | Sesiune, director, model, fereastra de utilizare, cost cumulat, proces viu, ultimele linii de stderr | | `/stop` | Opreste turul in desfasurare din firul curent | | `/cleanup` | Listeaza procesele lasate in urma (rulare seaca). `/cleanup force:True` le opreste | | `/permisiuni` | Ce s-a aprobat pentru tot firul. `/permisiuni revoca:True` sterge aprobarile | | `/help` | Lista de mai sus, in fir | Comenzile sunt **application commands** (`/`), inregistrate pe guild-urile din `DISCORD_GUILD_IDS` la pornirea botului, deci apar in lista de comenzi a Discord. Vechiul prefix `!` nu mai executa nimic: botul raspunde doar cu indiciul catre comanda `/` echivalenta. Orice alt mesaj din canal pleaca la Claude ca prompt, ca inainte. Un fir de Discord = o sesiune Claude. Canalul principal are si el sesiunea lui, cea implicita. Subsolul fiecarui raspuns arata modelul, durata si costul. ### Atasamente Pozele si fisierele text trimise pe Discord ajung la Claude in acelasi tur cu mesajul. Un mesaj **doar** cu atasament, fara text, e valid — nu mai e respins ca gol. Merge si mid-tur: o poza trimisa in timpul unui tur intra pe stdin ca steering, nu deschide tur nou. | Tip | Ce se intampla | Limite | |-----|----------------|--------| | Imagini `png`, `jpeg`, `gif`, `webp` | Devin blocuri `image` — Claude le vede | max 4/mesaj, max 3,5 MB fiecare | | Fisiere text (`.txt`, `.md`, `.log`, `.csv`, `.json`, `.sql`, `.py`, `.sh`, …) | Continutul e inserat in prompt | max 4/mesaj, max 100 KB fiecare (peste atat, trunchiat) | | Orice altceva (PDF, Office, arhive, `svg`, `heic`) | Doar numit in prompt, cu motivul | — | Nimic nu dispare tacut: ce n-a putut fi citit (prea mare, tip neacceptat, descarcare esuata) apare la finalul promptului intr-o linie „Atasamente ignorate: …", deci Claude stie ca ai trimis ceva si poate cere altceva. O imagine stricata nu anuleaza restul mesajului. Limita de 3,5 MB e pe octetii bruti fiindca base64 umfla cu ~4/3, iar API-ul refuza imaginile peste ~5 MB codate. Marimea e verificata de doua ori: intai cea declarata de Discord (ca sa nu descarcam degeaba), apoi cea reala, dupa descarcare. ### Despre `/cleanup` `KillMode=control-group` opreste arborele serviciului la restart, dar **nu prinde ce s-a desprins**: un server pornit cu `&` intr-un tur, un `nohup`, un job lung reparentat la init. Alea raman si se aduna — ~440 MB bucata cu tot cu serverele MCP, pe un container cu istoric de OOM. **DOMENIUL e cgroup-ul serviciului `claude-discord.service` si numai el.** Pe LXC 171 ruleaza permanent sesiuni Claude interactive (tmux, ttyd, agenti) care n-au nicio legatura cu puntea; acelea stau in `tmux-spawn-*.scope` si NU sunt raportate niciodata, nici macar in rularea seaca. Regula e fail-closed: daca cgroup-ul unui proces nu poate fi citit, procesul e considerat neeligibil — mai bine ratam un orfan decat sa oprim sesiunea cuiva. Fara aceasta limitare, `/cleanup force:True` dat din Discord si-ar opri propria sesiune impreuna cu tot ce ruleaza omul in tmux. Inauntrul cgroup-ului, e orfan orice proces neprotejat care nu se leaga de `state.json`, indiferent cum se numeste — asa raman prinse si serverele MCP pornite de `claude` (`npm exec @playwright/mcp`, `node ...`), care altfel ar scapa fiindca nu se cheama `claude`. Un fir costa in jur de 440 MB cu tot cu MCP, nu 300. **Ruleaza sec (dry-run) in mod implicit** — intai vezi lista, apoi decizi. Ce e inregistrat in `state.json` si toti descendentii acelor procese (adica turul care ruleaza chiar acum) nu sunt niciodata atinse, iar potrivirea se face si pe `pid_start_time`, ca un PID reciclat sa nu duca la oprirea altui proces. Copiii se opresc inaintea parintilor, altfel s-ar reparenta si ar scapa. --- ## Instalare ### Partea automata ```bash cd /workspace/romfastsql/proxmox/lxc171-claude-agent/discord-bridge ./ops/install.sh ``` Scriptul e idempotent (poti sa-l rulezi de cate ori vrei) si face: 1. `~/.claude-discord/` cu drepturi `0700`, plus `logs/` si `approvals/` 2. `~/.claude-discord/env` cu `0600`, copiat din `ops/env.example` — **nu suprascrie niciodata un env existent** 3. venv in `~/.claude-discord/venv` + dependintele din `requirements.txt` 4. `loginctl enable-linger claude` — fara asta serviciul de utilizator moare la logout si nu porneste la boot 5. symlink `~/.config/systemd/user/claude-discord.service` -> `ops/claude-discord.service`, apoi `daemon-reload` si `systemd-analyze verify` 6. intrare de crontab pentru `logrotate` (zilnic, 04:10) Scriptul **nu porneste** serviciul. Dupa ce completezi env-ul: ```bash ./ops/install.sh --start ``` ### Partea manuala (o faci tu, o singura data) Nu se poate automatiza: cere un om logat in Discord. 1. **Creeaza aplicatia Discord.** https://discord.com/developers/applications -> *New Application*. E o aplicatie **noua, dedicata** puntii — nu refolosi aplicatia MoltBot/OpenClaw. 2. **Deschide sectiunea *Bot*.** Portalul creeaza user-ul bot odata cu aplicatia, deci daca nu vezi un buton *Add Bot* e normal — intri direct in *Bot*. 3. **Ia token-ul.** *Bot* -> *Reset Token* -> copiaza. **Se arata o singura data.** Il pui in `~/.claude-discord/env`, la `DISCORD_TOKEN=`. Fisierul e `0600` si nu e versionat. Daca token-ul ajunge vreodata intr-un commit, reseteaza-l imediat din portal — cine il are poate comanda infrastructura. 4. **Activeaza intents.** *Bot* -> *Privileged Gateway Intents* -> porneste **MESSAGE CONTENT INTENT**. Fara el botul primeste mesajele goale si nu face nimic. Dupa comutator apare jos bara **Save Changes** — daca nu o apesi, setarea NU se salveaza (usor de ratat pe telefon). Reincarca pagina si confirma ca a ramas pornit. Nu ai nevoie de aprobare de la Discord: verificarea e ceruta abia de la 100 de servere. (*Server Members* si *Presence* nu sunt necesare — lasa-le oprite.) 5. **Invita botul intr-un guild PRIVAT** al tau. *OAuth2* -> *URL Generator* -> scopes: `bot` **si** `applications.commands` -> permisiuni: *View Channel*, *Send Messages*, *Read Message History*, *Create Public Threads*, *Send Messages in Threads*, *Attach Files*, *Embed Links*, *Add Reactions*. Deschide URL-ul generat si alege serverul. Bifele sunt greu de nimerit pe telefon; linkul echivalent, gata calculat (`APPLICATION_ID` e in *General Information*): ``` https://discord.com/oauth2/authorize?client_id=APPLICATION_ID&scope=bot%20applications.commands&permissions=309237763136 ``` `309237763136` = exact permisiunile de mai sus. Fara *View Channel* botul nu vede canalul deloc, oricat de permis ar fi in allowlist. Fara scope-ul `applications.commands` botul merge, dar inregistrarea comenzilor `/` esueaza cu *403 Missing Access* (scrie in log linkul de reinvitare) si comenzile nu apar in lista. Un bot deja invitat se re-invita cu acelasi link: se adauga doar scope-ul lipsa. **Nu-l invita intr-un server cu alti oameni** — cine scrie in canalul permis comanda direct containerul. 6. **Ia ID-urile pentru allowlist.** In Discord: *Settings* -> *Advanced* -> **Developer Mode** pornit. Apoi click dreapta (pe telefon: apasare lunga) -> *Copy Server ID* / *Copy Channel ID* / *Copy User ID*. Le pui in `~/.claude-discord/env`: `DISCORD_GUILD_IDS`, `DISCORD_CHANNEL_IDS`, `DISCORD_USER_IDS` (separate prin virgula). **Allowlist gol = nimic permis** (fail-closed). Mesajele de la webhook-uri si de la alti boti sunt ignorate din principiu. 7. **Pune destinatarul alertelor**: `ALERT_RECIPIENT=` in acelasi env. 8. **Verifica plafonul de cost**: `COST_CAP_USD_DAY=` (implicit `off`). Pe abonament (Pro/Max) lasa `off`: costul raportat de CLI e doar pretul echivalent la API, nu o factura. Pune un numar (ex. `5.00`) doar daca platesti per token. 9. Abia acum: `./ops/install.sh --start`. --- ## Operare ### Unde te uiti ```bash systemctl --user status claude-discord # e viu? journalctl --user -u claude-discord -n 200 # ce a facut ultima data tail -f ~/.claude-discord/logs/bot.log # logul aplicatiei tail -f ~/.claude-discord/logs/alerts.log # ce alerte s-au trimis / au esuat systemctl --user restart claude-discord # repornire ``` Aceleasi lucruri, cu butoane, in **dashboard-ul de control** ([dashboard/README.md](dashboard/README.md)) — stare, restart, orfani, confirmari, jurnale: ``` https://claude-agent.tailf7372d.ts.net/claude # din tailnet (ca /echo la moltbot) # fara login: DASHBOARD_AUTH=off ``` Procesul e legat de `127.0.0.1:18790`; in tailnet il publica `tailscale serve`. Fara Tailscale: `ssh -L 18790:127.0.0.1:18790 -N claude@10.0.20.171`. Acelasi panou include si sectiunea **"Maria — WhatsApp + RAG"**, care controleaza proiectul sibling `../maria-whatsapp-bridge/` (punte WhatsApp Baileys + consumer RAG): start/stop/restart, cod QR de asociere, depozit de documente, reindexare si sincronizare Google Drive. Decizie deliberata: un singur dashboard comun pentru ambele punti de pe acest container, nu un panou separat per serviciu. | Fisier | Ce e | |--------|------| | `~/.claude-discord/env` | token + allowlist + limite (0600) | | `~/.claude-discord/state.json` | sesiuni, directoare, pid-uri, cost | | `~/.claude-discord/logs/bot.log` | stdout/stderr al botului (rotit zilnic, 14 zile) | | `~/.claude-discord/logs/alerts.log` | jurnalul alertelor | | `~/.claude-discord/logs/dashboard.log` | stdout/stderr al dashboard-ului de control | | `~/.claude-discord/alerts-dedup.json` | fereastra de dedup a alertelor | | `~/.claude-discord/approvals/` | cereri de confirmare intre hook si bot | ### Cost Costul se vede in trei locuri: in subsolul fiecarui raspuns (turul curent + cumulat pe fir), in `/status`, si in `state.json` la cheia `cost`. E pretul echivalent la API raportat de CLI: pe abonament nu se factureaza, deci ramane doar o masura de consum. Plafonul zilnic e optional. `COST_CAP_USD_DAY=off` (implicit, si `0` sau gol) inseamna fara plafon: cheltuiala se contorizeaza mai departe, dar nimic nu se opreste. Cu un numar pozitiv, la atingerea lui botul nu mai accepta tururi noi si trimite email; plafonul se reseteaza la schimbarea zilei. ### Alerte pe email `alerts.py` urmeaza tiparul deja folosit in repo (vezi `proxmox/vm109-windows-dr/scripts/pveelite-down-alert.sh`): ``` mail -s "[LEVEL] subiect" "$ALERT_RECIPIENT" # LEVEL: INFO | WARN | CRITICAL ``` Se trimite alerta pentru: proces mort neasteptat, crash loop, plafon de cost atins, `state.json` corupt, orfani detectati la sweep. Doua garantii care conteaza: - **`alert()` nu arunca niciodata exceptii.** O alerta esuata nu are voie sa doboare botul; orice eroare ajunge in `alerts.log` si atat. - **Dedup 1h pe `dedup_key`**, persistat pe disc. Fara el, un crash loop ar trimite sute de emailuri identice. **Dependinta:** binarul `mail`. Pe LXC 171 e instalat pachetul **`bsd-mailx`** (`/usr/bin/mail`), iar transportul e **postfix**, deja prezent (`/usr/sbin/sendmail`). Pe o masina unde lipseste: ```bash sudo apt-get install -y bsd-mailx ``` Daca `mail` lipseste, alertele **nu se pierd**: se degradeaza la scriere in `~/.claude-discord/logs/alerts.log`, cu tot cu corpul mesajului, si `install.sh` te avertizeaza la instalare. Dar nimeni nu mai primeste nimic pe email — deci trateaza lipsa lui ca pe o defectiune, nu ca pe o optiune. Test manual, fara sa pornesti botul: ```bash ALERT_RECIPIENT=tu@romfast.ro python3 alerts.py INFO "test punte" "corp de test" tail -2 ~/.claude-discord/logs/alerts.log ``` ### Cand pica | Simptom | Ce faci | |---------|---------| | Botul nu raspunde deloc in Discord | `systemctl --user status claude-discord`. Daca e `failed`, `journalctl --user -u claude-discord -n 100`. Cauza #1: token invalid sau **MESSAGE CONTENT INTENT** oprit. | | Botul e viu dar tace, iar in log curg `WSServerHandshakeError: 503` | Gatewayul regional memorat ca `resume_gateway_url` a picat, iar discord.py il reincearca la infinit. Verifica: `curl -si --http1.1 -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' 'https://gateway.discord.gg/?v=10&encoding=json'` — un `101` acolo plus `503` pe hostul regional confirma. Watchdogul iese singur dupa `GATEWAY_WATCHDOG_S` (implicit 300s) si systemd reporneste; daca vrei mai repede, `systemctl --user restart claude-discord`. | | Botul e viu dar ignora mesajele | Allowlist. Verifica `DISCORD_GUILD_IDS` / `DISCORD_CHANNEL_IDS` / `DISCORD_USER_IDS` din env. Respingerea e **tacuta**, intentionat. | | Comenzile `/` nu apar in lista din Discord | Botul a fost invitat fara scope-ul `applications.commands`. In log: `sync de comenzi slash esuat` + linkul de reinvitare. Reinvita botul, apoi `systemctl --user restart claude-discord`. | | Unitul se invarte in restart | Dupa 5 porniri esuate in 300s systemd renunta si lasa unitul `failed` (e voit). Repara, apoi `systemctl --user reset-failed claude-discord && systemctl --user start claude-discord`. | | Firul e blocat pe hourglass | Botul a fost restartat la mijlocul unui tur. Turul **nu** se reia automat (risc de dubla executie sub `bypassPermissions`); sweep-ul de la pornire pune un avertisment in fir. Trimite mesajul din nou. | | Memoria containerului creste | `/cleanup` (sec), apoi `/cleanup force:True`. Vezi si `systemctl --user show claude-discord -p MemoryCurrent`. | | „Plafon de cost atins" | E limita zilnica, nu o eroare. Pe abonament pune `COST_CAP_USD_DAY=off` in env si reporneste; altfel ridica valoarea sau asteapta ziua urmatoare. | | Un tur pare inghetat | Mesajul live arata `⏳ ruleaza de 2m30s` sub unealta curenta (din `tool_progress`); cat timp numarul creste, turul lucreaza. Daca sta pe loc, `/stop`. | | „Limita de utilizare atinsa" in `/status` | Fereastra abonamentului (`rate_limit_event`), nu plafonul de cost. Randul `utilizare` arata tipul si ora resetarii; nu se poate ocoli, se asteapta. | | Nu vin emailuri de alerta | `command -v mail`; `mailq`; `tail ~/.claude-discord/logs/alerts.log`. Un `NESENT` in log iti spune exact de ce. | | Dupa reboot serviciul nu porneste | `loginctl show-user claude -p Linger` trebuie sa fie `yes`. Daca nu: `sudo loginctl enable-linger claude`. | ### Teste ```bash cd /workspace/romfastsql/proxmox/lxc171-claude-agent/discord-bridge python3 -m pytest -q # suita rapida, fara retea si fara Discord python3 -m pytest -m e2e # testele care ating CLI-ul real (lente) ``` Testele de ops (`tests/test_alerts.py`, `tests/test_cleanup.py`) nu trimit email real si nu omoara procese reale: folosesc un `mail` fals si copii de `sleep` pe care le pornesc si le opresc ele insele. --- ## Securitate — ce e si ce nu e Puntea ruleaza ca utilizatorul `claude`, cu `--permission-mode bypassPermissions`, si are **exact accesul pe care il are omul in terminal**: `/workspace`, cheile SSH, nodurile Proxmox, LXC-urile, VM-urile. Asta e **functionalitate ceruta**, nu scapare — puntea exista tocmai ca sa poti administra infrastructura de pe telefon. Ce apara efectiv: 1. **Control de acces pe canal.** Guild + canal + utilizator pe allowlist, gol = nimic permis. Webhook-urile si botii sunt respinsi. Contul tau de Discord devine, practic, o cheie de infrastructura — pune-i 2FA. 2. **Confirmare pentru operatiuni ireversibile.** Un hook `PreToolUse` opreste comanda si posteaza butoane in fir; fara raspuns in fereastra de timp raspunsul e **deny** (fail-closed). Verificat: a blocat un `rm -rf`, a asteptat aprobarea externa 20s si a permis apoi executia, fara timeout. Butonul **Allow (tot firul)** memoreaza tiparul `(regula, motiv)` — nu comanda — pentru firul curent, ca o sesiune care lucreaza pe acelasi host sa nu ceara zece confirmari identice; `/new`, `/permisiuni revoca:True` si TTL-ul de 12h il sterg. Detalii in [`security/README.md`](security/README.md). 3. **Wrapper `infra`** cu lista explicita de hosturi + token Proxmox cu ACL. Ce **nu** apara: regulile `deny` din settings. Sub `bypassPermissions` ele sunt un strat cosmetic — verificat, `/usr/bin/ssh -V` si `bash -c "ssh -V"` trec pe langa ele. Nu te baza pe ele ca pe o bariera. --- ## Limitari cunoscute (asumate) - **Nu exista jurnal de audit independent.** Stratul de audit append-only pe branch dedicat a fost considerat si **respins constient**. Consecinta, asumata: la o problema — o comanda distructiva care a trecut, o modificare pe care nimeni nu si-o aminteste — **nu exista o inregistrare independenta care sa spuna ce s-a intamplat, cand si pe ce host**. Ce ramane sunt loguri care pot fi sterse de chiar procesul care le scrie: `bot.log`, `journalctl`, jurnalele de sesiune din `~/.claude/projects/` si istoricul git al repo-urilor atinse. Daca vreodata conteaza „cine si ce", asta e golul de acoperit primul. - **Fara reluare automata a turului pierdut.** Un restart la mijlocul unui tur pierde turul; nu se reia singur, fiindca sub `bypassPermissions` jumatate din comenzi sunt deja executate si o reluare le-ar rula a doua oara. - **Rate limit-ul Discord e o degradare tacuta.** Intervalul de editare se adapteaza (1s -> 5s), dar cand Discord franeaza nu apare niciun mesaj: raspunsul doar apare mai incet. E singura cale fara test din analiza modurilor de esec — acceptata, fiindca esecul e intarziere, nu pierdere. - **`/cleanup` nu e infailibil.** Domeniul lui e strict cgroup-ul serviciului, deci un proces care a iesit complet din cgroup ii scapa — asta e pretul deliberat platit ca sa nu atinga niciodata sesiunile interactive de pe container. Lista `NEVER_KILL` din `cleanup.py` protejeaza infrastructura sesiunii (systemd, sshd, tmux, code-server) — deci un proces cu un asemenea nume in linia de comanda nu va fi oprit niciodata, chiar daca e orfan. - **Fara voce.** Mesajele audio nu ajung la Claude. Imaginile si fisierele text ajung (vezi „Atasamente" mai sus); PDF, Office, arhive si celelalte tipuri sunt doar numite in prompt, nu citite. - **Fara dashboard web** — exista deja pe MoltBot, puntea nu-l duplica. - **Un singur container.** Daca LXC 171 e oprit, puntea e oprita. Nu are redundanta si nu e in HA. --- ## Fisiere | Fisier | Ce e | Lane | |--------|------|------| | `bot.py` | adaptorul Discord: allowlist, comenzi, butoane, atasamente | A | | `session_store.py` | `state.json`: scriere atomica, lock per fir, PID reuse | A | | `runner.py` | proces persistent per fir, stdin JSONL, reaper | A | | `stream.py` | parser tolerant de stream JSONL | A | | `render.py` | chunker + loop de editare per canal | A | | `limits.py` | max procese, timeout, rate limit, plafon de cost | A | | `commands_slash.py` | declararea si inregistrarea comenzilor `/` | A | | `config.py` | citeste `~/.claude-discord/env` | A | | `security/confirm_hook.py` | hook `PreToolUse`, fail-closed | B | | `security/approvals.py` | canal de aprobari hook <-> bot | B | | `security/infra` | wrapper cu lista de hosturi permise | B | | `alerts.py` | alerte email, dedup 1h, nu arunca niciodata | C | | `cleanup.py` | `/cleanup`: orfani, dry-run implicit | C | | `ops/claude-discord.service` | unit systemd de utilizator | C | | `ops/install.sh` | instalare idempotenta | C | | `ops/env.example` | sablon de configurare | C | | `ops/logrotate.conf` | rotatia logurilor | C | | `dashboard/api.py` | dashboard de control (stare, restart, orfani, confirmari) | C | | `dashboard/index.html`, `login.html`, `static/` | interfata dashboard-ului | C | | `dashboard/claude-discord-dashboard.service` | unit systemd pentru dashboard | C | | `INTERFACES.md` | contractul intre module | orchestrator |