fix(maria): lacat intre reindexari, scriere atomica a indexului, XML invalid raportat

Descoperit la prima sincronizare reala din Drive: `maria-sync.timer` a pornit
peste rularea manuala si doua procese faceau embeddings in paralel pe acelasi
Ollama, ambele urmand sa scrie acelasi rag_index.json. Embedding-ul a incetinit
de la ~7s la ~20s din concurenta, iar ultimul care termina ar fi suprascris
munca celuilalt.

- config.exclusive(): lacat `flock` intre procese, luat la intrarea in sync.py si
  indexer.py. Nu asteapta — a doua rulare iese curat cu "o reindexare e deja in
  curs", fiindca ar reface exact acelasi lucru. Verificat pe procese reale.
- indexer scrie indexul atomic (tmp + os.replace): consumer-ul reciteste fisierul
  la 30s si putea prinde un JSON pe jumatate scris.
- build() intoarce `warnings` pentru XML-urile care nu se pot parsa, iar rularea
  din linia de comanda le scrie in stderr. Pana acum, un XML invalid se indexa
  tacut ca text simplu, cu o singura linie pierduta in log.

Context de performanta, masurat pe LXC 171 fara alta incarcare: un embedding
`nomic-embed-text` ia ~7,3s, deci o reindexare completa a celor 173 de chunk-uri
dureaza ~21 de minute — mai mult decat intervalul timer-ului. Nu e o problema
practica (amprenta reindexeaza doar la schimbare, iar lacatul opreste
suprapunerea), dar explica de ce prima rulare pare blocata.

docs/rclone-google-drive-headless.md: procedura de conectare a unui container
headless la Drive prin `rclone authorize`, cu transcriptul rularii reale de pe
Windows, capcanele (sync e distructiv pe destinatie, connection string in loc de
cale pe nume, unde stau secretele) si de ce nu contul de serviciu. Indexata in
CLAUDE.md.

6 teste noi (lacat, eliberare la exceptie, scriere atomica, avertismente).

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 18:42:01 +00:00
parent f3f5eedb79
commit 3b7069e954
6 changed files with 243 additions and 5 deletions

View File

@@ -0,0 +1,117 @@
# rclone + Google Drive pe un container headless (fără cont de serviciu)
Procedura de conectare a unui container Linux fără browser la Google Drive.
Folosită prima dată pentru depozitul de documente al Mariei (LXC 171,
`maria-whatsapp-bridge`), dar e generală — se aplică oricărui container din cluster.
**Ideea:** autorizarea OAuth are nevoie de un browser, containerul nu are. Deci
autorizezi pe o mașină care are browser (stația Windows), iar rclone îți dă un token
pe care îl muți pe container. Nu e nevoie de cont de serviciu și nu se atinge deloc
consola Google Cloud.
## De ce nu contul de serviciu
Prima variantă documentată era un service account în Google Cloud Console, cu dosarul
din Drive partajat către adresa lui. Funcționează, dar cere: proiect în Google Cloud,
activare Drive API, creare cont, descărcare JSON, partajare manuală a dosarului. Șapte
pași în două interfețe. `rclone authorize` face același lucru în doi pași, dintr-o
fereastră de browser deja autentificată.
Contul de serviciu rămâne varianta mai bună **doar** dacă ai nevoie de acces care nu
depinde de niciun cont de om (ex. un serviciu care trebuie să meargă și după ce omul
respectiv pleacă din firmă).
## Pasul 1 — autorizezi pe Windows
Descarcă arhiva portabilă de la <https://rclone.org/downloads/> (`rclone-vX-windows-amd64.zip`),
dezarhiveaz-o oriunde — nu cere instalare. În acel dosar:
```
rclone.exe authorize "drive" --drive-scope=drive.readonly
```
`drive.readonly` fiindcă sincronizarea doar citește. Dacă ai nevoie și de scriere,
folosește `drive`, dar atunci un `rclone sync` greșit poate șterge în Drive.
Se deschide browserul. Dacă nu se deschide, rclone scrie linkul în consolă. Cum arată
o rulare reușită:
```
C:\Users\mmari\Downloads\rclone-v1.75.0-windows-amd64>rclone.exe authorize "drive" --drive-scope=drive.readonly
NOTICE: Config file "C:\\Users\\mmari\\AppData\\Roaming\\rclone\\rclone.conf" not found - using defaults
NOTICE: Make sure your Redirect URL is set to "http://127.0.0.1:53682/" in your custom config.
NOTICE: If your browser doesn't open automatically go to the following link: http://127.0.0.1:53682/auth?state=...
NOTICE: Log in and authorize rclone for access
NOTICE: Waiting for code...
NOTICE: Got code
Paste the following into your remote machine --->
{"access_token":"ya29...","token_type":"Bearer","refresh_token":"1//03...","expiry":"...","expires_in":3599}
<---End paste
```
Cele două mesaje despre „Config file not found" și „Redirect URL" **nu sunt erori** —
`authorize` nu scrie niciun config pe Windows, doar obține tokenul. Dosarul rclone de
pe Windows poate fi șters după.
Copiază tot JSON-ul dintre `--->` și `<---`, inclusiv acoladele.
## Pasul 2 — creezi remote-ul pe container
```bash
rclone config create gdrive drive \
scope=drive.readonly \
token='<TOKEN_JSON>' \
--non-interactive
```
Ghilimelele simple sunt obligatorii — JSON-ul conține ghilimele duble.
Pentru Maria există un script care face pasul ăsta plus verificarea, scrierea în env și
prima sincronizare: `maria-whatsapp-bridge/ops/setup-drive.sh '<TOKEN_JSON>'`.
## Pasul 3 — fixezi dosarul după ID, nu după nume
```bash
rclone lsf 'gdrive,root_folder_id=<FOLDER_ID>:'
```
`<FOLDER_ID>` e din URL-ul dosarului în Drive
(`https://drive.google.com/drive/folders/<FOLDER_ID>`).
Sintaxa `remote,parametru=valoare:` e „connection string"-ul rclone. **Folosește-o în loc
de căi pe nume** (`gdrive:romfast/document_store`): calea pe nume se rupe dacă dosarul e
mutat sau redenumit în Drive, iar `rclone sync` nu dă eroare — pur și simplu nu găsește
nimic și, dacă e sincronizare bidirecțională, poate goli destinația.
## Unde stau secretele
Tokenul ajunge în `~/.config/rclone/rclone.conf` pe container. Fișierul conține
`refresh_token`, adică acces continuu la Drive-ul contului — **nu ajunge niciodată în
git** și nu se copiază între containere.
`access_token` expiră într-o oră; `refresh_token` îl reînnoiește singur, la nesfârșit,
cât timp aplicația rămâne autorizată în contul Google. Se poate revoca oricând din
<https://myaccount.google.com/permissions>.
Dacă tokenul e revocat sau expiră, sincronizarea începe să eșueze — se reiau pașii 1-2.
## Capcane
- **`rclone sync` e distructiv pe destinație.** Șterge local ce nu mai există în sursă.
Depozitul devine o oglindă a dosarului din Drive, nu o colecție care crește: un fișier
pus manual acolo dispare la prima sincronizare. Dacă vrei doar adăugare, e `rclone copy`.
- **Filtrează ce aduci.** `--include '*.md' --include '*.xml'` etc. Un dosar de Drive
strânge în timp tot felul de lucruri (`.json`, `.ps1`, `.docx`) care n-au ce căuta în
depozit.
- **`rclone authorize` nu merge pe container**, nici prin Tailscale: pornește un server
local pe `127.0.0.1:53682` la care redirectează Google, deci browserul trebuie să fie
pe aceeași mașină.
- **Versiunea de pe container poate fi mai veche** decât cea de pe Windows (aici: 1.60.1
vs 1.75.0). Nu contează pentru token — formatul e stabil de mult.
## Vezi și
- `proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md`, secțiunea
„Sincronizare cu Google Drive" — configurarea concretă pentru Maria.
- `proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/setup-drive.sh` — scriptul
care face pașii 2-3 plus prima sincronizare.