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>
212 lines
8.3 KiB
HTML
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 → 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>
|