feat(discord-bridge): dashboard publicat in tailnet prin tailscale serve

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
This commit is contained in:
Claude Agent
2026-08-30 13:27:59 +00:00
parent 57867672f2
commit dd7fa28fa5
9 changed files with 173 additions and 34 deletions

View File

@@ -4,8 +4,8 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Punte Discord — control</title>
<link rel="stylesheet" href="/static/tokens.css">
<link rel="stylesheet" href="/static/app.css">
<link rel="stylesheet" href="static/tokens.css">
<link rel="stylesheet" href="static/app.css">
</head>
<body>
@@ -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 '<div class="check ' + (c.pass ? 'pass' : 'fail') + '"><span class="mark">' +
@@ -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)';