feat(maria-dashboard): reset pentru firele de discutie active
Nu exista nicio cale sa resetezi manual un fir blocat pe o discutie veche (nici comanda de chat, nici buton) — un test in grupul "Maria Test" a ramas ancorat pe un subiect vechi fara sa poata fi curatat fara interventie directa pe server. Sectiune noua in dashboard, intre "Trimise la suport" si "Clienti cunoscuti": listeaza firele active din ~/.maria-bridge/conversations/ si le sterge la cerere (rutele GET/POST /api/maria/fire), cu aceeasi sanitizare de nume de fisier ca rag/fir.py (oglinda, nu import — coliziune de modul intre proiecte). 11/11 teste (6 noi + cele de clienti), fara regresii. Co-Authored-By: Claude Agent <noreply@anthropic.com>
This commit is contained in:
@@ -291,6 +291,46 @@ def maria_delete_client(cid: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _maria_conv_dir():
|
||||
return MARIA_STATE_DIR / "conversations"
|
||||
|
||||
|
||||
def _maria_cale_fir(jid: str):
|
||||
# oglinda lui rag/fir.py:_cale — tine-o la fel
|
||||
if not jid:
|
||||
raise ValueError("jid lipsa")
|
||||
return _maria_conv_dir() / (re.sub(r"[^0-9a-zA-Z@._-]", "_", jid) + ".json")
|
||||
|
||||
|
||||
def maria_fire() -> list[dict]:
|
||||
d = _maria_conv_dir()
|
||||
if not d.is_dir():
|
||||
return []
|
||||
out = []
|
||||
for f in d.glob("*.json"):
|
||||
try:
|
||||
fir = json.loads(f.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
out.append({
|
||||
"jid": fir.get("jid"),
|
||||
"at": fir.get("at"),
|
||||
"ancora": (fir.get("ancora") or "")[:200],
|
||||
"schimburi": len(fir.get("schimburi") or []),
|
||||
"ref": fir.get("ref"),
|
||||
})
|
||||
out.sort(key=lambda x: x.get("at") or 0, reverse=True)
|
||||
return out
|
||||
|
||||
|
||||
def maria_delete_fir(jid: str) -> bool:
|
||||
path = _maria_cale_fir(jid)
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _maria_run(script: str, timeout: float = 600.0, extra_args: list[str] | None = None) -> dict:
|
||||
interpreter = str(MARIA_VENV_PY) if MARIA_VENV_PY.exists() else sys.executable
|
||||
args = [interpreter, str(MARIA_RAG_DIR / script), *(extra_args or [])]
|
||||
@@ -805,6 +845,8 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
return self.send_json({"escalations": maria_escalations()})
|
||||
if path == "/api/maria/clients":
|
||||
return self.send_json({"clients": maria_clients()})
|
||||
if path == "/api/maria/fire":
|
||||
return self.send_json({"fire": maria_fire()})
|
||||
if path == "/api/maria/logs":
|
||||
try:
|
||||
n = min(max(int(qs.get("lines", ["200"])[0]), 1), 2000)
|
||||
@@ -849,6 +891,8 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
return self.handle_maria_client_write()
|
||||
if path == "/api/maria/clients/delete":
|
||||
return self.handle_maria_client_delete()
|
||||
if path == "/api/maria/fire/delete":
|
||||
return self.handle_maria_fire_delete()
|
||||
if path == "/api/maria/reindex":
|
||||
return self.send_json(maria_reindex())
|
||||
if path == "/api/maria/sync":
|
||||
@@ -988,6 +1032,15 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
ok = maria_delete_client(cid)
|
||||
return self.send_json({"ok": ok, "id": cid}, 200 if ok else 404)
|
||||
|
||||
def handle_maria_fire_delete(self):
|
||||
data = self.read_json()
|
||||
jid = str(data.get("jid") or "")
|
||||
try:
|
||||
ok = maria_delete_fir(jid)
|
||||
except ValueError as exc:
|
||||
return self.send_json({"ok": False, "error": str(exc)}, 400)
|
||||
return self.send_json({"ok": ok, "jid": jid}, 200 if ok else 404)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
bind = config.get("DASHBOARD_BIND") or "127.0.0.1"
|
||||
|
||||
@@ -120,6 +120,16 @@
|
||||
<div id="mEsc" class="empty">se încarcă…</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Fire de discuție active <span class="count" id="mFirCount"></span></h2>
|
||||
<div class="table-scroll">
|
||||
<table class="stack-mobil">
|
||||
<thead><tr><th>JID</th><th>Subiect</th><th>Ultima activitate</th><th></th></tr></thead>
|
||||
<tbody id="mFire"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Clienți cunoscuți <span class="count" id="mClientCount"></span></h2>
|
||||
<div class="table-scroll">
|
||||
@@ -596,6 +606,24 @@ async function mDelClient(id) {
|
||||
await mLoadClients();
|
||||
}
|
||||
|
||||
async function mLoadFire() {
|
||||
var res = await api('api/maria/fire');
|
||||
var fire = res.ok ? (res.data.fire || []) : [];
|
||||
document.getElementById('mFirCount').textContent = fire.length ? fire.length : '';
|
||||
document.getElementById('mFire').innerHTML = fire.length ? fire.map(function (f) {
|
||||
return '<tr><td data-label="JID" class="mono">' + esc(f.jid) + '</td>'
|
||||
+ '<td data-label="Subiect">' + esc(f.ancora) + '</td>'
|
||||
+ '<td data-label="Ultima activitate">' + (f.at ? new Date(f.at * 1000).toLocaleString('ro-RO') : '—') + '</td>'
|
||||
+ '<td><button class="small danger" onclick="mDelFir(\'' + esc(f.jid) + '\')">Resetează</button></td></tr>';
|
||||
}).join('') : '<tr><td colspan="4" class="empty" data-label="">niciun fir activ</td></tr>';
|
||||
}
|
||||
|
||||
async function mDelFir(jid) {
|
||||
if (!confirm('Resetezi firul cu ' + jid + '? Maria va porni o discutie noua la urmatorul mesaj.')) return;
|
||||
await post('api/maria/fire/delete', { jid: jid });
|
||||
await mLoadFire();
|
||||
}
|
||||
|
||||
var mLogFile = 'bridge';
|
||||
function mSetLog(which) { mLogFile = which; mRefreshLogs(); }
|
||||
async function mRefreshLogs() {
|
||||
@@ -610,7 +638,7 @@ async function mRefreshLogs() {
|
||||
|
||||
// ── pornire ───────────────────────────────────────────────────
|
||||
refresh(); refreshDoctor(); refreshLogs();
|
||||
mRefresh(); mRefreshLogs(); mLoadClients();
|
||||
mRefresh(); mRefreshLogs(); mLoadClients(); mLoadFire();
|
||||
setInterval(refresh, 5000);
|
||||
setInterval(refreshDoctor, 30000);
|
||||
setInterval(mRefresh, 4000);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Teste pentru firele de discutie Maria din dashboard/api.py.
|
||||
|
||||
Apeleaza direct functiile, cu MARIA_STATE_DIR mutat in tmp — la fel ca
|
||||
test_maria_clients.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "dashboard"))
|
||||
|
||||
api = pytest.importorskip("api", reason="dashboard/api.py")
|
||||
|
||||
JID = "120363409761730101@g.us"
|
||||
|
||||
|
||||
def _sanitizat(jid: str) -> str:
|
||||
# aceeasi transformare ca rag/fir.py:_cale
|
||||
return re.sub(r"[^0-9a-zA-Z@._-]", "_", jid) + ".json"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def maria_dir(tmp_path, monkeypatch):
|
||||
d = tmp_path / "maria-bridge"
|
||||
d.mkdir()
|
||||
monkeypatch.setattr(api, "MARIA_STATE_DIR", d)
|
||||
return d
|
||||
|
||||
|
||||
def _scrie_fir(d, jid=JID, ancora="ORA-06550 la salvarea facturii", schimburi=None):
|
||||
conv = d / "conversations"
|
||||
conv.mkdir(parents=True, exist_ok=True)
|
||||
fir = {"jid": jid, "at": 1700000000.0, "ancora": ancora,
|
||||
"schimburi": schimburi if schimburi is not None else [{"rol": "om", "text": "buna"}]}
|
||||
(conv / _sanitizat(jid)).write_text(json.dumps(fir, ensure_ascii=False), encoding="utf-8")
|
||||
return fir
|
||||
|
||||
|
||||
def test_lista_fire(maria_dir):
|
||||
_scrie_fir(maria_dir)
|
||||
fire = api.maria_fire()
|
||||
assert len(fire) == 1
|
||||
assert fire[0]["jid"] == JID
|
||||
assert fire[0]["ancora"] == "ORA-06550 la salvarea facturii"
|
||||
assert fire[0]["schimburi"] == 1
|
||||
|
||||
|
||||
def test_sterge_fir(maria_dir):
|
||||
_scrie_fir(maria_dir)
|
||||
assert api.maria_delete_fir(JID) is True
|
||||
assert not (maria_dir / "conversations" / _sanitizat(JID)).exists()
|
||||
|
||||
|
||||
def test_sterge_a_doua_oara_esueaza(maria_dir):
|
||||
_scrie_fir(maria_dir)
|
||||
api.maria_delete_fir(JID)
|
||||
assert api.maria_delete_fir(JID) is False
|
||||
|
||||
|
||||
def test_fisier_corupt_e_ignorat(maria_dir):
|
||||
conv = maria_dir / "conversations"
|
||||
conv.mkdir(parents=True, exist_ok=True)
|
||||
(conv / "stricat.json").write_text("{nu e json", encoding="utf-8")
|
||||
assert api.maria_fire() == []
|
||||
|
||||
|
||||
def test_director_lipsa(maria_dir):
|
||||
assert api.maria_fire() == []
|
||||
|
||||
|
||||
def test_jid_gol_ridica_valueerror(maria_dir):
|
||||
with pytest.raises(ValueError):
|
||||
api.maria_delete_fir("")
|
||||
Reference in New Issue
Block a user