From dd7fa28fa513a483b3fd2ff26010a110ef21a2fe Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sun, 30 Aug 2026 13:27:59 +0000 Subject: [PATCH] feat(discord-bridge): dashboard publicat in tailnet prin tailscale serve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude-agent.tailf7372d.ts.net/punte — acelasi tipar ca /echo de pe moltbot: procesul ramane legat de 127.0.0.1, tailscaled il proxeaza si pune HTTPS. Montarea sub prefix a cerut doua schimbari: - toate URL-urile din pagini sunt acum relative, fiindca --set-path TAIE prefixul inainte de a proxa (serverul vede /api/status, browserul cere /punte/api/status). DASHBOARD_PREFIX ramane necesar doar pentru redirectul de login, si e acceptat si intact pe intrare, ca sa mearga si curl direct pe localhost. - adresa fara slash final (/punte) primeste 301 catre /punte/: altfel URL-urile relative s-ar rezolva la radacina hostului, unde proxy-ul nu trimite nimic incoace, si panoul ar arata gol fara nicio eroare vizibila. ops/install.sh configureaza serve-ul daca sudo permite; altfel spune comanda. Sase teste noi pentru montarea sub prefix (31 in total pe dashboard). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7 --- CLAUDE.md | 2 +- .../discord-bridge/README.md | 11 +++-- .../discord-bridge/dashboard/README.md | 44 ++++++++++++++--- .../discord-bridge/dashboard/api.py | 46 ++++++++++++++++-- .../discord-bridge/dashboard/index.html | 26 +++++----- .../discord-bridge/dashboard/login.html | 8 ++-- .../discord-bridge/ops/env.example | 4 ++ .../discord-bridge/ops/install.sh | 19 ++++++++ .../discord-bridge/tests/test_dashboard.py | 47 +++++++++++++++++++ 9 files changed, 173 insertions(+), 34 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a9e3644..57c5b58 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,7 @@ input/ # Oracle DMP files for import - **Chatbot architecture**: `proxmox/lxc104-flowise/docs/prd.md` - **Docker Sandboxes (sbx) — agenți AI izolați**: `proxmox/lxc102-docker/README.md` - **Punte Discord → Claude Code (comandă LXC 171 de pe telefon, dintr-un guild privat)**: `proxmox/lxc171-claude-agent/discord-bridge/README.md` -- **Dashboard de control al punții Discord (stare, restart, orfani, confirmări; tunel SSH pe 18790)**: `proxmox/lxc171-claude-agent/discord-bridge/dashboard/README.md` +- **Dashboard de control al punții Discord (stare, restart, orfani, confirmări; https://claude-agent.tailf7372d.ts.net/punte)**: `proxmox/lxc171-claude-agent/discord-bridge/dashboard/README.md` - **Disaster recovery**: `proxmox/vm109-windows-dr/README.md` - **Instalare/migrare Oracle — care director se folosește**: `proxmox/lxc108-oracle/docs/instalare-si-migrare-oracle.md` - **ROA Windows setup scripts (XE/SE 21c)**: `proxmox/lxc108-oracle/roa-windows-setup/README.md` diff --git a/proxmox/lxc171-claude-agent/discord-bridge/README.md b/proxmox/lxc171-claude-agent/discord-bridge/README.md index 56e41d6..f7f8a57 100644 --- a/proxmox/lxc171-claude-agent/discord-bridge/README.md +++ b/proxmox/lxc171-claude-agent/discord-bridge/README.md @@ -189,12 +189,15 @@ 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, la `http://127.0.0.1:18790` (tunel SSH; e legat de localhost intentionat): +jurnale: -```bash -ssh -L 18790:127.0.0.1:18790 -N claude@10.0.20.171 & -grep DASHBOARD_TOKEN ~/.claude-discord/env ``` +https://claude-agent.tailf7372d.ts.net/punte # din tailnet (ca /echo la moltbot) +grep DASHBOARD_TOKEN ~/.claude-discord/env # tokenul de login +``` + +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`. | Fisier | Ce e | |--------|------| diff --git a/proxmox/lxc171-claude-agent/discord-bridge/dashboard/README.md b/proxmox/lxc171-claude-agent/discord-bridge/dashboard/README.md index 178a8d9..589ec18 100644 --- a/proxmox/lxc171-claude-agent/discord-bridge/dashboard/README.md +++ b/proxmox/lxc171-claude-agent/discord-bridge/dashboard/README.md @@ -6,7 +6,8 @@ de server (stdlib `http.server`, zero dependinte), aceiasi tokeni de design, ace tipar de endpoint-uri ca in `handlers/eco.py`. ``` -http://127.0.0.1:18790 +https://claude-agent.tailf7372d.ts.net/punte <- din tailnet, ca /echo la moltbot +http://127.0.0.1:18790 <- local / prin tunel SSH ``` ## Ce arata si ce poate face @@ -97,15 +98,43 @@ 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_PORT` (implicit `18790`), `DASHBOARD_PREFIX` (implicit gol; `/punte` cand e +publicat prin `tailscale serve --set-path`). ## Acces -Fiind legat de localhost, se ajunge la el prin tunel SSH — la fel ca la dashboard-ul -lui echo: +### 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. + +```bash +sudo tailscale serve --bg --set-path /punte http://127.0.0.1:18790 +tailscale serve status +``` + +``` +https://claude-agent.tailf7372d.ts.net/punte +``` + +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 `/punte/api/status`. Toate URL-urile din pagini sunt + **relative**, deci merg la orice prefix, fara sa stie de el. `DASHBOARD_PREFIX` + e nevoie doar pentru redirecturi si e acceptat si intact pe intrare (`curl` + direct pe localhost cu `/punte/...` functioneaza). +- adresa **fara slash final** (`/punte`) ar rezolva `api/status` la radacina + hostului, unde proxy-ul nu mai trimite nimic incoace; de aceea forma fara slash + primeste un 301 catre `/punte/`. + +### Prin tunel SSH ```bash -# de pe statia de lucru (direct sau prin Tailscale: 100.95.55.51) ssh -L 18790:127.0.0.1:18790 -N claude@10.0.20.171 & # apoi http://localhost:18790 ``` @@ -119,7 +148,8 @@ cd proxmox/lxc171-claude-agent/discord-bridge python3 -m pytest tests/test_dashboard.py -q ``` -25 de teste, fara retea si fara `systemctl` real (dublura inregistreaza apelurile). +31 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 si verificarea de regresie pentru `deny(ssh)`. +corupt, montarea sub prefix (cu si fara slash final) si verificarea de regresie +pentru `deny(ssh)`. diff --git a/proxmox/lxc171-claude-agent/discord-bridge/dashboard/api.py b/proxmox/lxc171-claude-agent/discord-bridge/dashboard/api.py index fbca525..207b9b6 100644 --- a/proxmox/lxc171-claude-agent/discord-bridge/dashboard/api.py +++ b/proxmox/lxc171-claude-agent/discord-bridge/dashboard/api.py @@ -60,6 +60,27 @@ def infra_log() -> Path: COOKIE_NAME = "dashboard" COOKIE_MAX_AGE = 60 * 60 * 24 * 30 + +def mount_prefix() -> str: + """Prefixul sub care e montat panoul (`DASHBOARD_PREFIX`, ex. `/punte`). + + `tailscale serve --set-path /punte` TAIE prefixul inainte de a proxa, deci in + mod normal aici ajunge `/api/status`. Prefixul e acceptat totusi si intact, + pentru cazul unui proxy care nu taie si pentru `curl` direct pe localhost. + Paginile nu depind de el: toate URL-urile din HTML sunt relative. + """ + pfx = (config.get("DASHBOARD_PREFIX") or "").strip().rstrip("/") + if pfx and not pfx.startswith("/"): + pfx = "/" + pfx + return pfx + + +def strip_prefix(path: str) -> str: + pfx = mount_prefix() + if pfx and (path == pfx or path.startswith(pfx + "/")): + return path[len(pfx):] or "/" + return path + _TOKEN: str | None = None @@ -378,19 +399,34 @@ class Handler(SimpleHTTPRequestHandler): # --- GET ----------------------------------------------------------- def do_GET(self): - path = urlparse(self.path).path + raw = urlparse(self.path).path + path = strip_prefix(raw) + if path == "/" and not raw.endswith("/"): + # `/punte` fara slash final: URL-urile relative din pagina s-ar + # rezolva la radacina hostului (`/api/status`), unde proxy-ul nu mai + # trimite nimic incoace. Fortam forma cu slash. + self.send_response(301) + self.send_header("Location", raw + "/") + self.send_header("Content-Length", "0") + self.end_headers() + return if path.startswith("/api/"): if not self.authed(): return self.deny() return self.route_get(path) if path in ("/", "/index.html") and not self.authed(): + # Prefixul reintra AICI in mod deliberat. Un "/login.html" absolut ar + # arunca browserul in radacina hostului (alt serviciu), iar un + # "login.html" relativ se rezolva gresit cand adresa vine fara slash + # final (`/punte` -> `/login.html`). Cu prefixul reatasat, ambele + # forme ajung unde trebuie, si direct pe localhost la fel: calea de + # intrare e curatata oricum de `strip_prefix`. self.send_response(302) - self.send_header("Location", "/login.html") + self.send_header("Location", mount_prefix() + "/login.html") self.send_header("Content-Length", "0") self.end_headers() return - if path == "/": - self.path = "/index.html" + self.path = "/index.html" if path == "/" else path return super().do_GET() def route_get(self, path: str): @@ -428,7 +464,7 @@ class Handler(SimpleHTTPRequestHandler): # --- POST ---------------------------------------------------------- def do_POST(self): - path = urlparse(self.path).path + path = strip_prefix(urlparse(self.path).path) if path == "/api/auth/login": return self.handle_login() if path == "/api/auth/logout": diff --git a/proxmox/lxc171-claude-agent/discord-bridge/dashboard/index.html b/proxmox/lxc171-claude-agent/discord-bridge/dashboard/index.html index c935b74..fd8e099 100644 --- a/proxmox/lxc171-claude-agent/discord-bridge/dashboard/index.html +++ b/proxmox/lxc171-claude-agent/discord-bridge/dashboard/index.html @@ -4,8 +4,8 @@ Punte Discord — control - - + + @@ -102,7 +102,7 @@ function toast(msg, kind) { } async function api(path, opts) { var r = await fetch(path, opts || {}); - if (r.status === 401) { location.href = '/login.html'; throw new Error('neautentificat'); } + if (r.status === 401) { location.href = 'login.html'; throw new Error('neautentificat'); } var data = null; try { data = await r.json(); } catch (e) { data = {}; } return { ok: r.ok, status: r.status, data: data }; @@ -130,7 +130,7 @@ function mb(bytes) { // ── stare ───────────────────────────────────────────────────── var busy = false; async function refresh() { - var res = await api('/api/status'); + var res = await api('api/status'); if (!res.ok) return; var s = res.data, svcInfo = s.service; @@ -173,7 +173,7 @@ async function refresh() { } async function refreshApprovals() { - var res = await api('/api/approvals'); + var res = await api('api/approvals'); if (!res.ok) return; var list = res.data.approvals || []; document.getElementById('approvals').innerHTML = list.length ? list.map(function (a) { @@ -188,13 +188,13 @@ async function refreshApprovals() { } async function decide(id, d) { - var res = await post('/api/approvals/decide', { request_id: id, decision: d }); + var res = await post('api/approvals/decide', { request_id: id, decision: d }); toast(res.ok ? 'Trimis: ' + d : ('Eșuat: ' + (res.data.error || res.status)), res.ok ? 'ok' : 'bad'); refresh(); } async function refreshDoctor() { - var res = await api('/api/doctor'); + var res = await api('api/doctor'); if (!res.ok) return; document.getElementById('doctor').innerHTML = (res.data.checks || []).map(function (c) { return '
' + @@ -210,7 +210,7 @@ async function svc(action, force) { !confirm('Sigur „' + action + '” pe serviciul punții?')) return; busy = true; try { - var res = await post('/api/service', { action: action, force: !!force }); + var res = await post('api/service', { action: action, force: !!force }); if (res.status === 409) { var n = (res.data.inflight || []).length; if (confirm(n + ' fir(e) au tur în desfășurare. Le întrerupi?')) { @@ -232,7 +232,7 @@ async function svc(action, force) { async function cleanup(dry) { if (!dry && !confirm('Omor procesele orfane găsite?')) return; - var res = await post('/api/cleanup', { dry_run: dry }); + var res = await post('api/cleanup', { dry_run: dry }); if (!res.ok) { toast('Eșuat: ' + (res.data.error || res.status), 'bad'); return; } var n = (res.data.orphans || []).length; toast(dry ? (n + ' orfan(i) găsiți') : (n + ' orfan(i) tratați'), n ? 'bad' : 'ok'); @@ -241,14 +241,14 @@ async function cleanup(dry) { async function restartSelf() { if (!confirm('Repornesc dashboard-ul? Pagina se reîncarcă în câteva secunde.')) return; - await post('/api/restart-self', {}); + await post('api/restart-self', {}); toast('Dashboard-ul repornește…', ''); setTimeout(function () { location.reload(); }, 4000); } async function logout() { - await post('/api/auth/logout', {}); - location.href = '/login.html'; + await post('api/auth/logout', {}); + location.href = 'login.html'; } // ── jurnal ──────────────────────────────────────────────────── @@ -257,7 +257,7 @@ function setLog(which) { logFile = which; refreshLogs(); } async function refreshLogs() { document.getElementById('tabBot').className = 'small' + (logFile === 'bot' ? ' primary' : ''); document.getElementById('tabInfra').className = 'small' + (logFile === 'infra' ? ' primary' : ''); - var res = await api('/api/logs?lines=300&file=' + logFile); + var res = await api('api/logs?lines=300&file=' + logFile); if (!res.ok) return; var el = document.getElementById('log'); el.textContent = (res.data.lines || []).join('\n') || '(gol)'; diff --git a/proxmox/lxc171-claude-agent/discord-bridge/dashboard/login.html b/proxmox/lxc171-claude-agent/discord-bridge/dashboard/login.html index cdf5d52..f3403c6 100644 --- a/proxmox/lxc171-claude-agent/discord-bridge/dashboard/login.html +++ b/proxmox/lxc171-claude-agent/discord-bridge/dashboard/login.html @@ -4,8 +4,8 @@ Autentificare — Punte Discord - - + +