Files
ROMFASTSQL/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/dashboard/index.html
Claude Agent dd5553e327 Add Maria WhatsApp+RAG bridge as a service (LXC 171)
Move the /tmp prototype (Baileys bridge + RAG consumer) into git as a
proper sibling project to discord-bridge/: own systemd --user units
(whatsapp bridge, rag consumer, dashboard, periodic Drive sync timer),
a filesystem document store with a stdlib control dashboard (start/
stop/restart, document CRUD, reindex, Google Drive sync via rclone),
and an idempotent ops/install.sh following the same conventions.

Co-Authored-By: Claude Agent <noreply@anthropic.com>
2026-08-31 17:38:51 +00:00

212 lines
8.3 KiB
HTML

<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Maria — WhatsApp + RAG</title>
<style>
:root{color-scheme:dark}
body{font-family:system-ui,sans-serif;background:#111;color:#eee;margin:0;padding:24px;
max-width:920px;margin-inline:auto}
h1{font-size:20px}
h2{font-size:15px;color:#aaa;margin-top:28px}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:12px}
.card{background:#1b1b1b;border-radius:10px;padding:16px}
.row{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;font-size:14px}
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}
.dot.on{background:#4caf50} .dot.off{background:#e57373}
button{padding:6px 12px;border-radius:6px;border:1px solid #333;background:#232323;
color:#eee;cursor:pointer;font-size:13px;margin-right:6px}
button:hover{background:#2e2e2e}
button.danger:hover{background:#5c2020}
table{width:100%;border-collapse:collapse;font-size:13px;margin-top:8px}
td,th{padding:6px 4px;border-bottom:1px solid #262626;text-align:left}
textarea{width:100%;min-height:120px;background:#0d0d0d;color:#eee;border:1px solid #333;
border-radius:6px;padding:8px;box-sizing:border-box;font-family:monospace}
input[type=text]{padding:6px;border-radius:6px;border:1px solid #333;background:#0d0d0d;color:#eee}
pre{background:#0d0d0d;padding:10px;border-radius:6px;max-height:280px;overflow:auto;font-size:12px}
img.qr{width:220px;height:220px;border-radius:8px;background:#fff;padding:8px}
.muted{color:#888;font-size:12px}
a.logout{color:#888;font-size:12px;float:right;cursor:pointer}
</style>
</head>
<body>
<h1>Maria — punte WhatsApp + RAG <a class="logout" id="logout">deconectare</a></h1>
<div class="grid">
<div class="card">
<div class="row"><b>Punte WhatsApp</b><span id="bridge-state"></span></div>
<div class="row muted" id="bridge-detail"></div>
<button onclick="svc('bridge','start')">start</button>
<button onclick="svc('bridge','stop')">stop</button>
<button onclick="svc('bridge','restart')">restart</button>
</div>
<div class="card">
<div class="row"><b>Consumer RAG</b><span id="rag-state"></span></div>
<div class="row muted" id="rag-detail"></div>
<button onclick="svc('rag','start')">start</button>
<button onclick="svc('rag','stop')">stop</button>
<button onclick="svc('rag','restart')">restart</button>
</div>
</div>
<h2>Conectare WhatsApp</h2>
<div class="card" id="wa-card">
<div class="row" id="wa-status">se incarca...</div>
<div id="wa-qr"></div>
</div>
<h2>Documente indexate (depozit RAG)</h2>
<div class="card">
<div class="row muted" id="index-detail"></div>
<div class="row muted" id="sync-detail"></div>
<button onclick="reindex()">reconstruieste indexul</button>
<button onclick="syncNow()">sincronizeaza din Drive acum</button>
<table id="docs-table"><thead><tr><th>nume</th><th>marime</th><th></th></tr></thead><tbody></tbody></table>
<h2 style="margin-top:16px">Adauga document (.txt / .md)</h2>
<input type="text" id="doc-name" placeholder="ex: politica-retururi.md" style="width:260px">
<br><br>
<textarea id="doc-content" placeholder="continutul documentului..."></textarea>
<br><br>
<button onclick="addDoc()">salveaza si reindexeaza</button>
<div class="muted" id="doc-msg"></div>
</div>
<h2>Loguri</h2>
<div class="card">
<select id="log-service" onchange="loadLogs()">
<option value="bridge">whatsapp</option>
<option value="rag">rag</option>
<option value="dashboard">dashboard</option>
</select>
<button onclick="loadLogs()">reincarca</button>
<pre id="logs"></pre>
</div>
<script>
async function api(path, opts) {
const r = await fetch(path, opts);
if (r.status === 401) { location.href = '/login.html'; throw new Error('neautentificat'); }
return r.json();
}
function fmtUptime(s) {
if (s == null) return '—';
const h = Math.floor(s/3600), m = Math.floor((s%3600)/60);
return `${h}h${m}m`;
}
function renderUnit(prefix, info) {
document.getElementById(prefix+'-state').innerHTML =
`<span class="dot ${info.active?'on':'off'}"></span>${info.state}/${info.sub}`;
document.getElementById(prefix+'-detail').textContent =
info.active ? `pid ${info.pid ?? '?'} · sus de ${fmtUptime(info.uptime_s)} · ${info.restarts} restarturi`
: `${info.restarts} restarturi`;
}
async function refresh() {
const s = await api('/api/status');
renderUnit('bridge', s.bridge);
renderUnit('rag', s.rag);
const wa = s.whatsapp || {};
const waStatus = document.getElementById('wa-status');
const waQr = document.getElementById('wa-qr');
if (wa.connected) {
waStatus.innerHTML = `<span class="dot on"></span>conectat ca ${wa.phone || '?'}`;
waQr.innerHTML = '';
} else if (wa.qr) {
waStatus.innerHTML = `<span class="dot off"></span>neconectat — scaneaza codul QR din WhatsApp &rarr; Dispozitive conectate`;
waQr.innerHTML = `<img class="qr" src="${wa.qr}">`;
} else {
waStatus.innerHTML = `<span class="dot off"></span>${wa.reachable === false ? 'puntea nu ruleaza' : 'astept codul QR...'}`;
waQr.innerHTML = '';
}
document.getElementById('index-detail').textContent =
`${s.index.chunks} chunk-uri din ${s.index.documents_indexed} documente indexate · ${s.documents} documente in depozit` +
(s.index.mtime ? ` · indexat ${new Date(s.index.mtime*1000).toLocaleString('ro-RO')}` : '');
const sync = s.sync || {};
document.getElementById('sync-detail').textContent = sync.drive_remote
? `Drive: ${sync.drive_remote}` + (sync.synced_at ? ` · ultima sincronizare ${new Date(sync.synced_at*1000).toLocaleString('ro-RO')}` : ' · inca nesincronizat')
: 'Sincronizare Drive dezactivata (DRIVE_REMOTE nesetat in env) — doar upload manual';
const docs = await api('/api/documents');
const tbody = document.querySelector('#docs-table tbody');
tbody.innerHTML = '';
for (const d of docs.documents) {
const tr = document.createElement('tr');
tr.innerHTML = `<td>${d.name}</td><td>${d.size} B</td><td><button class="danger" onclick="delDoc('${d.name}')">sterge</button></td>`;
tbody.appendChild(tr);
}
}
async function svc(which, action) {
const r = await api('/api/service', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({service: which, action}),
});
if (!r.ok) alert(r.error || 'a esuat');
refresh();
}
async function addDoc() {
const name = document.getElementById('doc-name').value.trim();
const content = document.getElementById('doc-content').value;
const msg = document.getElementById('doc-msg');
if (!name || !content) { msg.textContent = 'completeaza numele si continutul'; return; }
const r = await api('/api/documents', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({name, content}),
});
if (!r.ok) { msg.textContent = r.error || 'a esuat'; return; }
msg.textContent = 'salvat, reindexez...';
const ri = await api('/api/reindex', {method: 'POST'});
msg.textContent = ri.ok ? 'salvat si reindexat' : ('salvat, reindexarea a esuat: ' + (ri.error || ri.stderr || ''));
document.getElementById('doc-content').value = '';
refresh();
}
async function delDoc(name) {
if (!confirm(`Stergi ${name}?`)) return;
await api('/api/documents/delete', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({name}),
});
await reindex();
}
async function reindex() {
const r = await api('/api/reindex', {method: 'POST'});
if (!r.ok) alert('reindexare esuata: ' + (r.error || r.stderr || ''));
refresh();
}
async function syncNow() {
const r = await api('/api/sync', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({force: true}),
});
if (!r.ok) alert('sincronizare esuata: ' + (r.error || r.stderr || ''));
refresh();
}
async function loadLogs() {
const which = document.getElementById('log-service').value;
const r = await api(`/api/logs?service=${which}&lines=200`);
document.getElementById('logs').textContent = (r.lines || []).join('\n');
}
document.getElementById('logout').addEventListener('click', async () => {
await fetch('/api/auth/logout', {method: 'POST'});
location.href = '/login.html';
});
refresh();
loadLogs();
setInterval(refresh, 4000);
</script>
</body>
</html>