Files
ROMFASTSQL/proxmox/lxc171-claude-agent/discord-bridge/dashboard/index.html
Claude Agent ca6b53e0ca feat(maria): rank hibrid, escaladare la suport si dictionar de erori Oracle
Trei probleme legate, gasite testand cu capturi reale.

1. RASPUNSURI GENERICE. Cand raspunsul nu era in documente, modelul primea
   oricum top-3 chunk-uri si compunea ceva plauzibil — utilizatorul pleca cu
   impresia ca a primit ajutor. Acum, daca nu avem acoperire, modelul nu mai e
   intrebat deloc: intrebarea pleaca la suport, cu referinta (M-260831-A1B2) si
   confirmare onesta — "am trimis" doar daca notificarea chiar a plecat, altfel
   "am inregistrat, dar nu am putut trimite". Jurnalul se scrie intotdeauna, in
   ~/.maria-bridge/escalations/, si se vede in dashboard. Daca a fost o captura,
   imaginea insasi se retrimite la suport (endpoint nou /send-image, limitat la
   MEDIA_DIR).

2. ORDONARE (rank.py). Un prag pe cosinus nu putea decide "avem raspunsul?":
   masurat pe indexul viu, intrebarile bune dau 0,600-0,816 si cele straine
   0,534-0,685 — intervale care SE SUPRAPUN. Lipseau codurile: ORA-01722, D406,
   CIF sunt tokeni exacti pe care cautarea semantica ii topeste. Acum se ordoneaza
   de doua ori — embeddings si BM25 — si se fuzioneaza clasamentele (RRF), iar
   decizia de acoperire se ia pe dovezi: cosinus mare, sau cosinus de mijloc cu o
   fractiune din termenii distinctivi gasita chiar in chunk-uri. Fractiunea conteaza:
   cu un singur termen, "cum imi resetez parola de la Windows" trecea drept
   acoperita fiindca "parola" apare in documente.
   Pragurile sunt masurate, nu alese: ops/calibrate-rank.py ruleaza un set de cazuri
   pe indexul real si matura grila. 15/15 la strong=0.70 weak=0.58 ratio=0.5.

3. DICTIONAR DE ERORI ORACLE. 29 de erori uzuale, scrise pentru utilizatorul din
   fata aplicatiei: ce inseamna, ce poate incerca singur, cand sa sune. Fara nume
   de servere, IP-uri sau pasi de administrare — Maria e chatbot pentru clienti.
   Fisierul din git e SAMANTA: depozitul e oglinda Drive-ului, deci trebuie pus in
   document_store ca sa nu dispara la sincronizare.

Pe drum, doua lucruri gasite in log:
- fiecare mesaj din self-chat sosea de DOUA ori, pe @s.whatsapp.net si pe @lid,
  deci Maria raspundea de doua ori. Dedup pe key.id in punte.
- reindexarea reface toate embeddings-urile (~7 s fiecare pe CPU): adaugarea unui
  document la 170 de chunk-uri insemna 20 de minute. Acum refoloseste vectorii
  chunk-urilor nemodificate din indexul precedent, fara vreun fisier nou de stare.

Textul OCR se logheaza acum INTEGRAL, cu interogarea de cautare si sursele alese
cu scorurile lor — fara asta un raspuns gresit nu se poate explica: nu stiai daca
a citit prost captura sau a cautat prost in documente.

6 fisiere de test noi/extinse, 68 pass (de la 42).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
2026-08-31 22:08:50 +00:00

544 lines
25 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>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 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) + 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 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();
setInterval(refresh, 5000);
setInterval(refreshDoctor, 30000);
setInterval(mRefresh, 4000);
</script>
</body>
</html>