feat(maria): asociere prin cod de telefon si citirea capturilor cu erori

Doua lucruri cerute de utilizator, ambele pe acelasi drum: sa poti lega
puntea fara sa ai un ecran de scanat, si sa poti trimite Mariei o poza cu
eroarea in loc sa transcrii mesajul.

Asociere prin cod de 8 caractere (alternativa la QR):
- endpoint-ul /pair exista, dar nu era folosibil: fara `browser` explicit
  WhatsApp refuza codul, iar dupa introducerea lui corecta serverul cere un
  restart (515) care consuma din bugetul de reincercari si putea opri puntea.
  Acum descriptorul e Browsers.ubuntu('Chrome') si restartRequired reconecteaza
  imediat, fara sa numere.
- codul are TTL de 3 minute, iar /status il da doar cat timp e valabil —
  un cod expirat afisat in dashboard trimite omul sa tasteze degeaba.
- dashboard: camp pentru numar + buton, in acelasi card cu QR-ul.

Imagini cu erori (capturi de ecran):
- puntea descarca imaginile in ~/.maria-bridge/media/ (imageMessage sau
  document cu mimetype image/*, si prin ambalajele efemer/"vezi o data" —
  fara despachetare pareau mesaje fara continut si se aruncau tacut).
- rag/ocr.py: tesseract ron+eng. Modelul de raspuns e strict text, deci OCR
  nu e o optiune de calitate, e singura cale.
- cautarea in index merge DOAR pe liniile care arata a eroare; o fereastra
  intreaga de meniuri si totaluri dilueaza embedding-ul si scoate chunk-uri
  fara legatura. Modelul primeste captura intreaga, marcata ca text OCR.
- capturile se sterg imediat dupa citire (pot contine date de client).
- cand nu se citeste nimic si nu exista legenda, Maria cere textul erorii
  in loc sa raspunda in gol.

16 teste noi (42 in total). install.sh verifica tesseract; README documenteaza
ambele metode de asociere si drumul unei capturi.

Separat, in docs/chatboti-si-punti.md: chatul "Eu" e vazut de AMBELE punti de pe
numar. Puntea lui Echo (LXC 110) e asociata ca dispozitiv :11 al aceluiasi cont,
momentan nelegata dar pornita — daca se reasociaza, raspunde in "Eu" langa Maria.
Notat si ca serviciile lui Echo sunt unitati de UTILIZATOR: `systemctl is-active`
ca root raspunde "inactive" desi botul ruleaza.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
This commit is contained in:
Claude Agent
2026-08-31 21:22:28 +00:00
parent f5c8df7adf
commit 7a1d2a2073
12 changed files with 733 additions and 36 deletions

View File

@@ -98,6 +98,29 @@ def maria_whatsapp_status() -> dict:
return {"connected": False, "phone": None, "qr": None, "reachable": False}
def maria_request_pairing_code(phone: str) -> tuple[dict, int]:
"""Cere puntii un cod de asociere de 8 caractere pentru `phone`.
Dashboard-ul nu decide nimic despre numar — puntea valideaza si raspunde; noi
doar transmitem, ca sa nu existe doua reguli de validare care se pot desincroniza.
"""
body = json.dumps({"phone": phone}).encode("utf-8")
req = urllib.request.Request(
f"{maria_bridge_url()}/pair", data=body,
headers={"Content-Type": "application/json"}, method="POST",
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8")), resp.status
except urllib.error.HTTPError as exc:
try:
return json.loads(exc.read().decode("utf-8")), exc.code
except ValueError:
return {"ok": False, "error": f"puntea a raspuns {exc.code}"}, exc.code
except (urllib.error.URLError, TimeoutError, OSError, ValueError) as exc:
return {"ok": False, "error": f"puntea WhatsApp nu raspunde: {exc}"}, 503
def maria_index_info() -> dict:
try:
entries = json.loads(MARIA_INDEX_FILE.read_text(encoding="utf-8"))
@@ -710,6 +733,8 @@ class Handler(SimpleHTTPRequestHandler):
return self.handle_decide()
if path == "/api/maria/service":
return self.handle_maria_service()
if path == "/api/maria/pair":
return self.handle_maria_pair()
if path == "/api/maria/documents":
return self.handle_maria_document_write()
if path == "/api/maria/documents/delete":
@@ -803,6 +828,16 @@ class Handler(SimpleHTTPRequestHandler):
time.sleep(1.0)
return self.send_json({"ok": True, "action": action, "service": unit_info(unit)})
def handle_maria_pair(self):
"""Cod de asociere WhatsApp — alternativa la QR, cand nu poti scana ecranul."""
data = self.read_json()
phone = str(data.get("phone") or "")
who = tailnet_user(self.headers) or "local"
print(f"[actiune] cod de asociere WhatsApp pentru {phone}, cerut de {who}",
file=sys.stderr, flush=True)
out, status = maria_request_pairing_code(phone)
return self.send_json(out, status)
def handle_maria_document_write(self):
data = self.read_json()
name = str(data.get("name") or "")