5.15 (propagare design + dashboard editare) si 5.14 (mapare LLM distilata) inchise dupa /code-review high. 8 buguri reparate TDD: - HIGH modal nu se deschidea pe randul slim (base.html: trimitere-slim) - HIGH /repune trunchia prestatii (declaratie incompleta la RAR) -> iterare peste existing, codes pozitional - HIGH embeddings incarca model ~230MB degeaba pe corpus gol -> poarta has_corpus() - HIGH picker chips gol pe re-render eroare -> conn/account_id pe toate ramurile - MED obs re-derivat dupa stergere explicita -> _merge_override pastreaza obs='' - MED mapare salvata fara denumire poluă GOLD -> _record_gold_validation guard - MED typo nome_prestatie -> nume_prestatie in select /repune - MED bucketare timp +3h gresita iarna -> SQLite localtime + TZ=Europe/Bucharest Embeddings WIRE-uit functional (PRD #15, decizie user): ensure_embeddings_corpus construieste corpus din nomenclator, gated pe AUTOPASS_EMBEDDINGS_ENABLED (default off). Marime model corectata ~50MB->~230MB (estimare PRD gresita). Cleanup: hoist load_* din bucla bulk-fix; import re la top. Regresie: 1256 passed, 1 deselected (live), 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85 lines
8.8 KiB
Markdown
85 lines
8.8 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Ce este
|
|
|
|
Gateway web (Python / FastAPI) care preia prezentari de service-auto si le declara la **RAR AUTOPASS** (Legea 142/2023, OM 210/2024). Inlocuieste integrarea Visual FoxPro `RarAutoPass` (ROAAUTO), arhivata in `legacy-vfp/` (doar referinta).
|
|
|
|
Limba proiectului este **romana**: cod, comentarii, commit-uri, documentatie. Fara emoji in raspunsuri sau fisiere noi (preferinta proiect).
|
|
|
|
## Surse de adevar (citeste-le inainte sa modifici comportament)
|
|
|
|
- `docs/api-rar-contract.md` — contractul RAR. **Unde un plan/cod difera de contract, contractul are dreptate.**
|
|
- `docs/ROADMAP.md` — singura sursa de progres + procesul de lucru. O sesiune noua porneste de aici. Doar sectiunea "Stadiu Implementare" se modifica pe parcurs; detaliile stau in PRD-uri (`docs/prd/`).
|
|
|
|
## Comenzi
|
|
|
|
```bash
|
|
pip3 install -r requirements.txt # Python 3.12+
|
|
|
|
# Rulare locala (dev): API + worker sunt PROCESE SEPARATE
|
|
uvicorn app.main:app --reload --port 8010 # API: dashboard /, Swagger /docs, /healthz, /metrics
|
|
python3 -m app.worker # worker (necesar doar pentru a procesa coada)
|
|
|
|
# Wrapper-ul start.sh ambaleaza mediu (test/prod) + rol (api/worker/both/finalizate)
|
|
./start.sh test both --send # API + worker, trimite efectiv la RAR test (loguri in .run/)
|
|
./start.sh test finalizate # listeaza prezentarile inregistrate la RAR (verificare independenta)
|
|
./start.sh status # stare procese + /healthz
|
|
./start.sh stop # opreste procesele pornite cu "both"
|
|
./start-test.sh / ./start-prod.sh # fixeaza mediul, forwardeaza rolul
|
|
|
|
# Teste (pytest; folosesc FastAPI TestClient + SQLite temporar). Testele live RAR sunt
|
|
# skip implicit (marker `live`) — `pytest -q` nu atinge endpointul real.
|
|
python3 -m pytest -q
|
|
python3 -m pytest tests/test_worker_reconcile.py -q # un fisier
|
|
python3 -m pytest tests/test_worker_reconcile.py::test_x -q # un singur test
|
|
python3 -m pytest -q -m "not live" # exclude explicit testele live
|
|
|
|
# Test LIVE pe RAR test (opt-in, skip implicit; atinge endpointul real -> creeaza FINALIZATA):
|
|
# reproduce lantul mapare inline -> queued -> worker -> sent -> verificare in finalizate.
|
|
AUTOPASS_LIVE_RAR=1 python3 -m pytest tests/test_live_rar.py -q # necesita settings.xml cu creds <test>
|
|
|
|
# Lifecycle chei API (admin, doar din CLI — nu exista suprafata HTTP)
|
|
python3 -m tools.apikey create --account 2 # cheie afisata O SINGURA DATA (rfak_...)
|
|
python3 -m tools.apikey list|rotate|revoke
|
|
|
|
# Docker (deploy): api + worker + autoheal, acelasi image + volum SQLite
|
|
docker compose up --build
|
|
```
|
|
|
|
## Arhitectura
|
|
|
|
**Doua procese peste acelasi SQLite (WAL) persistent**, care comunica EXCLUSIV prin tabela `submissions`:
|
|
|
|
- **API** (`app.main:app`) — API v1 (`app/api/v1/router.py` + `import_router.py`), dashboard web HTMX (`app/web/routes.py` + `templates/`), `/healthz`, `/metrics`. Worker-ul **nu** ruleaza ca task aici: un worker mort nu trebuie sa lase containerul "sanatos".
|
|
- **Worker** (`app/worker/__main__.py`) — bucla: heartbeat → recupereaza orfane → claim atomic (`BEGIN IMMEDIATE`) → login RAR (per cont) → `postPrezentare` → update. Retry/backoff exponential, reconciliere anti-duplicat, lease pe randuri `sending` orfane, re-login la JWT expirat (TTL 30h). Send DEZACTIVAT implicit (`AUTOPASS_WORKER_SEND_ENABLED=false`) — sigur pentru probe.
|
|
|
|
**Doua canale de intrare** convergent in coada `submissions`:
|
|
- Canal API (Treapta 1): `POST /v1/prezentari` pentru ROAAUTO / soft propriu.
|
|
- Import web (Treapta 2): upload xlsx/csv → mapare coloane → preview → commit (`app/import_parse.py`, `import_router.py`, dashboard).
|
|
|
|
Flux: validare (`validation.py`) → mapare operatie→cod (`mapping.py`) → enqueue cu PII criptat → worker trimite → dashboard monitorizeaza live.
|
|
|
|
### Invariante critice (usor de stricat)
|
|
|
|
- **`AUTOPASS_CREDS_KEY` trebuie sa fie ACEEASI intre API si worker.** API cripteaza creds RAR (Fernet), worker le decripteaza. Chei diferite → worker nu poate decripta → trimiterile esueaza. `start.sh both` genereaza o cheie efemera partajata; pentru prod pune una persistenta in `.env`. (`crypto.py`)
|
|
- **Idempotenta = hash de continut canonic** server-side (`idempotency.py`), pentru ca RAR accepta duplicate si nu are nr. comanda. `build_key` normalizeaza INTOTDEAUNA `account_id` la `account_or_default` (None == 1) INAINTE de hash — altfel acelasi rand logic primeste chei diferite pe canalele API vs import (OV-2). `canonicalize_row` normeaza VIN/nr/odometru (strip ".0" din coercion Excel) inainte de validare si de cheie.
|
|
- **`FINALIZATA` e terminal la RAR** — fara anulare/corectie prin API. De aceea reconcilierea anti-duplicat: pe eroare **ambigua** (timeout / TransportError / 502/503/504 / 429 / 408) sau rand `sending` orfan, worker-ul cauta in finalizate (match pe vin+dataPrestatie+odometruFinal) si marcheaza `sent` fara a re-trimite (`reconcile.py`). **EXCEPTIE: un RAR 500 cu mesaj** (`RarError.rar_message`, ex. `ORA-12899`) e un esec DEFINITIV (RAR a raspuns „am esuat", nu o pierdere de raspuns) → worker-ul NU reconciliaza si NU reincearca, marcheaza `error` cu mesajul RAR (`RAR_EROARE_SERVER`). Altfel ar marca fals `sent` pe un record PARTIAL pe care RAR (ne-tranzactional) il lasa la esec.
|
|
- **Creds RAR per cont**: durabile in `accounts.rar_creds_enc` (canal web, fallback re-login) SAU efemere in `submissions.rar_creds_enc` (canal API, sterse dupa primul login reusit). Worker incearca submission-ul intai, apoi fallback la cont. Purjarea sterge DOAR `submissions.rar_creds_enc`, NU `accounts.rar_creds_enc`.
|
|
- **Auth API-key** (`auth.py`): identifica CONTUL ROAAUTO, separat de credentialele RAR. Stocam doar SHA-256 al cheii. Enforcement prin `AUTOPASS_REQUIRE_API_KEY`: `false` (dev) → fara cheie merge pe cont id=1, cheie invalida → 401; `true` (prod) → cheie obligatorie pe `/v1/*` protejat. POST-urile + rutele de import sunt account-scoped; GET-urile de listare sunt si ele account-scoped (5.15/US-011: fragmentele `_fragments/submissions|trimitere|mapari|status|jurnal|nomenclator|trimiteri-versiune` sub `require_login` + scope, 404-before-leak pe id strain; `GET /v1/prezentari(/{id})`/`/v1/mapari`/`/v1/audit/export` filtrate pe cont). `GET /v1/nomenclator` ramane public intentionat (coduri RAR publice, fara PII).
|
|
- **Mapare coloane retinuta per `(account_id, signature_coloane)`** (`column_mappings`): la urmatorul fisier cu aceleasi coloane, pentru acelasi cont, maparea se reaplica automat. Un cont poate avea mai multe formate memorate simultan.
|
|
- **Mapare operatie→cod**: prestatie poate veni cu `cod_prestatie` (cod RAR direct) sau `cod_op_service` (cod intern) + `denumire`. Nerezolvat → submission `needs_mapping` (nu se trimite), apare in editorul web cu sugestie fuzzy; la salvarea maparii se re-rezolva automat submission-urile blocate.
|
|
- **`cod_prestatie` e VALIDAT fata de nomenclator la ingestie** (`resolve_prestatii(..., valid_codes)`): un cod direct NECUNOSCUT in nomenclator NU se mai trimite raw — e promovat la `cod_op_service` (denumire=cod) si tratat ca operatie de mapat. Motiv (confirmat live 2026-06-23): RAR accepta NUMAI coduri din nomenclator (coloana `COD_PRESTATIE` max 5 car.); un cod necunoscut da **HTTP 500** (`ORA-12899`), iar RAR **NU e tranzactional** → lasa un record PARTIAL `FINALIZATA` (terminal) chiar pe esec, pe care reconcilierea worker-ului l-ar marca fals `sent`. Comportamentul la cod necunoscut/nemapat: `on_unmapped_error` (camp boolean top-level pe `POST /v1/prezentari` + `/valideaza`) = `false` (intra in editor, `needs_mapping`) sau `true` (respinge fara enqueue → `submission_id=null` + `erori`). Default = `accounts.on_unmapped_error_default` (implicit `false`/`0`); precedenta cerere > cont > `false`.
|
|
- **WAF RAR da 403 fara User-Agent de browser** — toate apelurile httpx trimit `User-Agent: Mozilla/5.0` (`config.py`, confirmat live).
|
|
- **422 fara echo de credentiale**: handler-ul global de validare in `main.py` pastreaza type/loc/msg dar DROP-a `input`/`ctx` (altfel ar reflecta `rar_credentials.password`).
|
|
- **Retentie**: `submissions` sent + `import_batches` primesc `purge_after = now + 90 zile`; worker-ul purjeaza odata pe ora (T16, GDPR/L.142).
|
|
|
|
### Masina de stari submissions
|
|
|
|
`queued → sending → sent` (succes, cu `id_prezentare` de la RAR). Ramuri: `needs_mapping` (cod nerezolvat), `needs_data` (RAR 400, validare continut), `error` (max retries / 4xx nerecuperabil / **RAR 500 cu mesaj — esec definitiv** / creds invalide / login 401 — NU se face retry pe creds gresite). Backoff: `next_attempt_at = now + base*2^retry`, plafonat. Schema completa: `app/schema.sql`.
|
|
|
|
## Mod non-interactiv
|
|
|
|
Vezi `/workspace/CLAUDE.md` (workspace-level): cand esti lansat cu `claude -p`, creeaza fisiere noi DOAR in `/workspace/.claude-work/<task>/`, nu in `/workspace`. Modificarile la fisiere existente se fac in locatia originala.
|