Files
ROMFASTSQL/proxmox/lxc171-claude-agent/discord-bridge/dashboard/index.html
Claude Agent 610638ad30 feat(maria): profil de client (firma), asociat cu mai multe numere
Un client poate avea mai multi angajati care scriu pe WhatsApp de pe
numere diferite; toti gasesc acum acelasi profil (rag/client.py,
~/.maria-bridge/clients.json). Profilul intra in prompt sub o sectiune
DESPRE CLIENT, la fiecare mesaj, stateless ca restul Mariei — dar nu
atinge cautarea/scorul din rag/rank.py, ca sa nu strice pragurile deja
calibrate. Administrare din dashboard (sectiune noua "Clienti cunoscuti"),
nu din chat, ca sa ramana pe canalul autentificat.

117/117 teste maria-whatsapp-bridge + teste noi pentru rutele de dashboard.

Co-Authored-By: Claude Agent <noreply@anthropic.com>
2026-09-08 10:35:30 +00:00

620 lines
29 KiB
HTML

<!DOCTYPE html>
<html lang="ro" data-theme="dark">
<head>
<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">
</head>
<body>
<header class="header">
<span class="logo"><span class="dot" id="dot"></span> Punte Discord</span>
<span class="spacer"></span>
<span class="meta" id="lastPoll">—</span>
<button class="small" onclick="toggleTheme()" title="Schimbă tema">◐</button>
<button class="small" id="btnLogout" onclick="logout()" hidden>Ieși</button>
</header>
<div class="wrap">
<!-- ── serviciu ───────────────────────────────────────────── -->
<section class="card">
<h2>Serviciu <span class="count" id="unitName"></span></h2>
<div class="metrics" id="metrics"></div>
<div class="actions" style="margin-top:var(--space-5)">
<button class="primary" id="btnRestart" onclick="svc('restart')">Repornește</button>
<button id="btnStart" onclick="svc('start')">Pornește</button>
<button class="danger" id="btnStop" onclick="svc('stop')">Oprește</button>
<span class="spacer"></span>
<button class="small" onclick="cleanup(true)">Caută orfani</button>
<button class="small danger" onclick="cleanup(false)">Curăță orfani</button>
<button class="small" onclick="restartSelf()">Repornește dashboard-ul</button>
</div>
</section>
<div class="grid-2">
<!-- ── fire ─────────────────────────────────────────────── -->
<section class="card">
<h2>Fire active <span class="count" id="threadCount"></span></h2>
<div class="table-scroll">
<table class="stack-mobil">
<thead><tr><th>Fir</th><th>Model</th><th>Director</th><th>Stare</th><th class="num">USD</th></tr></thead>
<tbody id="threads"></tbody>
</table>
</div>
</section>
<!-- ── diagnostic ───────────────────────────────────────── -->
<section class="card">
<h2>Diagnostic</h2>
<div id="doctor"><div class="empty">se încarcă…</div></div>
</section>
</div>
<!-- ── aprobări ───────────────────────────────────────────── -->
<section class="card">
<h2>Confirmări în așteptare <span class="count" id="apprCount"></span></h2>
<div id="approvals"><div class="empty">niciuna</div></div>
</section>
<!-- ── jurnal ─────────────────────────────────────────────── -->
<section class="card">
<h2>Jurnal
<span class="count">
<button class="small" id="tabBot" onclick="setLog('bot')">bot.log</button>
<button class="small" id="tabInfra" onclick="setLog('infra')">infra.log</button>
<button class="small" onclick="refreshLogs()">↻</button>
</span>
</h2>
<pre class="log" id="log">se încarcă…</pre>
</section>
<!-- ── Maria: WhatsApp + RAG ──────────────────────────────── -->
<h2 style="margin-top:var(--space-6)">Maria — WhatsApp + RAG</h2>
<div class="grid-2">
<section class="card">
<h2>Punte WhatsApp <span class="count" id="mBridgeUnit"></span></h2>
<div class="metrics" id="mBridgeMetrics"></div>
<div class="actions" style="margin-top:var(--space-5)">
<button onclick="mSvc('bridge','start')">Pornește</button>
<button class="danger" onclick="mSvc('bridge','stop')">Oprește</button>
<button class="primary" onclick="mSvc('bridge','restart')">Repornește</button>
</div>
</section>
<section class="card">
<h2>Consumer RAG <span class="count" id="mRagUnit"></span></h2>
<div class="metrics" id="mRagMetrics"></div>
<div class="actions" style="margin-top:var(--space-5)">
<button onclick="mSvc('rag','start')">Pornește</button>
<button class="danger" onclick="mSvc('rag','stop')">Oprește</button>
<button class="primary" onclick="mSvc('rag','restart')">Repornește</button>
</div>
</section>
</div>
<section class="card">
<h2>Conectare WhatsApp</h2>
<div id="mWaStatus" class="empty">se încarcă…</div>
<div id="mWaQr"></div>
<div id="mWaPair" hidden>
<div class="detail" style="margin-top:var(--space-5)">
Nu poți scana codul QR? Cere un <strong>cod de asociere</strong> și
introdu-l pe telefon în WhatsApp → Dispozitive conectate → Conectează un
dispozitiv → <em>Conectează cu număr de telefon</em>.
</div>
<div class="actions" style="margin-top:var(--space-3)">
<input type="text" id="mPairPhone" placeholder="40723197939 (internațional, fără +)"
style="flex:1;min-width:220px">
<button onclick="mPair()">Cere cod de asociere</button>
</div>
<div id="mPairOut" class="detail" style="margin-top:var(--space-3)"></div>
</div>
</section>
<section class="card">
<h2>Trimise la suport <span class="count" id="mEscCount"></span></h2>
<div class="detail" id="mSupportDetail"></div>
<div id="mEsc" class="empty">se încarcă…</div>
</section>
<section class="card">
<h2>Clienți cunoscuți <span class="count" id="mClientCount"></span></h2>
<div class="table-scroll">
<table class="stack-mobil">
<thead><tr><th>Nume</th><th>Numere</th><th></th><th></th></tr></thead>
<tbody id="mClients"></tbody>
</table>
</div>
<h2 style="margin-top:var(--space-5)" id="mClientFormTitle">Adaugă client</h2>
<input type="text" id="mClientId" placeholder="id (ex: acme)" style="width:100%;margin-bottom:var(--space-3)">
<input type="text" id="mClientNume" placeholder="nume firmă" style="width:100%;margin-bottom:var(--space-3)">
<textarea id="mClientProfil" placeholder="profil (context liber despre client)..." style="width:100%;min-height:80px;margin-bottom:var(--space-3)"></textarea>
<textarea id="mClientNumere" placeholder="numere de telefon, unul pe linie" style="width:100%;min-height:80px;font-family:monospace"></textarea>
<div class="actions" style="margin-top:var(--space-3)">
<button class="primary" onclick="mSaveClient()">Salvează</button>
<button class="small" onclick="mResetClientForm()">Renunță</button>
</div>
<div class="detail" id="mClientMsg"></div>
</section>
<section class="card">
<h2>Documente indexate <span class="count" id="mDocCount"></span></h2>
<div class="detail" id="mIndexDetail"></div>
<div class="detail" id="mSyncDetail"></div>
<div class="actions" style="margin:var(--space-5) 0">
<button onclick="mReindex()">Reconstruiește indexul</button>
<button onclick="mSyncNow()">Sincronizează din Drive acum</button>
</div>
<div class="table-scroll">
<table class="stack-mobil">
<thead><tr><th>Nume</th><th class="num">Mărime</th><th></th></tr></thead>
<tbody id="mDocs"></tbody>
</table>
</div>
<h2 style="margin-top:var(--space-5)">Adaugă document (.txt / .md)</h2>
<input type="text" id="mDocName" placeholder="ex: politica-retururi.md" style="width:100%;margin-bottom:var(--space-3)">
<textarea id="mDocContent" placeholder="conținutul documentului..." style="width:100%;min-height:120px;font-family:monospace"></textarea>
<div class="actions" style="margin-top:var(--space-3)">
<button class="primary" onclick="mAddDoc()">Salvează și reindexează</button>
</div>
<div class="detail" id="mDocMsg"></div>
</section>
<section class="card">
<h2>Jurnal Maria
<span class="count">
<button class="small" id="mTabBridge" onclick="mSetLog('bridge')">whatsapp.log</button>
<button class="small" id="mTabRag" onclick="mSetLog('rag')">rag.log</button>
<button class="small" onclick="mRefreshLogs()">↻</button>
</span>
</h2>
<pre class="log" id="mLog">se încarcă…</pre>
</section>
</div>
<div id="toast"></div>
<script>
// ── tema ──────────────────────────────────────────────────────
(function () {
var saved = localStorage.getItem('theme');
if (saved) document.documentElement.setAttribute('data-theme', saved);
})();
function toggleTheme() {
var next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
}
// ── utilitare ─────────────────────────────────────────────────
function esc(s) {
return String(s === null || s === undefined ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function toast(msg, kind) {
var el = document.createElement('div');
el.className = 'toast ' + (kind || '');
el.textContent = msg;
document.getElementById('toast').appendChild(el);
setTimeout(function () { el.remove(); }, 6000);
}
async function api(path, opts) {
var r = await fetch(path, opts || {});
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 };
}
function post(path, body) {
return api(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body || {})
});
}
function dur(s) {
if (s === null || s === undefined) return '—';
s = Math.max(0, Math.floor(s));
var d = Math.floor(s / 86400), h = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60);
if (d) return d + 'z ' + h + 'h';
if (h) return h + 'h ' + m + 'm';
if (m) return m + 'm';
return s + 's';
}
function mb(bytes) {
return bytes === null || bytes === undefined ? '—' : (bytes / 1048576).toFixed(0) + ' MB';
}
// ── stare ─────────────────────────────────────────────────────
var busy = false;
async function refresh() {
var res = await api('api/status');
if (!res.ok) return;
var s = res.data, svcInfo = s.service;
// fara autentificare, butonul "Ieși" nu are ce sa faca
document.getElementById('btnLogout').hidden = (s.auth === false);
document.getElementById('unitName').textContent = svcInfo.unit;
document.getElementById('dot').className = 'dot ' + (svcInfo.active ? 'on' : 'off');
document.getElementById('lastPoll').textContent =
(s.user ? s.user + ' · ' : '') + 'actualizat ' + new Date().toLocaleTimeString('ro-RO');
var capTxt = s.cost_cap > 0 ? ' / ' + s.cost_cap.toFixed(2) : '';
document.getElementById('metrics').innerHTML = [
['Stare', '<span class="pill ' + (svcInfo.active ? 'ok' : 'bad') + '">' + esc(svcInfo.state) + '/' + esc(svcInfo.sub) + '</span>'],
['Uptime', dur(svcInfo.uptime_s)],
['PID', svcInfo.pid || '—'],
['Memorie cgroup', mb(svcInfo.memory_bytes)],
['Restarturi', svcInfo.restarts],
['Fire', s.threads.length],
['Cost azi', (Number((s.cost || {}).usd || 0)).toFixed(2) + capTxt + ' USD']
].map(function (m) {
return '<div class="metric"><div class="label">' + m[0] + '</div><div class="value">' + m[1] + '</div></div>';
}).join('');
document.getElementById('btnStart').disabled = busy || svcInfo.active;
document.getElementById('btnStop').disabled = busy || !svcInfo.active;
document.getElementById('btnRestart').disabled = busy;
// fire
document.getElementById('threadCount').textContent = s.threads.length + ' în state.json';
document.getElementById('threads').innerHTML = s.threads.length ? s.threads.map(function (t) {
var st = t.inflight ? '<span class="pill busy">tur în lucru</span>'
: t.alive ? '<span class="pill ok">proces viu</span>'
: '<span class="pill">inactiv</span>';
// data-label: pe telefon capul de tabel dispare si eticheta vine din CSS
return '<tr><td class="mono" data-label="Fir">' + esc(t.thread_id) +
'</td><td data-label="Model">' + esc(t.model || '—') +
'</td><td class="mono" data-label="Director">' + esc(t.cwd || '—') +
'</td><td data-label="Stare">' + st +
'</td><td class="num" data-label="USD">' + Number(t.cost_usd || 0).toFixed(2) +
'</td></tr>';
}).join('') : '<tr><td colspan="5" class="empty" data-label="">niciun fir înregistrat</td></tr>';
// aprobări
document.getElementById('apprCount').textContent = s.pending_approvals || '';
if (s.pending_approvals) { refreshApprovals(); }
else { document.getElementById('approvals').innerHTML = '<div class="empty">niciuna</div>'; }
}
async function refreshApprovals() {
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) {
return '<div class="check"><div style="flex:1">' +
'<div class="mono" style="color:var(--text-primary)">' + esc(a.command) + '</div>' +
'<div class="detail">' + esc(a.reason || a.rule || '') +
(a.expires_in !== null && a.expires_in !== undefined ? ' · expiră în ' + dur(a.expires_in) : '') +
' · fir ' + esc(a.thread_id) + '</div></div>' +
'<div class="check-actions">' +
'<button class="small primary" onclick="decide(\'' + esc(a.request_id) + '\',\'allow\')">Permite</button>' +
'<button class="small" title="Permite și nu mai întreba pentru operațiuni de același fel în acest fir" onclick="decide(\'' + esc(a.request_id) + '\',\'allow_session\')">Permite (tot firul)</button>' +
'<button class="small danger" onclick="decide(\'' + esc(a.request_id) + '\',\'deny\')">Refuză</button>' +
'</div></div>';
}).join('') : '<div class="empty">niciuna</div>';
}
async function decide(id, 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');
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">' +
(c.pass ? '✓' : '✗') + '</span><span class="name">' + esc(c.name) +
'</span><span class="detail">' + esc(c.detail) + '</span></div>';
}).join('');
}
// ── acțiuni ───────────────────────────────────────────────────
async function svc(action, force) {
if (busy) return;
if (action !== 'start' && !force &&
!confirm('Sigur „' + action + '” pe serviciul punții?')) return;
busy = true;
try {
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?')) {
busy = false;
return svc(action, true);
}
toast('Anulat — tururile continuă.', '');
} else if (res.ok) {
toast('Serviciu: ' + action + ' OK', 'ok');
} else {
toast('Eșuat: ' + (res.data.error || res.status), 'bad');
}
} finally {
busy = false;
refresh();
refreshDoctor();
}
}
async function cleanup(dry) {
if (!dry && !confirm('Omor procesele orfane găsite?')) return;
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');
document.getElementById('log').textContent = res.data.report || '(fără raport)';
}
async function restartSelf() {
if (!confirm('Repornesc dashboard-ul? Pagina se reîncarcă în câteva secunde.')) return;
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';
}
// ── jurnal ────────────────────────────────────────────────────
var logFile = 'bot';
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);
if (!res.ok) return;
var el = document.getElementById('log');
el.textContent = (res.data.lines || []).join('\n') || '(gol)';
el.scrollTop = el.scrollHeight;
}
// ── Maria: WhatsApp + RAG ───────────────────────────────────────
var mBusy = false;
function mMetricsHtml(info) {
return [
['Stare', '<span class="pill ' + (info.active ? 'ok' : 'bad') + '">' + esc(info.state) + '/' + esc(info.sub) + '</span>'],
['Uptime', dur(info.uptime_s)],
['PID', info.pid || '—'],
['Restarturi', info.restarts]
].map(function (m) {
return '<div class="metric"><div class="label">' + m[0] + '</div><div class="value">' + m[1] + '</div></div>';
}).join('');
}
function mShowPairCode(code, expiresIn) {
var pretty = code.length === 8 ? code.slice(0, 4) + '-' + code.slice(4) : code;
var out = document.getElementById('mPairOut');
out.dataset.code = code;
out.innerHTML =
'<div style="font-family:monospace;font-size:1.9rem;letter-spacing:.18em;margin:var(--space-3) 0">'
+ esc(pretty) + '</div><span class="muted">tastează-l pe telefon'
+ (expiresIn ? ' — expiră în ~' + Math.round(expiresIn / 60) + ' min' : '')
+ '. Dacă expiră, cere altul.</span>';
}
async function mPair() {
var phone = document.getElementById('mPairPhone').value.trim();
var out = document.getElementById('mPairOut');
out.textContent = 'cer codul…';
var res = await post('api/maria/pair', { phone: phone });
if (res.ok && res.data && res.data.code) {
mShowPairCode(res.data.code, res.data.expires_in);
} else {
out.innerHTML = '<span class="pill bad">eroare</span> '
+ esc((res.data && res.data.error) || 'nu am putut cere codul');
}
mRefresh();
}
async function mRefresh() {
var res = await api('api/maria/status');
if (!res.ok) return;
var s = res.data;
document.getElementById('mBridgeUnit').textContent = s.bridge.unit;
document.getElementById('mBridgeMetrics').innerHTML = mMetricsHtml(s.bridge);
document.getElementById('mRagUnit').textContent = s.rag.unit;
document.getElementById('mRagMetrics').innerHTML = mMetricsHtml(s.rag);
var wa = s.whatsapp || {};
var waStatus = document.getElementById('mWaStatus');
var waQr = document.getElementById('mWaQr');
// Cardul de asociere are rost doar cât timp puntea rulează și nu e conectată.
document.getElementById('mWaPair').hidden = !!wa.connected || wa.reachable === false;
var pairOut = document.getElementById('mPairOut');
if (wa.pairing && wa.pairing.code) {
mShowPairCode(wa.pairing.code, wa.pairing.expires_in);
} else if (!wa.connected && pairOut.dataset.code) {
// Codul afisat a expirat la punte. Lasat pe ecran, ar trimite omul sa
// tasteze un cod pe care WhatsApp il refuza fara sa spuna de ce.
pairOut.innerHTML = '<span class="muted">codul a expirat — cere altul</span>';
delete pairOut.dataset.code;
}
if (wa.connected) {
waStatus.innerHTML = '<span class="pill ok">conectat</span> ca ' + esc(wa.phone || '?');
waQr.innerHTML = '';
document.getElementById('mPairOut').innerHTML = '';
} else if (wa.qr) {
waStatus.innerHTML = '<span class="pill bad">neconectat</span> — scanează codul QR din WhatsApp → Dispozitive conectate';
waQr.innerHTML = '<img src="' + wa.qr + '" style="width:220px;height:220px;border-radius:8px;background:#fff;padding:8px;margin-top:var(--space-4)">';
} else {
waStatus.innerHTML = '<span class="pill bad">' + (wa.reachable === false ? 'puntea nu rulează' : 'aștept codul QR…') + '</span>';
waQr.innerHTML = '';
}
document.getElementById('mIndexDetail').textContent =
s.index.chunks + ' chunk-uri din ' + s.index.documents_indexed + ' documente indexate' +
(s.index.mtime ? ' · indexat ' + new Date(s.index.mtime * 1000).toLocaleString('ro-RO') : '');
var sync = s.sync || {};
document.getElementById('mSyncDetail').textContent = sync.drive_remote
? 'Drive: ' + sync.drive_remote + (sync.synced_at ? ' · ultima sincronizare ' + new Date(sync.synced_at * 1000).toLocaleString('ro-RO') : ' · încă nesincronizat')
: 'Sincronizare Drive dezactivată (DRIVE_REMOTE nesetat) — doar upload manual';
document.getElementById('mSupportDetail').textContent = s.support_jid
? 'Canal de suport: ' + s.support_jid
: 'SUPPORT_JID nesetat — escaladările se scriu doar în jurnal, nu anunță pe nimeni';
var escRes = await api('api/maria/escalations');
var escaladari = escRes.ok ? (escRes.data.escalations || []) : [];
document.getElementById('mEscCount').textContent = escaladari.length ? escaladari.length + ' recente' : '';
document.getElementById('mEsc').className = escaladari.length ? '' : 'empty';
document.getElementById('mEsc').innerHTML = escaladari.length ? escaladari.map(function (e) {
var stare = e.notified
? '<span class="pill ok">trimis</span>'
: '<span class="pill bad">netrimis' + (e.notify_error ? ' — ' + esc(e.notify_error) : '') + '</span>';
return '<div style="border-top:1px solid var(--border);padding:var(--space-4) 0">'
+ '<div>' + stare + ' <code>' + esc(e.ref || '') + '</code> <strong>' + esc(e.from || '?') + '</strong> '
+ '<span class="muted">' + (e.at ? new Date(e.at * 1000).toLocaleString('ro-RO') : '') + '</span>'
+ (e.had_image ? ' <span class="muted">· captură</span>' : '') + '</div>'
+ '<pre style="white-space:pre-wrap;margin:var(--space-3) 0;font-size:.85em">' + esc(e.text || '(fără text)') + '</pre>'
+ '<div class="muted">' + esc(e.reason || '') + '</div></div>';
}).join('') : 'nicio întrebare trimisă la suport';
var docsRes = await api('api/maria/documents');
var docs = docsRes.ok ? (docsRes.data.documents || []) : [];
var indexate = docs.filter(function (d) { return !d.shadowed_by; }).length;
document.getElementById('mDocCount').textContent =
docs.length + ' în depozit' + (indexate !== docs.length ? ' · ' + indexate + ' indexate' : '');
document.getElementById('mDocs').innerHTML = docs.length ? docs.map(function (d) {
var local = d.local ? ' <span class="muted" title="nu vine din Drive; sincronizarea nu il sterge">· local</span>' : '';
var umbrit = d.shadowed_by
? ' <span class="muted" title="acelaşi document exista intr-un format mai bogat">· umbrit de ' + esc(d.shadowed_by) + '</span>'
: '';
return '<tr><td data-label="Nume" class="mono">' + esc(d.name) + local + umbrit + '</td>' +
'<td data-label="Mărime" class="num">' + d.size + ' B</td>' +
'<td><button class="small danger" onclick="mDelDoc(\'' + esc(d.name) + '\')">Șterge</button></td></tr>';
}).join('') : '<tr><td colspan="3" class="empty" data-label="">niciun document</td></tr>';
}
async function mSvc(which, action) {
if (mBusy) return;
if (action !== 'start' && !confirm('Sigur „' + action + '” pe ' + which + '?')) return;
mBusy = true;
try {
var res = await post('api/maria/service', { service: which, action: action });
toast(res.ok ? 'Maria ' + which + ': ' + action + ' OK' : ('Eșuat: ' + (res.data.error || res.status)), res.ok ? 'ok' : 'bad');
} finally {
mBusy = false;
mRefresh();
}
}
async function mReindex() {
var res = await post('api/maria/reindex', {});
toast(res.ok && res.data.ok ? 'Index reconstruit' : ('Eșuat: ' + (res.data.error || res.data.stderr || res.status)), res.ok && res.data.ok ? 'ok' : 'bad');
mRefresh();
}
async function mSyncNow() {
var res = await post('api/maria/sync', { force: true });
toast(res.ok && res.data.ok ? 'Sincronizat' : ('Eșuat: ' + (res.data.error || res.data.stderr || res.status)), res.ok && res.data.ok ? 'ok' : 'bad');
mRefresh();
}
async function mAddDoc() {
var name = document.getElementById('mDocName').value.trim();
var content = document.getElementById('mDocContent').value;
var msg = document.getElementById('mDocMsg');
if (!name || !content) { msg.textContent = 'completează numele și conținutul'; return; }
var res = await post('api/maria/documents', { name: name, content: content });
if (!res.ok) { msg.textContent = 'eșuat: ' + (res.data.error || res.status); return; }
msg.textContent = 'salvat, reindexez…';
var ri = await post('api/maria/reindex', {});
msg.textContent = ri.ok && ri.data.ok ? 'salvat și reindexat' : ('salvat, reindexarea a eșuat: ' + (ri.data.error || ri.data.stderr || ''));
document.getElementById('mDocContent').value = '';
mRefresh();
}
async function mDelDoc(name) {
if (!confirm('Ștergi ' + name + '?')) return;
await post('api/maria/documents/delete', { name: name });
await mReindex();
}
var mClientsCache = [];
async function mLoadClients() {
var res = await api('api/maria/clients');
var clienti = res.ok ? (res.data.clients || []) : [];
mClientsCache = clienti;
document.getElementById('mClientCount').textContent = clienti.length ? clienti.length : '';
document.getElementById('mClients').innerHTML = clienti.length ? clienti.map(function (c) {
return '<tr><td data-label="Nume">' + esc(c.nume || c.id) + '</td>'
+ '<td data-label="Numere" class="mono">' + esc((c.numere || []).join(', ')) + '</td>'
+ '<td><button class="small" onclick="mEditClient(\'' + esc(c.id) + '\')">Editează</button></td>'
+ '<td><button class="small danger" onclick="mDelClient(\'' + esc(c.id) + '\')">Șterge</button></td></tr>';
}).join('') : '<tr><td colspan="4" class="empty" data-label="">niciun client</td></tr>';
}
function mEditClient(id) {
var c = mClientsCache.find(function (x) { return x.id === id; });
if (!c) return;
document.getElementById('mClientId').value = c.id;
document.getElementById('mClientId').disabled = true;
document.getElementById('mClientNume').value = c.nume || '';
document.getElementById('mClientProfil').value = c.profil || '';
document.getElementById('mClientNumere').value = (c.numere || []).join('\n');
document.getElementById('mClientFormTitle').textContent = 'Editează client: ' + (c.nume || c.id);
document.getElementById('mClientMsg').textContent = '';
}
function mResetClientForm() {
document.getElementById('mClientId').value = '';
document.getElementById('mClientId').disabled = false;
document.getElementById('mClientNume').value = '';
document.getElementById('mClientProfil').value = '';
document.getElementById('mClientNumere').value = '';
document.getElementById('mClientFormTitle').textContent = 'Adaugă client';
document.getElementById('mClientMsg').textContent = '';
}
async function mSaveClient() {
var id = document.getElementById('mClientId').value.trim();
var nume = document.getElementById('mClientNume').value.trim();
var profil = document.getElementById('mClientProfil').value;
var numere = document.getElementById('mClientNumere').value;
var msg = document.getElementById('mClientMsg');
if (!id || !nume) { msg.textContent = 'completează id și nume'; return; }
var res = await post('api/maria/clients', { id: id, nume: nume, profil: profil, numere: numere });
if (!res.ok) { msg.textContent = 'eșuat: ' + (res.data.error || res.status); return; }
mResetClientForm();
await mLoadClients();
}
async function mDelClient(id) {
if (!confirm('Ștergi clientul ' + id + '?')) return;
await post('api/maria/clients/delete', { id: id });
await mLoadClients();
}
var mLogFile = 'bridge';
function mSetLog(which) { mLogFile = which; mRefreshLogs(); }
async function mRefreshLogs() {
document.getElementById('mTabBridge').className = 'small' + (mLogFile === 'bridge' ? ' primary' : '');
document.getElementById('mTabRag').className = 'small' + (mLogFile === 'rag' ? ' primary' : '');
var res = await api('api/maria/logs?lines=300&service=' + mLogFile);
if (!res.ok) return;
var el = document.getElementById('mLog');
el.textContent = (res.data.lines || []).join('\n') || '(gol)';
el.scrollTop = el.scrollHeight;
}
// ── pornire ───────────────────────────────────────────────────
refresh(); refreshDoctor(); refreshLogs();
mRefresh(); mRefreshLogs(); mLoadClients();
setInterval(refresh, 5000);
setInterval(refreshDoctor, 30000);
setInterval(mRefresh, 4000);
</script>
</body>
</html>