refactor: comentarii strict functionale, fara referinte PRD/stories

Curatare globala a comentariilor si docstring-urilor (app, tools, teste,
scripturi): eliminate referintele la PRD-uri, US-xxx, task-uri istorice si
review-uri; pastrata doar informatia functionala, formulata scurt. Regula
adaugata in CLAUDE.md (sectiunea Stil). Fara modificari de cod sau comportament.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-07-06 13:48:38 +00:00
parent 86408887e5
commit 44f261e269
226 changed files with 1313 additions and 1760 deletions

112
CLAUDE.md
View File

@@ -1,104 +1,86 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Ghid Claude Code pentru acest repo.
## 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).
Gateway web (Python/FastAPI) care preia prezentari service-auto si le declara la **RAR AUTOPASS** (L.142/2023, OM 210/2024). Inlocuieste integrarea VFP `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).
## Stil
## Surse de adevar (citeste-le inainte sa modifici comportament)
- Limba proiectului: **romana** (cod, comentarii, commit-uri, docs). Fara emoji.
- **Comentarii scurte, strict functionale.** Doar ce nu reiese din cod. FARA referinte la PRD-uri, stories (US-xxx), review-uri sau istoricul deciziei.
- `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/`).
## Surse de adevar
- `docs/api-rar-contract.md` — contractul RAR; la divergenta, contractul are dreptate.
- `docs/ROADMAP.md` — progres + proces de lucru; sesiunile noi pornesc de aici. Se modifica doar "Stadiu Implementare"; detaliile stau in `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)
# Dev: API + worker = procese SEPARATE
uvicorn app.main:app --reload --port 8010 # dashboard /, /docs, /healthz, /metrics
python3 -m app.worker # proceseaza 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.sh = mediu (test/prod) + rol (api/worker/both/finalizate); loguri in .run/
./start.sh test both --send # trimite efectiv la RAR test
./start.sh test finalizate # listeaza prezentarile inregistrate la RAR
./start.sh status | stop
./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.
# Teste (TestClient + SQLite temporar); testele live RAR sunt skip implicit (marker `live`)
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
python3 -m pytest tests/test_worker_reconcile.py::test_x -q
AUTOPASS_LIVE_RAR=1 python3 -m pytest tests/test_live_rar.py -q # LIVE opt-in: creeaza FINALIZATA; cere settings.xml cu creds <test>
# 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>
# Chei API (admin, doar CLI); cheia rfak_... se afiseaza O SINGURA DATA
python3 -m tools.apikey create --account 2 | list | rotate | revoke
# 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
docker compose up --build # deploy: api + worker + autoheal, acelasi image + volum SQLite
```
## Arhitectura
**Doua procese peste acelasi SQLite (WAL) persistent**, care comunica EXCLUSIV prin tabela `submissions`:
**Doua procese peste acelasi SQLite (WAL)**, 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.
- **API** (`app.main:app`) — API v1 (`app/api/v1/`), dashboard HTMX (`app/web/` + `templates/`), `/healthz`, `/metrics`. Worker-ul NU ruleaza aici: un worker mort nu trebuie sa lase containerul "sanatos".
- **Worker** (`app/worker/__main__.py`) — bucla: heartbeat → recuperare orfane → claim atomic (`BEGIN IMMEDIATE`) → login RAR per cont → `postPrezentare` → update. Retry/backoff exponential, reconciliere anti-duplicat, lease pe `sending` orfane, re-login la JWT expirat (TTL 30h). Send OFF implicit (`AUTOPASS_WORKER_SEND_ENABLED=false`).
**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).
**Doua canale de intrare** in coada: `POST /v1/prezentari` (ROAAUTO/soft propriu) si import web xlsx/csv → mapare coloane → preview → commit (`app/import_parse.py`, `import_router.py`).
Flux: validare (`validation.py`) → mapare operatie→cod (`mapping.py`) → enqueue cu PII criptat → worker trimite → dashboard monitorizeaza live.
Flux: validare (`validation.py`) → mapare operatie→cod (`mapping.py`) → enqueue cu PII criptat → worker trimite → dashboard live.
### Invariante critice (usor de stricat)
### Invariante critice
- **`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).
- **Admin bootstrap fara hardcodare**: la `/signup`, primul user din TOATA baza (`count_admins()==0`, verificat in tranzactie) primeste `is_admin=1` automat; orice signup ulterior e neadmin. `is_admin` da acces la `/admin` (panou global, toate conturile) — verificat pe `account_id`-ul sesiunii, dar neconditionat DE care `account_id` a primit userul (semnul de "cine e admin" nu are legatura cu contul `id=1`). Contul `id=1` e doar bucket-ul de fallback pentru trafic API neautentificat in dev (`AUTOPASS_REQUIRE_API_KEY=false`) — separat de conceptul de admin. Fix manual daca primul signup s-a consumat deja gresit: `python3 -m tools.account set-admin --account N`.
- **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).
- **`AUTOPASS_CREDS_KEY` identica intre API si worker** (Fernet, `crypto.py`): chei diferite → worker nu decripteaza creds → trimiterile esueaza. `start.sh both` genereaza cheie efemera partajata; in prod, cheie persistenta in `.env`.
- **Idempotenta = hash de continut canonic** server-side (`idempotency.py`) RAR accepta duplicate, fara nr. comanda. `build_key` normalizeaza `account_id` la `account_or_default` (None == 1) INAINTE de hash, altfel acelasi rand primeste chei diferite pe API vs import. `canonicalize_row` normeaza VIN/nr/odometru (strip ".0" din Excel) inainte de validare si cheie.
- **`FINALIZATA` e terminal la RAR** (fara anulare/corectie). Pe eroare **ambigua** (timeout/TransportError/502/503/504/429/408) sau `sending` orfan, worker-ul cauta in finalizate (vin+dataPrestatie+odometruFinal) si marcheaza `sent` fara re-trimitere (`reconcile.py`). **EXCEPTIE: RAR 500 cu mesaj** (`RarError.rar_message`, ex. `ORA-12899`) = esec DEFINITIV → fara reconciliere/retry, `error` cu mesajul RAR (`RAR_EROARE_SERVER`); altfel ar marca fals `sent` un record PARTIAL lasat de RAR (ne-tranzactional).
- **Creds RAR per cont**: durabile in `accounts` (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 contul. Purjarea sterge DOAR creds efemere din `submissions`.
- **Auth API-key** (`auth.py`): identifica contul ROAAUTO (separat de creds RAR); stocam doar SHA-256. `AUTOPASS_REQUIRE_API_KEY`: `false` (dev) → fara cheie = cont id=1, cheie invalida → 401; `true` (prod) → obligatorie pe `/v1/*` protejat. POST-urile, importul si GET-urile de listare (API + fragmente dashboard) sunt account-scoped, 404-before-leak pe id strain. `GET /v1/nomenclator` public intentionat (coduri RAR publice, fara PII).
- **Admin bootstrap**: primul user din TOATA baza la `/signup` (`count_admins()==0`, in tranzactie) devine `is_admin=1`; restul nu. `is_admin` da acces la `/admin` (panou global) si e independent de contul id=1 (acela e doar fallback dev pentru API neautentificat). Fix manual: `python3 -m tools.account set-admin --account N`.
- **Mapare coloane memorata per `(account_id, signature_coloane)`** (`column_mappings`): reaplicata automat la fisiere cu aceleasi coloane; un cont poate avea mai multe formate.
- **Mapare operatie→cod**: prestatia vine cu `cod_prestatie` (cod RAR) sau `cod_op_service` + `denumire`. Nerezolvat → `needs_mapping` (nu se trimite), editor web cu sugestie fuzzy; salvarea maparii re-rezolva automat submission-urile blocate.
- **`cod_prestatie` VALIDAT fata de nomenclator la ingestie** (`resolve_prestatii(..., valid_codes)`): cod necunoscut NU se trimite raw — e promovat la `cod_op_service` (denumire=cod) si intra la mapat. Motiv (confirmat live): RAR accepta doar coduri din nomenclator (max 5 car.); cod necunoscut HTTP 500 `ORA-12899`, iar RAR ne-tranzactional lasa record PARTIAL `FINALIZATA` pe care reconcilierea l-ar marca fals `sent`. La cod nemapat: `on_unmapped_error` (boolean top-level pe `POST /v1/prezentari` + `/valideaza`) = `false` editor/`needs_mapping`; `true` respins fara enqueue (`submission_id=null` + `erori`). Precedenta: cerere > `accounts.on_unmapped_error_default` > `false`.
- **WAF RAR da 403 fara User-Agent de browser** — httpx trimite mereu `User-Agent: Mozilla/5.0` (`config.py`).
- **422 fara echo de credentiale**: handler-ul din `main.py` pastreaza type/loc/msg dar DROP-a `input`/`ctx` (altfel reflecta `rar_credentials.password`).
- **Retentie**: `submissions` sent + `import_batches` `purge_after = now + 90 zile`; worker purjeaza orar (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`.
`queued → sending → sent` (cu `id_prezentare` de la RAR). Ramuri: `needs_mapping`, `needs_data` (RAR 400), `error` (max retries / 4xx nerecuperabil / RAR 500 cu mesaj / creds invalide sau login 401 — fara retry pe creds gresite). Backoff: `next_attempt_at = now + base*2^retry`, plafonat. Schema: `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.
Vezi `/workspace/CLAUDE.md`: cu `claude -p`, fisierele noi merg DOAR in `/workspace/.claude-work/<task>/`; modificarile la fisiere existente raman in locatia originala.
## Skill routing
When the user's request matches an available skill, invoke it via the Skill tool. When in doubt, invoke the skill.
Cand cererea se potriveste unui skill, invoca-l prin Skill tool; la indoiala, invoca-l.
Key routing rules:
- Product ideas/brainstorming → invoke /office-hours
- Strategy/scope → invoke /plan-ceo-review
- Architecture → invoke /plan-eng-review
- Design system/plan review → invoke /design-consultation or /plan-design-review
- Full review pipeline → invoke /autoplan
- Bugs/errors → invoke /investigate
- QA/testing site behavior → invoke /qa or /qa-only
- Code review/diff check → invoke /review
- Visual polish → invoke /design-review
- Ship/deploy/PR → invoke /ship or /land-and-deploy
- Save progress → invoke /context-save
- Resume context → invoke /context-restore
- Author a backlog-ready spec/issue → invoke /spec
- Idei produs → /office-hours · Strategie → /plan-ceo-review · Arhitectura → /plan-eng-review
- Design system/plan → /design-consultation, /plan-design-review · Pipeline complet → /autoplan
- Bug-uri → /investigate · QA site → /qa, /qa-only · Code review → /review · Polish vizual → /design-review
- Ship/deploy/PR → /ship, /land-and-deploy · Salvare/reluare context → /context-save, /context-restore · Spec → /spec