diff --git a/CLAUDE.md b/CLAUDE.md index 5dc8e1b..f42da03 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 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 +# 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//`, 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//`; 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 diff --git a/app/accounts.py b/app/accounts.py index 23ec5b8..766ac4e 100644 --- a/app/accounts.py +++ b/app/accounts.py @@ -67,7 +67,7 @@ def create_account( # Planul cerut: pastram doar valori valide; orice altceva -> NULL (defensiv). req_plan = requested_plan if requested_plan in VALID_TIERS else None try: - # Trial Pro automat la creare (PRD 5.17 US-001): tier='free' + trial_until=now+30z. + # Trial Pro automat la creare: tier='free' + trial_until=now+30z. trial_until = ( (datetime.now(timezone.utc) + timedelta(days=30)).strftime("%Y-%m-%d %H:%M:%S") ) @@ -151,7 +151,7 @@ def set_status(conn: sqlite3.Connection, account_id: int, status: str) -> None: def get_auto_send(conn: sqlite3.Connection, account_id: int) -> bool: - """Comutatorul Auto per cont (PRD 5.19 US-001): `accounts.auto_send_enabled` ca bool. + """Comutatorul Auto per cont: `accounts.auto_send_enabled` ca bool. Cont inexistent -> False (Auto OFF: sigur, tine randurile). Nu comita (ca set_active). """ @@ -164,7 +164,7 @@ def get_auto_send(conn: sqlite3.Connection, account_id: int) -> bool: def set_auto_send(conn: sqlite3.Connection, account_id: int, enabled: bool) -> None: - """Comuta `accounts.auto_send_enabled` (PRD 5.19 US-001). Idempotent (set pe aceeasi + """Comuta `accounts.auto_send_enabled`. Idempotent (set pe aceeasi valoare nu arunca). Scoped pe cont. Nu comita — respecta conventia fisierului (apelantul comita, ca la set_active/set_status). Cont inexistent -> UPDATE fara efect (no-op sigur). """ @@ -175,8 +175,8 @@ def set_auto_send(conn: sqlite3.Connection, account_id: int, enabled: bool) -> N def held_for_account(conn: sqlite3.Connection, account_id: int) -> int: - """Snapshot-ul `held` la ingestie (PRD 5.19 US-001). SURSA UNICA (chokepoint) folosita de - TOATE situ-rile de ingestie care scriu `status='queued'` (US-002). + """Snapshot-ul `held` la ingestie. SURSA UNICA (chokepoint) folosita de + TOATE situ-rile de ingestie care scriu `status='queued'`. Intoarce 0 (nu tine) daca `accounts.auto_send_enabled=1`, altfel 1 (tine randul). Cont inexistent -> 1 (sigur: tine randul, nu-l trimite fara comutator explicit). @@ -215,7 +215,7 @@ def set_tier( "UPDATE accounts SET tier=?, trial_until=? WHERE id=?", (tier, trial_until, account_id), ) - # Audit in app_events (decizie PRD 5.17 US-008, fara PII nou) + # Audit in app_events (fara PII nou) try: from .observ import log_event log_event( diff --git a/app/api/v1/import_router.py b/app/api/v1/import_router.py index 5935660..b1f1bd6 100644 --- a/app/api/v1/import_router.py +++ b/app/api/v1/import_router.py @@ -1,4 +1,4 @@ -"""Router import Treapta 2 — upload fisier xlsx/csv, preview, commit, export. +"""Router import — upload fisier xlsx/csv, preview, commit, export. Endpointuri: POST /v1/import — upload fisier, staging in import_batches/import_rows @@ -190,7 +190,7 @@ def _resolve_row_for_preview( denumire = str(denumire_val).strip() if denumire_val not in (None, "") else str(operatie_val) mapped["prestatii"] = [{"cod_op_service": str(operatie_val), "denumire": denumire}] - # obs derive-on-empty (D7/E3 PRD 5.15): daca obs e gol si avem operatie, + # obs derive-on-empty: daca obs e gol si avem operatie, # copiem denumirea operatiei in obs (nu o mutam — op_service ramane neatins). # DERIVE-ON-EMPTY: doar cand obs e gol, ca sa fie idempotent la re-preview/re-editare. obs_curent = str(mapped.get("obs") or "").strip() @@ -236,7 +236,7 @@ def _resolve_row_for_preview( if all_flags and not reviewed: # needs_review: validarea a trecut, dar flagurile (date ambigue, formule) cer confirmare manuala. # Daca reviewed=True (operatorul a confirmat explicit valorile in modal), sarim - # acest return si continuam spre ok/needs_data (US-007, PRD 5.12). + # acest return si continuam spre ok/needs_data. return { "resolved_status": "needs_review", "resolved": mapped, @@ -244,8 +244,6 @@ def _resolve_row_for_preview( "flags": all_flags, } - # US-001 (PRD 5.11): ramura auto_send eliminata din preview. - if errors: return { "resolved_status": "needs_data", @@ -361,7 +359,7 @@ def apply_row_override( new_override = _merge_override(current, fields) enc = encrypt_creds(new_override) if new_override else None - # D#9 (PRD 5.12): resetam reviewed=0 la orice schimbare de valoare — operatorul + # Resetam reviewed=0 la orice schimbare de valoare — operatorul # trebuie sa reconfirme dupa editare. NU conditionam pe reviewed curent: orice override # (chiar si revert la valoarea initiala) anuleaza confirmarea implicita. conn.execute("UPDATE import_rows SET override_json=?, reviewed=0 WHERE id=?", (enc, row["rid"])) @@ -965,7 +963,7 @@ def commit_import( if batch["status"] == "committed": raise HTTPException(status_code=409, detail="batch deja comis") - # D#8 (PRD 5.12): gate commit derivat din DB `reviewed` pe AMBELE canale. + # Gate commit derivat din DB `reviewed` pe AMBELE canale. # API: reviewed_rows pastrat (contract stabil) dar seteaza reviewed=1 in DB inainte # de interogare. Randurile needs_review cu reviewed=1 sunt incluse in comit. if req.reviewed_rows: @@ -1032,7 +1030,7 @@ def commit_import( if n_total_ok == 0: raise HTTPException(status_code=422, detail="Niciun rand ok de confirmat.") - # Rezolva mediul RAR tinta al lotului (US-009): cerut > default cont > ancora globala. + # Rezolva mediul RAR tinta al lotului: cerut > default cont > ancora globala. try: env = rezolva_rar_env(conn, account_id, req.rar_env) except ValueError as e: @@ -1044,8 +1042,8 @@ def commit_import( "disponibile": e.disponibile, }) - # T3 (PRD 5.17): enforce volum plan — INAINTE de enqueue (invariant idempotenta). - # Decizie #21: respingere TOTALA a lotului (nu enqueue partial tacut). + # Enforce volum plan — INAINTE de enqueue (invariant idempotenta). + # Respingere TOTALA a lotului (nu enqueue partial tacut). from ...config import get_settings as _get_settings from ...plans import PLANS, effective_tier, monthly_usage from ...observ import log_event as _log_event_plan @@ -1126,7 +1124,7 @@ def commit_import( conn.execute("BEGIN IMMEDIATE") try: purge_after_sql = "datetime('now', '+90 days')" - # PRD 5.19 US-002: snapshot `held` din comutatorul contului (chokepoint unic + # Snapshot `held` din comutatorul contului (chokepoint unic # held_for_account) — 0 daca Auto ON, 1 daca Auto OFF. Per-cont -> o data. held_val = held_for_account(conn, acct) @@ -1162,7 +1160,7 @@ def commit_import( denumire = str(denumire_val).strip() if denumire_val not in (None, "") else str(operatie_val) mapped["prestatii"] = [{"cod_op_service": str(operatie_val), "denumire": denumire}] - # obs derive-on-empty (D7/E3 PRD 5.15): copiere denumire in obs daca obs e gol. + # obs derive-on-empty: copiere denumire in obs daca obs e gol. # Identic cu logica din _resolve_row_for_preview (override aplicat tot ultimul). obs_curent = str(mapped.get("obs") or "").strip() if not obs_curent and operatie_val: @@ -1272,7 +1270,7 @@ def commit_import( class RandEditIn(BaseModel): """Campuri de continut editabile in preview. None = ne-trimis (neschimbat); "" = sterge override-ul (revine la valoarea din fisier). - obs = text liber fara validare de continut (US-005 PRD 5.15).""" + obs = text liber fara validare de continut.""" vin: str | None = None nr_inmatriculare: str | None = None data_prestatie: str | None = None diff --git a/app/api/v1/integrare_router.py b/app/api/v1/integrare_router.py index fad9ac2..1629cbf 100644 --- a/app/api/v1/integrare_router.py +++ b/app/api/v1/integrare_router.py @@ -57,7 +57,7 @@ def ping( conn.close() autentificat_cu_cheie = acct is not None - # Verificam daca contul are creds RAR stocate (per-env, US-013). + # Verificam daca contul are creds RAR stocate (per mediu). aid = account_or_default(account_id) conn = get_connection() try: diff --git a/app/api/v1/router.py b/app/api/v1/router.py index 6e1ea56..f23ddd7 100644 --- a/app/api/v1/router.py +++ b/app/api/v1/router.py @@ -88,7 +88,7 @@ def _erori_nemapate(unmapped: list[dict]) -> list[dict]: ] -# PRD 5.19 US-010: mesaj uman pentru un rand tinut manual (queued AND held=1). +# Mesaj uman pentru un rand tinut manual (queued AND held=1). # Reutilizeaza vocabularul existent AUTO_SEND_OPRIT (errors.py) — nu inventam al treilea # vocabular "auto_send". Onestitate: raspunsul NU trebuie sa arate un "queued" curat fals. MOTIV_HELD = ( @@ -107,8 +107,7 @@ def _motiv_clasificare(cl: dict) -> str | None: None cand status='queued'. Acopera ramurile de blocaj: erori de continut (needs_data) si coduri fara mapare RAR (needs_mapping). - Dupa US-001: needs_mapping apare EXCLUSIV cand unmapped e non-gol - (ramura auto_send_oprit era inaccesibila si a fost eliminata). + needs_mapping apare EXCLUSIV cand unmapped e non-gol. """ if cl["status"] == "queued": return None @@ -127,7 +126,7 @@ def _rezultat_enqueue(submission_id: int | None, cl: dict, held: int = 0, **extr Populeaza erori (validare continut), nemapate (coduri fara mapare) si motiv (uman) pentru orice status != 'queued'. Aditiv: pe 'queued' toate raman goale/None, EXCEPTAND - randurile tinute manual (PRD 5.19 US-010: `held=1` -> held=true + motiv non-null). + randurile tinute manual (`held=1` -> held=true + motiv non-null). """ tinut = bool(held) motiv = _motiv_clasificare(cl) or _motiv_held(cl["status"], tinut) @@ -179,7 +178,7 @@ def create_prezentari( conn = get_connection() results: list[SubmissionResult] = [] try: - # load_mapping_meta incarca maparea op->cod RAR; dupa US-001, auto_send nu mai tine randuri. + # load_mapping_meta incarca maparea op->cod RAR; auto_send nu mai tine randuri. mapping_meta = load_mapping_meta(conn, acct) mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()} # Validare cod_prestatie fata de nomenclator + modul la cod necunoscut/nemapat. @@ -189,12 +188,12 @@ def create_prezentari( text_rules = load_text_rules(conn, acct) error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error) - # US-005: rezolva mediul RAR tinta (cerut > default cont > ancora globala). + # Rezolva mediul RAR tinta (cerut > default cont > ancora globala). # MediuIndisponibil -> 422 inainte de orice enqueue (respinge tot lotul). try: env = rezolva_rar_env(conn, acct, req.rar_env) except MediuIndisponibil as e: - # US-012: audit blocare mediu indisponibil (tip='rar_env_blocat'). + # Audit blocare mediu indisponibil (tip='rar_env_blocat'). log_event( "rar_env_blocat", nivel="WARNING", @@ -220,8 +219,8 @@ def create_prezentari( detail=err_eroare("RAR_MEDIU_INDISPONIBIL", cauza="valoare invalida pentru rar_env"), ) - # T3 (PRD 5.17): enforce volum plan — INAINTE de build_key/enqueue (invariant idempotenta). - # Decizie #21: respingere TOTALA a lotului (nu enqueue partial tacut). + # Enforce volum plan — INAINTE de build_key/enqueue (invariant idempotenta). + # Respingere TOTALA a lotului (nu enqueue partial tacut). from ...config import get_settings as _get_settings from ...plans import PLANS, effective_tier, monthly_usage _settings = _get_settings() @@ -259,8 +258,8 @@ def create_prezentari( ), ) - # PRD 5.19 US-002: snapshot `held` din comutatorul contului (chokepoint unic - # held_for_account). 0 daca Auto ON, 1 daca Auto OFF (tine randul). Depinde de + # Snapshot `held` din comutatorul contului (chokepoint unic held_for_account). + # 0 daca Auto ON, 1 daca Auto OFF (tine randul). Depinde de # cont, nu de rand -> calculat o data. NU intra in payload/idempotenta. held_val = held_for_account(conn, acct) @@ -291,9 +290,8 @@ def create_prezentari( # on_unmapped_error=True: nu reactivam; randul ramane 'error'. results.append(_rezultat_respins(existing["id"], cl, rar_env=env)) continue - # PRD 5.19 US-002 (Eng Finding A — bug de bypass): la reactivare - # re-snapshot-am `held` din comutatorul contului. Fara asta randul - # pastra held VECHI si se auto-trimitea desi contul e Auto OFF. + # La reactivare re-snapshot-am `held` din comutatorul contului. Fara + # asta randul pastra held VECHI si se auto-trimitea desi contul e Auto OFF. cur = conn.execute( "UPDATE submissions SET status=?, payload_json=?, rar_error=?, " "rar_creds_enc=COALESCE(?, rar_creds_enc), retry_count=0, " @@ -304,7 +302,7 @@ def create_prezentari( ) if cur.rowcount == 1: # Creds noi se propaga si in slotul durabil per-env al contului - # — ambele canale converg pe parola corectata (US-013, env-aware). + # — ambele canale converg pe parola corectata (env-aware). if req.rar_credentials is not None: conn.execute( f"UPDATE accounts SET rar_creds_{env}_enc=?, rar_{env}_enabled=1 WHERE id=?", @@ -321,7 +319,7 @@ def create_prezentari( "SELECT id, status, id_prezentare, held FROM submissions WHERE id=?", (existing["id"],), ).fetchone() - # Echo onest pe dedup (PRD 5.19 US-002/US-010): propaga `held` al randului + # Echo onest pe dedup: propaga `held` al randului # existent — un rand tinut NU trebuie sa apara ca un "queued" curat fals. dedup_held = bool(existing["held"]) results.append( @@ -338,13 +336,13 @@ def create_prezentari( continue # Helper pur partajat cu dry-run: reproduce EXACT clasificarea - # (canonicalize + mapare op->cod + validare; auto_send gate eliminat dupa US-001). + # (canonicalize + mapare op->cod + validare; fara gate auto_send). cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules) if cl["blocked_error"]: # on_unmapped_error=True: respinge fara enqueue (cod necunoscut/nemapat). results.append(_rezultat_respins(None, cl, rar_env=env)) continue - # PRD 5.19 US-002: `held` = snapshot comutator cont (chokepoint held_for_account). + # `held` = snapshot comutator cont (chokepoint held_for_account). cur = conn.execute( "INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error, rar_creds_enc, rar_env, held) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?)", @@ -401,11 +399,11 @@ def valideaza_prezentari( text_rules = load_text_rules(conn, acct) error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error) - # US-005 (DX F5): rezolva env identic ca trimiterea reala si ecou-ieste in raspuns. + # Rezolva env identic ca trimiterea reala si ecou-ieste in raspuns. try: env = rezolva_rar_env(conn, acct, req.rar_env) except MediuIndisponibil as e: - # US-012: audit blocare mediu indisponibil pe dry-run (tip='rar_env_blocat'). + # Audit blocare mediu indisponibil pe dry-run (tip='rar_env_blocat'). log_event( "rar_env_blocat", nivel="WARNING", @@ -464,7 +462,7 @@ def list_prezentari( scope_sql, scope_params = account_scope_clause(account_id) # payload_json e plaintext (vezi submissions.payload_json); il citim doar ca # sa derivam campurile afisabile prin helper-ul partajat, nu il expunem. - # rar_env inclus (US-005): badge mediu in lista. + # rar_env inclus: badge mediu in lista. cols = ( "id, status, id_prezentare, rar_status_code, retry_count, " "created_at, updated_at, payload_json, rar_env, held" @@ -502,9 +500,9 @@ _PREZENTARE_FIELDS = frozenset({ # erori din catalog (niciodata creds, ex. RAR_CREDS_INVALIDE poarta doar cauza # "credentiale RAR invalide", fara parola). Face recovery-ul observabil prin API. "rar_error", - # US-005: mediul RAR tinta (Test/Productie) — necesar pentru badge + ecou API. + # Mediul RAR tinta (Test/Productie) — necesar pentru badge + ecou API. "rar_env", - # PRD 5.19 US-010: flag `held` (tinut manual) — onestitate GET (dev vede ca NU a plecat). + # Flag `held` (tinut manual) — onestitate GET (dev vede ca NU a plecat). "held", }) @@ -589,7 +587,7 @@ def trimite_acum_prezentare( submission_id: int, account_id: int = Depends(resolve_account_id), ) -> dict: - """Elibereaza manual un rand tinut (PRD 5.19 US-010, paritate API cu /repune). + """Elibereaza manual un rand tinut (paritate API cu /repune). Scoped strict pe contul cheii API (account_id din sesiune/cheie, NICIODATA din body). 404-before-leak pe id strain/inexistent. `held: 1 -> 0` DOAR daca randul e @@ -639,7 +637,7 @@ AUDIT_COLUMNS = [ "odometru_final", "prestatii", "rar_status_code", - # US-010 (PRD 5.20): mediul RAR tinta per trimitere. + # Mediul RAR tinta per trimitere. "rar_env", "created_at", "updated_at", @@ -694,7 +692,7 @@ def _audit_rows(conn, date_from: str | None, date_to: str | None, status: str, a "odometru_final": p.get("odometru_final") or "", "prestatii": codes, "rar_status_code": r["rar_status_code"] or "", - # US-010 (PRD 5.20): mediul RAR tinta — coloana audit. + # Mediul RAR tinta — coloana audit. "rar_env": r["rar_env"] or "", "created_at": r["created_at"], "updated_at": r["updated_at"], @@ -826,7 +824,7 @@ def set_rar_creds( req: RarCredsIn, account_id: int = Depends(resolve_account_id), ) -> dict: - """Seteaza creds RAR durabile per-cont, in slotul per-mediu (US-013, env-aware). + """Seteaza creds RAR durabile per-cont, in slotul per-mediu (env-aware). Slotul tinta: `req.rar_target` ('test'/'prod') sau ancora globala (AUTOPASS_RAR_ENV). Activeaza mediul selectat (`rar_{env}_enabled=1`). Worker-ul le foloseste ca @@ -855,7 +853,7 @@ def delete_rar_creds( account_id: int = Depends(resolve_account_id), env: str | None = None, ) -> dict: - """Sterge creds RAR durabile per-cont (revenire la modelul efemer Treapta 1). + """Sterge creds RAR durabile per-cont (revenire la modelul efemer per submission). `env` (query param): 'test' | 'prod' -> sterge DOAR slotul acelui mediu + dezactiveaza-l. Absent -> sterge AMBELE sloturi (revenire completa). Schimbare aditiva (back-compat). diff --git a/app/auth.py b/app/auth.py index d8767a8..2010b3a 100644 --- a/app/auth.py +++ b/app/auth.py @@ -168,7 +168,7 @@ def resolve_account_id( def require_api_access( account_id: int = Depends(resolve_account_id), ) -> int: - """Dependency FastAPI (T4, PRD 5.17): verifica ca tier-ul efectiv permite accesul la API. + """Dependency FastAPI: verifica ca tier-ul efectiv permite accesul la API. Reguli: - enforce_plans=False (kill-switch): sare verificarea. diff --git a/app/config.py b/app/config.py index ce5ea70..bd397ea 100644 --- a/app/config.py +++ b/app/config.py @@ -35,10 +35,10 @@ class Settings(BaseSettings): # Retentie randuri blocate (error/needs_data/needs_mapping). Mai scurt decat 90z # ale `sent` — un blocat n-are valoare de audit. blocked_retention_days: int = 30 - # Prag zile (PRD 5.19 US-007): randurile tinute manual (`queued AND held=1`) mai vechi de + # Prag zile: randurile tinute manual (`queued AND held=1`) mai vechi de # atat declanseaza bannerul de avertizare L.142 (declararea are termen legal). held_warn_days: int = 7 - # Retentie randuri tinute manual (PRD 5.19 US-008): un `queued AND held=1` mai vechi de atat + # Retentie randuri tinute manual: un `queued AND held=1` mai vechi de atat # expira la `error` (`TINUT_EXPIRAT`) + purge_after direct. Aliniat retentiei `sent` (90z). held_retention_days: int = 90 @@ -67,7 +67,7 @@ class Settings(BaseSettings): # False (dev): cookie fara Secure, functioneaza pe HTTP. session_https_only: bool = False - # --- Contact suport (US-001, PRD 5.12) --- + # --- Contact suport --- # Email/canal de contact afisat in mesaje catre utilizatori (ex. CUI duplicat la signup). # Nesetat -> fallback la formularea generica fara canal concret. support_email: str | None = None @@ -115,14 +115,13 @@ class Settings(BaseSettings): worker_retry_max_s: int = 300 worker_max_retries: int = 8 # peste atat -> error + banner - # --- Planuri de cont (PRD 5.17) --- + # --- Planuri de cont --- # Enforcement DUR al limitelor de plan (volum + acces API). True (implicit) = activ. # False = kill-switch de operare: sare toate gate-urile de plan (util pentru debugging - # sau rollback rapid fara revert de cod). Enforcement DUR e activ implicit de la deploy - # (decizie user 2026-06-28, decizia #22 autoplan): nu exista conturi legacy, produs in TESTE. + # sau rollback rapid fara revert de cod). Nu exista conturi legacy, produs in teste. enforce_plans: bool = True - # --- Embeddings (sugestie mapare, Stratul 2 PRD 5.14) --- + # --- Embeddings (sugestie mapare) --- # ACTIVAT implicit: editorul de mapari ofera sugestii semantice (model fastembed/ONNX). # Cost: prima folosire lazy-load-eaza modelul (~230MB pe disc) sincron in thread-ul de # cerere -> prima cerere /mapari poate dura 30-120s pana modelul intra in memorie; cererile @@ -131,7 +130,7 @@ class Settings(BaseSettings): # /mapari instant la prima cerere sau suita de teste rapida (cade pe GOLD/SILVER+fuzzy). embeddings_enabled: bool = True - # --- Seed corpus operatii etichetate (SILVER, PRD 5.18 US-004) --- + # --- Seed corpus operatii etichetate (SILVER) --- # ACTIVAT implicit: la init_db, populeaza mapping_suggestions din artefactul comis # `app/data/operatii-etichetate.json` (INSERT OR IGNORE). Asa SILVER nu mai e gol in # productie -> sugestii exact-match + corpus k-NN reale. SUGGESTION-ONLY. diff --git a/app/db.py b/app/db.py index 70032e4..d3fbe0f 100644 --- a/app/db.py +++ b/app/db.py @@ -37,7 +37,7 @@ def init_db() -> None: from .mapping import seed_nomenclator_if_empty seed_nomenclator_if_empty(conn) - # Seed corpus operatii etichetate -> mapping_suggestions (SILVER, PRD 5.18 US-004). + # Seed corpus operatii etichetate -> mapping_suggestions (SILVER). # Gated: OFF in teste (conftest), ON in productie. INSERT OR IGNORE -> idempotent. # DOAR daca mapping_suggestions e gol: seedul are ~17k randuri; re-rularea lui pe # FIECARE boot (API + worker concurent) tinea write-lock-ul indelung -> al doilea @@ -72,9 +72,9 @@ def _migrate(conn: sqlite3.Connection) -> None: if "row_index" not in sub_cols: conn.execute("ALTER TABLE submissions ADD COLUMN row_index INTEGER") if "rar_env" not in sub_cols: - # PRD 5.20 US-001. Mediul RAR tinta pe submission. Pe DB existent NU lasam + # Mediul RAR tinta pe submission. Pe DB existent NU lasam # randurile pe DEFAULT 'test': un rand prod pre-migrare etichetat 'test' ar fi - # reconciliat de worker (US-006) contra endpoint TEST -> no-match -> re-send prod + # reconciliat de worker contra endpoint TEST -> no-match -> re-send prod # = DUPLICAT REAL IREVERSIBIL. Backfill din AUTOPASS_RAR_ENV global (ancora de # migrare) + recompute idempotency_key env-aware. Ruleaza O SINGURA DATA (in # blocul de adaugare a coloanei); pe DB fresh coloana vine din schema.sql (fara rows). @@ -84,7 +84,7 @@ def _migrate(conn: sqlite3.Connection) -> None: ) _backfill_submissions_rar_env(conn) if "held" not in sub_cols: - # PRD 5.19 US-001: flag rand tinut manual. Default 0 (randurile pre-migrare nu erau + # Flag rand tinut manual. Default 0 (randurile pre-migrare nu erau # tinute — pastreaza comportamentul; comutatorul de cont e OFF/held la ingestie NOUA). conn.execute( "ALTER TABLE submissions ADD COLUMN held INTEGER NOT NULL DEFAULT 0 " @@ -93,11 +93,11 @@ def _migrate(conn: sqlite3.Connection) -> None: # Coloane accounts acc_cols = {r["name"] for r in conn.execute("PRAGMA table_info(accounts)").fetchall()} - # AUTO-FIX 6a ELIMINAT (US-013): NU mai adaugam accounts.rar_creds_enc — coloana e dropata. - # _migrate_accounts_medii gestioneaza absenta coloana (guard la ~219: if "rar_creds_enc" not in acc_cols: return). - # Medii RAR per cont (PRD 5.20 US-001): activare + slot creds + default, per mediu. + # NU mai adaugam accounts.rar_creds_enc — coloana e dropata. + # _migrate_accounts_medii gestioneaza absenta coloana (guard: if "rar_creds_enc" not in acc_cols: return). + # Medii RAR per cont: activare + slot creds + default, per mediu. _migrate_accounts_medii(conn, acc_cols) - # US-013: DROP coloana legacy accounts.rar_creds_enc dupa backfill complet. + # DROP coloana legacy accounts.rar_creds_enc dupa backfill complet. _drop_legacy_accounts_rar_creds(conn, acc_cols) if "active" not in acc_cols: # Conturi existente raman active (default 1). @@ -121,7 +121,7 @@ def _migrate(conn: sqlite3.Connection) -> None: "CHECK (on_unmapped_error_default IN (0, 1))" ) if "auto_send_enabled" not in acc_cols: - # PRD 5.19 US-001: comutator Auto per cont. Default 0 (Auto OFF) — conturile existente + # Comutator Auto per cont. Default 0 (Auto OFF) — conturile existente # trec pe „tine randurile" la ingestie noua pana cand operatorul porneste Auto explicit. # Contul id=1 ramane pe default (0), fara tratament special. conn.execute( @@ -129,10 +129,10 @@ def _migrate(conn: sqlite3.Connection) -> None: "CHECK (auto_send_enabled IN (0, 1))" ) if "email" not in acc_cols: - # Email canonic de contact al firmei (US-001, PRD 5.12). Nullable pt. conturi legacy. + # Email canonic de contact al firmei. Nullable pt. conturi legacy. conn.execute("ALTER TABLE accounts ADD COLUMN email TEXT") if "tier" not in acc_cols: - # Plan de cont (US-001, PRD 5.17). Legacy -> 'free' fara trial (enforcement DUR la deploy). + # Plan de cont. Legacy -> 'free' fara trial (enforcement DUR la deploy). conn.execute( "ALTER TABLE accounts ADD COLUMN tier TEXT NOT NULL DEFAULT 'free' " "CHECK (tier IN ('free','standard','pro','premium'))" @@ -173,7 +173,7 @@ def _migrate(conn: sqlite3.Connection) -> None: if "override_json" not in irows_cols: conn.execute("ALTER TABLE import_rows ADD COLUMN override_json TEXT") if "reviewed" not in irows_cols: - # Marcaj confirmare umana (US-007, PRD 5.12). NU intra in payload/idempotenta. + # Marcaj confirmare umana. NU intra in payload/idempotenta. # NOT NULL DEFAULT 0: valoare clara (0=neconfirmat), fara ambiguitate NULL vs 0. conn.execute( "ALTER TABLE import_rows ADD COLUMN reviewed INTEGER NOT NULL DEFAULT 0" @@ -194,7 +194,7 @@ def _migrate(conn: sqlite3.Connection) -> None: "ON submissions(account_id, status)" ) if "idx_submissions_held" not in existing_idx: - # PRD 5.19 US-001 (Eng MEDIUM): CREATE TABLE IF NOT EXISTS nu se declanseaza pe DB + # CREATE TABLE IF NOT EXISTS nu se declanseaza pe DB # existent -> indexul partial trebuie creat si aici, nu doar in schema.sql. conn.execute( "CREATE INDEX IF NOT EXISTS idx_submissions_held ON submissions(held) WHERE held=1" @@ -202,7 +202,7 @@ def _migrate(conn: sqlite3.Connection) -> None: def _migrate_accounts_medii(conn: sqlite3.Connection, acc_cols: set[str]) -> None: - """PRD 5.20 US-001: coloane medii RAR per cont + backfill din ancora globala. + """Coloane medii RAR per cont + backfill din ancora globala. Adauga (idempotent): rar_test_enabled/rar_prod_enabled (bife activare), rar_creds_test_enc/rar_creds_prod_enc (sloturi creds), rar_env_default. @@ -212,7 +212,7 @@ def _migrate_accounts_medii(conn: sqlite3.Connection, acc_cols: set[str]) -> Non momentul migrarii (ancora) — il copiem in slotul acelui mediu, activam DOAR acel mediu (celalalt dezactivat) si fixam default-ul pe el. Conturile fara creds raman pe default-urile coloanei (prod on / test off). Migrarea NU presupune env-ul; se - bazeaza pe ancora globala, exact cum opera contul inainte de 5.20. + bazeaza pe ancora globala. """ newly_added = "rar_env_default" not in acc_cols if "rar_test_enabled" not in acc_cols: @@ -252,12 +252,12 @@ def _migrate_accounts_medii(conn: sqlite3.Connection, acc_cols: set[str]) -> Non def _drop_legacy_accounts_rar_creds(conn: sqlite3.Connection, acc_cols: set[str]) -> None: - """PRD 5.20 US-013: DROP coloana legacy `accounts.rar_creds_enc` dupa backfill complet. + """DROP coloana legacy `accounts.rar_creds_enc` dupa backfill complet. Idempotent si sigur la fiecare boot (garda one-way: coloana absenta = nimic de facut). La eroare LOGHEAZA si lasa coloana pe loc (fail-safe — nu crapa boot-ul ambelor procese). Structura separata pentru testabilitate: `_garda_si_drop(conn)` expune pasul de - assert+backup+DROP izolat (fara re-backfill 6c), apelabil direct din teste. + assert+backup+DROP izolat (fara re-backfill), apelabil direct din teste. """ if "rar_creds_enc" not in acc_cols: return # garda one-way: coloana deja dropata sau DB fresh @@ -272,10 +272,10 @@ def _drop_legacy_accounts_rar_creds(conn: sqlite3.Connection, acc_cols: set[str] def _drop_legacy_rar_creds_impl(conn: sqlite3.Connection) -> None: - """Re-backfill (AUTO-FIX 6c) + delegate la _garda_si_drop. + """Re-backfill + delegate la _garda_si_drop. - Re-backfill-ul 6c acopera creds setate via POST /v1/conturi/rar-creds intre US-001 - si US-013 (pot fi DOAR in coloana veche). Ancora globala: AUTOPASS_RAR_ENV. + Re-backfill-ul acopera creds setate via POST /v1/conturi/rar-creds intre adaugarea + coloanei noi si drop-ul celei vechi (pot fi DOAR in coloana veche). Ancora globala: AUTOPASS_RAR_ENV. """ if sqlite3.sqlite_version_info < (3, 35, 0): print( @@ -344,7 +344,7 @@ def _garda_si_drop(conn: sqlite3.Connection) -> None: def _backfill_submissions_rar_env(conn: sqlite3.Connection) -> None: - """PRD 5.20 US-001 (AUTO-FIX G + E4/3): backfill rar_env + recompute idempotency_key. + """Backfill rar_env + recompute idempotency_key. Ruleaza O SINGURA DATA, imediat dupa ce coloana `submissions.rar_env` a fost adaugata pe un DB existent. Toate randurile pre-migrare au fost trimise (sau urmeaza) catre diff --git a/app/embeddings.py b/app/embeddings.py index 0bb8387..68d8fb7 100644 --- a/app/embeddings.py +++ b/app/embeddings.py @@ -1,13 +1,13 @@ -"""Modul embedding in-proces pentru sugestie cod RAR -- L14-S4. +"""Modul embedding in-proces pentru sugestie cod RAR. -Design (PRD 5.14, Decision #16/#16b): +Design: - Model multilingv via fastembed/ONNX (~230MB pe disc, quantizat, fara torch) - Lazy load la prima folosire, NU la import si NU pe /healthz - Worker NU incarca modelul (API-only) - Degradare gratioasa: daca modelul nu se incarca -> is_available()=False, suggest_nearest() -> [] fara exceptie, ingestia NU e blocata - Embeddings = DOAR sugestie (nu intra in lantul de enqueue/resolve_prestatii) -- NU apelat din resolve_prestatii/load_mapping (wiring vine in L14-S6 DUPA 5.15) +- NU apelat din resolve_prestatii/load_mapping API public (nivel modul): index_corpus(items) -> None @@ -28,7 +28,7 @@ log = logging.getLogger(__name__) # Modelul ales: paraphrase-multilingual-MiniLM-L12-v2 # ~230MB pe disc (ONNX quantizat), 384 dim, multilingv (ro/en/etc.), suportat de -# fastembed, fara torch. (Estimarea initiala din PRD de ~50MB a fost gresita.) +# fastembed, fara torch. FASTEMBED_MODEL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" @@ -137,7 +137,7 @@ class EmbeddingEngine: ) -> list[dict]: """Returneaza top_k vecini cosine [{cod, is_nul, similaritate}]. - `is_nul` (PRD 5.18 US-005): cand corpusul include exemple NUL (non-operatii), + `is_nul`: cand corpusul include exemple NUL (non-operatii), un vecin NUL = semnal de SUPRESIE, nu cod. Default False pe corpusuri vechi fara `is_nul` in itemi. Returneaza [] daca backend-ul lipseste, corpus-ul e gol sau apare orice exceptie (degradare gratioasa -- nu blocheaza ingestia). diff --git a/app/errors.py b/app/errors.py index fe11b18..76ac110 100644 --- a/app/errors.py +++ b/app/errors.py @@ -185,7 +185,7 @@ CATALOG: dict[str, dict[str, str]] = { " cererii (request_id) afisat." ), }, - # Coduri de plan (PRD 5.17) + # Coduri de plan "PLAN_LIMITA_LUNARA": { "problema": "Ai atins limita planului Gratuit (60 prestatii/luna)", "fix": ( diff --git a/app/idempotency.py b/app/idempotency.py index ab1fd1b..f2d704c 100644 --- a/app/idempotency.py +++ b/app/idempotency.py @@ -71,7 +71,7 @@ def canonicalize_row(raw: dict[str, Any]) -> dict[str, Any]: def build_key(account_id: int | None, canon: dict[str, Any], rar_env: str = "test") -> str: - """SHA-256 partajat canal-API + canal-import, env-aware (PRD 5.20 US-003). + """SHA-256 partajat canal-API + canal-import, env-aware. Aplica account_or_default inainte de hash: None si 1 colapseaza la aceeasi cheie => acelasi rand logic din canale diferite nu se trimite de doua ori. diff --git a/app/import_parse.py b/app/import_parse.py index 3351fd4..99cec40 100644 --- a/app/import_parse.py +++ b/app/import_parse.py @@ -1,4 +1,4 @@ -"""Parser fisiere xlsx/csv pentru import prezentari (Treapta 2). +"""Parser fisiere xlsx/csv pentru import prezentari. Arhitectura 2-treceri: Trecerea 1 — read_only=True: dim-check (FileTooLarge) + detectie multi-sheet. diff --git a/app/main.py b/app/main.py index a41e895..3a2f884 100644 --- a/app/main.py +++ b/app/main.py @@ -187,7 +187,7 @@ def metrics() -> str: conn = get_connection() try: rows = conn.execute("SELECT status, COUNT(*) AS n FROM submissions GROUP BY status").fetchall() - # Coada tinuta (PRD 5.19 US-007): observabilitate ops pt. default Auto OFF. + # Coada tinuta: observabilitate ops pt. default Auto OFF. # Gauge-uri DERIVATE (zero stare noua): total randuri tinute + varsta celui # mai vechi (din created_at), scoped global. Semnaleaza esecul silentios # cand prezentari raman nedeclarate tacit (risc L.142). diff --git a/app/mapping.py b/app/mapping.py index 9fe16d0..98a5e6b 100644 --- a/app/mapping.py +++ b/app/mapping.py @@ -52,7 +52,7 @@ def normalize_for_match(value: object) -> str: # --------------------------------------------------------------------------- # -# Pre-filtru determinist non-operatii (NUL) — US-001 PRD 5.18 # +# Pre-filtru determinist non-operatii (NUL) # # --------------------------------------------------------------------------- # # # Masuratoarea k-NN (memorie test-precizie-knn-embeddings) arata recall NUL doar @@ -88,7 +88,7 @@ _NUL_CTX_PIESA = re.compile( def prefiltru_nul(denumire: object) -> bool: """True daca operatia e gunoi evident (non-operatie de service) -> NUL determinist. - Ruleaza INAINTE de k-NN/embeddings in `enrich_suggestions` (US-006). Pur, fara DB. + Ruleaza INAINTE de k-NN/embeddings in `enrich_suggestions`. Pur, fara DB. Zero fals-pozitiv pe operatii reale (vezi comentariul de mai sus + tests). """ text = normalize_for_match(denumire) @@ -306,7 +306,7 @@ def resolve_prestatii( # Adnotare aditiva: marcheaza ca rezolvat-prin-regula cu pattern-ul # sursa. Payload-harmless (RAR citeste doar cod_prestatie). it["cod_sursa"] = f"{COD_SURSA_TEXT_RULE_PREFIX}{pattern_regula or ''}" - # US-001 (PRD 5.11): regula_fara_autosend nu se mai seteaza; + # regula_fara_autosend nu se mai seteaza; # auto_send nu mai tine randul (has_no_auto_send neutralizat). else: it["cod_prestatie"] = None @@ -466,7 +466,7 @@ def classify_prezentare( status = "needs_data" rar_error = json.dumps(errors, ensure_ascii=False) else: - # US-001 (PRD 5.11): ramura AUTO_SEND_OPRIT eliminata. + # Ramura AUTO_SEND_OPRIT eliminata. # Un cod rezolvat (mapare exacta sau regula text) -> queued direct. status = "queued" rar_error = None @@ -482,7 +482,7 @@ def classify_prezentare( def has_no_auto_send(resolved: list[dict], mapping_meta: dict[str, dict]) -> bool: - """Neutralizat dupa US-001 (PRD 5.11): auto_send nu mai tine randuri in needs_mapping. + """Neutralizat: auto_send nu mai tine randuri in needs_mapping. Simbolul este PASTRAT (importat in routes.py si import_router.py); stergerea ar produce ImportError la boot. Functia intoarce mereu False — codul rezolvat @@ -647,7 +647,7 @@ def _corpus_signature_silver(rows: list) -> str: def ensure_embeddings_corpus(conn, nomenclator: list[dict] | None = None) -> None: - """Construieste/actualizeaza corpusul embeddings din corpusul ETICHETAT (PRD 5.18 US-005). + """Construieste/actualizeaza corpusul embeddings din corpusul ETICHETAT. Sursa corpusului = `mapping_suggestions` (SILVER): exemple reale etichetate {denumire_normalizata -> cod, is_nul}, NU cele 18 categorii generice din @@ -660,8 +660,8 @@ def ensure_embeddings_corpus(conn, nomenclator: list[dict] | None = None) -> Non Cand e activat: indexeaza corpusul o singura data (lazy-load modelul ~230MB la prima chemare), re-indexeaza doar cand semnatura corpusului SILVER s-a schimbat. - Itemii NUL (is_nul=1, cod NULL) raman in corpus: un vecin NUL e semnal de supresie - (US-006). Degradare gratioasa: orice eroare lasa corpusul gol -> enrich cade pe restul. + Itemii NUL (is_nul=1, cod NULL) raman in corpus: un vecin NUL e semnal de supresie. + Degradare gratioasa: orice eroare lasa corpusul gol -> enrich cade pe restul. """ from .config import get_settings if not get_settings().embeddings_enabled: @@ -687,7 +687,7 @@ def ensure_embeddings_corpus(conn, nomenclator: list[dict] | None = None) -> Non ] _emb.index_corpus(items, signature=sig) except Exception: - pass # degradare gratioasa (#16b): esecul indexarii nu blocheaza editorul + pass # degradare gratioasa: esecul indexarii nu blocheaza editorul def enrich_suggestions( @@ -698,13 +698,13 @@ def enrich_suggestions( ) -> dict: """Imbogateste sugestiile cu GOLD partajat, SILVER LLM si embeddings NN. - Precedenta Eng-F2 (pentru sugestie-only, nu auto-send): + Precedenta (pentru sugestie-only, nu auto-send): shared GOLD > SILVER > embeddings (Account GOLD = operations_mapping propriu = deja rezolvat inainte de needs_mapping; nu apare in needs_mapping, deci nu e in precedenta de sugestie.) - Ordine completa (PRD 5.18 US-006): + Ordine completa: pre-filtru NUL determinist -> (daca NUL: fara cod, `surse['nul']=True`) altfel GOLD partajat > exact (SILVER) > k-NN embeddings. @@ -715,11 +715,11 @@ def enrich_suggestions( } INVARIANTE: - - Toate sursele = SUGGESTION-ONLY. NU intra in resolve_prestatii/load_mapping (#13). - - Pre-filtru NUL (US-001) ruleaza PRIMUL: gunoiul evident (ITP/plata/discount...) e + - Toate sursele = SUGGESTION-ONLY. NU intra in resolve_prestatii/load_mapping. + - Pre-filtru NUL ruleaza PRIMUL: gunoiul evident (ITP/plata/discount...) e marcat non-operatie INAINTE de k-NN, fara sugestie de cod. - - SILVER cu is_nul=1 (non-operatie/gunoi) NU produce sugestie (#4); vecin k-NN NUL idem. - - Degradare gratioasa pe embeddings (#16b): daca motorul nu e disponibil sau arunca, + - SILVER cu is_nul=1 (non-operatie/gunoi) NU produce sugestie; vecin k-NN NUL idem. + - Degradare gratioasa pe embeddings: daca motorul nu e disponibil sau arunca, returneaza sugestia disponibila din celelalte surse, fara exceptie. - Import local shared_store/embeddings: evita ciclu la import-time (shared_store importa normalize_for_match din mapping). @@ -730,7 +730,7 @@ def enrich_suggestions( if not denumire: return {"sugestie_principala": sugestie_principala, "surse": surse} - # 0. Pre-filtru NUL determinist (US-001) INAINTE de orice k-NN/lookup: non-operatie + # 0. Pre-filtru NUL determinist INAINTE de orice k-NN/lookup: non-operatie # evidenta -> fara cod, scurtcircuit (nu interogheaza embeddings/SILVER pe gunoi). if prefiltru_nul(denumire): surse["nul"] = True @@ -758,7 +758,7 @@ def enrich_suggestions( except Exception: pass # degradare gratioasa - # 3. Embeddings NN (similaritate semantica, degradare gratioasa #16b) + # 3. Embeddings NN (similaritate semantica, degradare gratioasa) if include_embeddings: try: from . import embeddings as _emb @@ -768,7 +768,7 @@ def enrich_suggestions( # ensure_embeddings_corpus (gated pe AUTOPASS_EMBEDDINGS_ENABLED); cand # flagul e off, has_corpus() ramane False si calea e un no-op real. if _emb.has_corpus(): - # F1 (US-005): corpusul k-NN e text NORMALIZAT (denumire_normalizata), + # Corpusul k-NN e text NORMALIZAT (denumire_normalizata), # deci query-ul TREBUIE normalizat la fel — altfel cosine degradeaza si # nu mai e configul sub care s-a masurat 94.3%. nn = _emb.suggest_nearest(normalize_for_match(denumire), top_k=1) @@ -776,7 +776,7 @@ def enrich_suggestions( # Evita recomandari irelevante cand corpus-ul e mic/partial. if nn and nn[0].get("similaritate", 0) >= EMB_MIN_SIMILARITATE: if nn[0].get("is_nul"): - # Vecin NUL (non-operatie) = semnal de SUPRESIE, nu cod (US-006). + # Vecin NUL (non-operatie) = semnal de SUPRESIE, nu cod. surse["nul"] = True elif nn[0].get("cod"): surse["embedding"] = str(nn[0]["cod"]) @@ -880,7 +880,7 @@ def reresolve_account(conn, account_id: int | None, batch_id: int | None = None) stats["still_blocked"] += 1 continue - # US-001 (PRD 5.11): ramura auto_send eliminata din reresolve. + # Ramura auto_send eliminata din reresolve. # Un cod rezolvat -> queued direct (review_manual ramane 0). errors = validate_prezentare(content) @@ -892,10 +892,10 @@ def reresolve_account(conn, account_id: int | None, batch_id: int | None = None) ) stats["needs_data"] += 1 else: - # PRD 5.19 US-002: re-snapshot `held` din comutatorul contului la trecerea + # Re-snapshot `held` din comutatorul contului la trecerea # needs_mapping -> queued (chokepoint held_for_account, `acct` deja normalizat # prin account_or_default la intrarea in reresolve_account). Altfel un rand - # deblocat din needs_mapping ar pleca automat desi contul e Auto OFF (R2). + # deblocat din needs_mapping ar pleca automat desi contul e Auto OFF. conn.execute( "UPDATE submissions SET status='queued', payload_json=?, rar_error=NULL, " "retry_count=0, next_attempt_at=NULL, held=?, updated_at=datetime('now') WHERE id=?", diff --git a/app/models.py b/app/models.py index d5d001d..b2b28ce 100644 --- a/app/models.py +++ b/app/models.py @@ -109,7 +109,7 @@ class SubmissionResult(BaseModel): # RE-ACTIVAT (re-clasificat + creds actualizate) la resubmit. `deduped` pastreaza # semantica actuala (clientii vechi care testeaza `deduped` nu se sparg). reactivated: bool = False - # Ecou PUR de coada (PRD 5.19 US-010): True cand randul e `queued AND held=1` + # Ecou PUR de coada: True cand randul e `queued AND held=1` # (tinut manual, cont Auto OFF) -> NU a plecat la RAR, asteapta eliberarea umana. # NU influenteaza payload/idempotenta; onestitate fata de integratorul API (invariant 5.7). held: bool = False diff --git a/app/operatii_seed.py b/app/operatii_seed.py index a097a76..032d04b 100644 --- a/app/operatii_seed.py +++ b/app/operatii_seed.py @@ -1,4 +1,4 @@ -"""Seeder corpus operatii etichetate -> mapping_suggestions (SILVER, PRD 5.18 US-004). +"""Seeder corpus operatii etichetate -> mapping_suggestions (SILVER). Artefactul `app/data/operatii-etichetate.json` e produs offline de `tools/mapare-llm/genereaza_seed.py` (etichetare LM Studio, o singura data) si comis diff --git a/app/payload.py b/app/payload.py index 5774c67..9d9feda 100644 --- a/app/payload.py +++ b/app/payload.py @@ -1,6 +1,6 @@ -"""Constructor payload postPrezentare (T4). +"""Constructor payload postPrezentare. -Reguli din contract (docs/api-rar-contract.md), confirmate live in T1: +Reguli din contract (docs/api-rar-contract.md), confirmate live: - status mereu "FINALIZATA". - tipPrestatie NU se trimite (server-generated GENERIC). - odometruFinal ca string; odometruInitial ca string cand e prezent, altfel null. diff --git a/app/payload_view.py b/app/payload_view.py index b158b60..06aafa2 100644 --- a/app/payload_view.py +++ b/app/payload_view.py @@ -1,8 +1,8 @@ """Extragere payload submission -> campuri afisabile. Helper PUR partajat intre canalul web (dashboard Trimiteri) si canalul API -(`GET /v1/prezentari`), ca extragerea sa NU diverge intre cele doua (decizie -eng review, DRY). Fara DB, fara request — primeste `payload_json` (text JSON +(`GET /v1/prezentari`), ca extragerea sa NU diverge intre cele doua. +Fara DB, fara request — primeste `payload_json` (text JSON plaintext, vezi `submissions.payload_json`) sau un dict deja parsat. Defensiv prin constructie: nu arunca niciodata pe payload malformat — degradeaza @@ -126,7 +126,7 @@ def prezentare_din_payload(payload: str | dict | None) -> dict[str, str]: op_service_denumire = _clean_str(item.get("denumire")) if op_service_cod else "" # obs: text liber observatii (camp RAR, optional). Conventie goala "" (nu EMPTY). - # US-005 PRD 5.15: obs traieste in payload_json (nu coloana separata). + # Traieste in payload_json, nu in coloana separata. obs = _clean_str(data.get("obs")) return { diff --git a/app/plans.py b/app/plans.py index caaf7ff..ef6b603 100644 --- a/app/plans.py +++ b/app/plans.py @@ -3,11 +3,11 @@ Pattern ca app/errors.py: catalog + helperi. Consumat de rutele de ingestie si dashboard. Nu importa DB, HTTP, sau orice alt modul intern cu efecte secundare. -Decizii implementare (PRD 5.17 / autoplan 2026-06-28): - - FREE_MONTHLY_LIMIT: constanta unica (T-CEO-2), tunabila fara arqueologie de cod. - - CONSUMED_STATUSES: decizie #20 — prestatie consumata = acceptata in coada. - - effective_tier: `now` injectabil (decizie #2) pentru teste deterministe. - - monthly_usage: pattern E7/5.15 (strftime localtime), `now` injectabil. +Note implementare: + - FREE_MONTHLY_LIMIT: constanta unica, tunabila fara arheologie de cod. + - CONSUMED_STATUSES: prestatie consumata = acceptata in coada. + - effective_tier: `now` injectabil pentru teste deterministe. + - monthly_usage: strftime localtime, `now` injectabil. """ from __future__ import annotations @@ -16,11 +16,10 @@ import sqlite3 from datetime import datetime, timezone # Limita lunara pentru planul Gratuit. -# Decizie user T-CEO-2 (2026-06-28): o singura constanta, referita din PLANS. -# Tunabila fara a modifica logica de enforcement. +# O singura constanta, referita din PLANS. Tunabila fara a modifica logica de enforcement. FREE_MONTHLY_LIMIT: int = 60 -# Statusurile care consuma din cota lunara (decizie #20, 2026-06-28). +# Statusurile care consuma din cota lunara. # Prestatie consumata = acceptata in coada (queued/sending/sent), nu cele respinse/blocate. # Rationale: limita e pe ce trimitem la RAR, nu pe incercari esuate sau blocate. CONSUMED_STATUSES: tuple[str, ...] = ("queued", "sending", "sent") @@ -30,7 +29,7 @@ CONSUMED_STATUSES: tuple[str, ...] = ("queued", "sending", "sent") # monthly_limit -- None = nelimitat; int = limita prestatii/luna # api_access -- True = acces import prin API (/v1/*); False = doar web dashboard # -# Aliniat landing-ului comercial (PRD 5.17 US-001): +# Aliniat landing-ului comercial: # Gratuit: 60/luna, fara API # Standard: nelimitat, fara API # Pro: nelimitat, cu API diff --git a/app/rar_env.py b/app/rar_env.py index 43ba425..8c83d8a 100644 --- a/app/rar_env.py +++ b/app/rar_env.py @@ -1,4 +1,4 @@ -"""Medii RAR per cont (PRD 5.20): disponibilitate + default efectiv. +"""Medii RAR per cont: disponibilitate + default efectiv. Sursa UNICA de adevar pentru REQ-DISP / REQ-DEFAULT: vizibilitatea selector/toggle in UI, validarea tintei in API si decizia worker-ului citesc TOATE de aici, ca sa @@ -92,7 +92,7 @@ def rar_env_efectiv_cont(conn: sqlite3.Connection, account_id: int) -> str | Non # --------------------------------------------------------------------------- # -# Exceptie si rezolvator de mediu tinta (US-004, dependent de US-002) # +# Exceptie si rezolvator de mediu tinta # # --------------------------------------------------------------------------- # class MediuIndisponibil(Exception): @@ -126,7 +126,7 @@ def rezolva_rar_env( 4. `cerut` None -> incearca rar_env_efectiv_cont (default-ul contului). 5. Daca contul nu are niciun mediu disponibil (rar_env_efectiv_cont == None) -> cade pe ancora globala get_settings().rar_env, normalizata la VALID_ENVS. - Acest fallback e intentionat (PRD 5.20 §2 Non-Goals): AUTOPASS_RAR_ENV ramane + Acest fallback e intentionat: AUTOPASS_RAR_ENV ramane ancora de migrare si fallback pentru actiuni fara cont (keepalive, canal API cu creds efemere pe conturi nou-create fara medii configurate). diff --git a/app/schema.sql b/app/schema.sql index 1582cbf..50acc1b 100644 --- a/app/schema.sql +++ b/app/schema.sql @@ -1,6 +1,5 @@ -- Schema SQLite (WAL) pentru gateway RAR AUTOPASS. --- Vezi plan.md sect. 5 + plan-treapta2.md sect. 4. --- Treapta 2: adauga conturi cu creds RAR durabile, tabele import, atestari. +-- Conturi cu creds RAR durabile, tabele import, atestari. PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; @@ -10,7 +9,7 @@ CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, cui TEXT, - email TEXT, -- email canonic de contact al firmei (US-001, PRD 5.12); nullable pt. conturi legacy + email TEXT, -- email canonic de contact al firmei; nullable pt. conturi legacy active INTEGER NOT NULL DEFAULT 1, -- lifecycle cont (3.1); gate „in asteptare" consumat de 3.3 -- Stare de ciclu de viata explicita (5.5). Superset al lui `active`: mentinem invariantul -- active=1 <=> status='active' (vezi accounts.set_status / set_active). Worker gate-uieste pe status. @@ -19,8 +18,8 @@ CREATE TABLE IF NOT EXISTS accounts ( -- vezi accounts.delete_account — randul ramane doar pentru audit). status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('pending','active','blocked','archived','deleted')), - -- Medii RAR per cont (PRD 5.20 US-001). Fiecare mediu = bifa de activare + slot creds. - -- accounts.rar_creds_enc (legacy env-less) a fost dropata in US-013 (SQLite DROP COLUMN). + -- Medii RAR per cont. Fiecare mediu = bifa de activare + slot creds. + -- accounts.rar_creds_enc (legacy env-less) a fost dropata (SQLite DROP COLUMN). -- medii_disponibile = enabled AND creds prezente (app/rar_env.py). Cont client nou = -- Productie on / Testare off (clientii declara real); contul operator se pune manual pe Testare. rar_test_enabled INTEGER NOT NULL DEFAULT 0 CHECK (rar_test_enabled IN (0, 1)), @@ -33,7 +32,7 @@ CREATE TABLE IF NOT EXISTS accounts ( -- 1 (respinge cererea fara enqueue). Override per-cerere via PrezentareRequest.on_unmapped_error. on_unmapped_error_default INTEGER NOT NULL DEFAULT 0 CHECK (on_unmapped_error_default IN (0, 1)), - -- Comutator Auto per cont (PRD 5.19 US-001). AL TREILEA `auto_send` din schema (R6): + -- Comutator Auto per cont. AL TREILEA `auto_send` din schema: -- DISTINCT de operations_mapping.auto_send si operation_text_rules.auto_send, care sunt -- PER-OPERATIE. Acesta e PER-CONT si guverneaza DOAR: (a) valoarea implicita a lui -- submissions.held la ingestie (held = 0 daca 1, altfel 1 — vezi accounts.held_for_account); @@ -104,7 +103,7 @@ CREATE TABLE IF NOT EXISTS submissions ( status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued','sending','sent','needs_mapping','needs_data','error')), payload_json TEXT NOT NULL, - -- Mediul RAR tinta al acestei trimiteri (PRD 5.20 US-001). DEFAULT 'test' e doar plasa + -- Mediul RAR tinta al acestei trimiteri. DEFAULT 'test' e doar plasa -- pentru randuri net-noi care nu seteaza explicit; fiecare INSERT (API/import/web) seteaza -- rar_env explicit. Backfill din AUTOPASS_RAR_ENV global la migrare (NU lasa pe DEFAULT). rar_env TEXT NOT NULL DEFAULT 'test' CHECK (rar_env IN ('test', 'prod')), @@ -113,12 +112,12 @@ CREATE TABLE IF NOT EXISTS submissions ( rar_error TEXT, id_prezentare INTEGER, -- data.id intors de RAR la succes retry_count INTEGER NOT NULL DEFAULT 0, - next_attempt_at TEXT, -- backoff: randul nu se ia inainte de acest moment (T2) - sending_since TEXT, -- pentru lease/timeout pe randuri 'sending' orfane (T2) - purge_after TEXT, -- sent + 90z (T16) - batch_id INTEGER, -- import batch (T7; NULL = canal API) - row_index INTEGER, -- rand in batch (T7; NULL = canal API) - -- Randuri tinute manual (PRD 5.19 US-001). rand `queued AND held=1` = "In asteptare + next_attempt_at TEXT, -- backoff: randul nu se ia inainte de acest moment + sending_since TEXT, -- pentru lease/timeout pe randuri 'sending' orfane + purge_after TEXT, -- sent + 90z + batch_id INTEGER, -- import batch (NULL = canal API) + row_index INTEGER, -- rand in batch (NULL = canal API) + -- Randuri tinute manual. rand `queued AND held=1` = "In asteptare -- (manual)": tinut pana la eliberare umana (trimitere manuala per rand/bulk: held 1 -> 0). -- Worker-ul (claim_one) ia doar `queued AND held=0`. held NU intra in payload/idempotenta -- si NU pleaca la RAR — e pur control de coada (ca import_rows.reviewed). Snapshot la @@ -134,7 +133,7 @@ CREATE INDEX IF NOT EXISTS idx_submissions_account_status ON submissions(account -- ruleaza executescript(schema.sql) INAINTE de _migrate; pe un DB existent pre-`held`/`batch_id` -- coloana inca nu exista in momentul executarii schemei, deci un CREATE INDEX pe ea ar esua -- (no such column). Indexul partial `idx_submissions_held ON submissions(held) WHERE held=1` --- (PRD 5.19 US-001) traieste doar in _migrate, ca `idx_submissions_batch`. +-- traieste doar in _migrate, ca `idx_submissions_batch`. -- Mapare coloane fisier -> campuri canonice (retinuta per cont, semnatura coloane). CREATE TABLE IF NOT EXISTS column_mappings ( @@ -162,7 +161,7 @@ CREATE TABLE IF NOT EXISTS import_batches ( already_sent INTEGER NOT NULL DEFAULT 0, duplicate_in_file INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), - purge_after TEXT -- created_at + 90z (T16) + purge_after TEXT -- created_at + 90z ); -- Randuri din lot de import (PII criptat cu Fernet). @@ -171,8 +170,8 @@ CREATE TABLE IF NOT EXISTS import_rows ( batch_id INTEGER NOT NULL REFERENCES import_batches(id) ON DELETE CASCADE, row_index INTEGER NOT NULL, raw_json TEXT NOT NULL, -- PII criptat (Fernet, ca submissions) - override_json TEXT, -- patch CANONIC editat in preview, criptat Fernet (3.6, Approach B); NULL = fara editare - reviewed INTEGER NOT NULL DEFAULT 0, -- US-007 (PRD 5.12): 0=neconfirmat, 1=confirmat de operator; NU intra in payload/idempotenta + override_json TEXT, -- patch CANONIC editat in preview, criptat Fernet; NULL = fara editare + reviewed INTEGER NOT NULL DEFAULT 0, -- 0=neconfirmat, 1=confirmat de operator; NU intra in payload/idempotenta resolved_status TEXT NOT NULL DEFAULT 'pending' CHECK (resolved_status IN ( 'pending','ok','needs_mapping','needs_data', @@ -207,15 +206,15 @@ CREATE TABLE IF NOT EXISTS users ( created_at TEXT NOT NULL DEFAULT (datetime('now')) ); --- Jurnal de aplicatie la nivel de eveniment (PRD 5.6). Dublu canal: aici (vizibil +-- Jurnal de aplicatie la nivel de eveniment. Dublu canal: aici (vizibil -- in tab "Jurnal") + log text rotativ (depanare). `tip` e text liber documentat --- (lista extensibila, decizie §5) — adaugam tipuri fara migrare. Toate valorile +-- (lista extensibila) — adaugam tipuri fara migrare. Toate valorile -- sunt REDACTATE la scriere (app/observ.py via app/security.py): parole/token -> -- ***REDACTED***, VIN/nr partial. `context_json` = metadate (NU payload PII integral). CREATE TABLE IF NOT EXISTS app_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL DEFAULT (datetime('now')), - request_id TEXT, -- corelare cu raspunsul clientului (US-002) + request_id TEXT, -- corelare cu raspunsul clientului account_id INTEGER, -- NULL = eveniment de sistem (fara cont) sursa TEXT NOT NULL DEFAULT 'api' CHECK (sursa IN ('api','worker')), @@ -224,14 +223,14 @@ CREATE TABLE IF NOT EXISTS app_events ( cod TEXT, -- cod din catalogul de erori (app/errors.py) daca aplica mesaj TEXT, -- mesaj scurt redactat context_json TEXT, -- JSON metadate redactate (submission_id, count, status...) - purge_after TEXT -- ts + log_retention_days (US-008) + purge_after TEXT -- ts + log_retention_days ); CREATE INDEX IF NOT EXISTS idx_app_events_ts ON app_events(ts); CREATE INDEX IF NOT EXISTS idx_app_events_account ON app_events(account_id, ts); CREATE INDEX IF NOT EXISTS idx_app_events_tip ON app_events(tip); --- Reguli automate de mapare pe text (substring, per cont). PRD 5.8 US-001. --- auto_send DEFAULT 0 (decizie CEO 2026-06-24): substring are blast-radius mai mare +-- Reguli automate de mapare pe text (substring, per cont). +-- auto_send DEFAULT 0: substring are blast-radius mai mare -- decat maparea exacta; o regula noua rezolva codul dar tine randul pentru verificare -- umana pana cand operatorul activeaza explicit "In coada". CREATE TABLE IF NOT EXISTS operation_text_rules ( @@ -246,7 +245,7 @@ CREATE TABLE IF NOT EXISTS operation_text_rules ( ); CREATE INDEX IF NOT EXISTS idx_text_rules_account ON operation_text_rules(account_id); --- Sugestii de mapare (strat SILVER, L14-S3 PRD 5.14). +-- Sugestii de mapare (strat SILVER). -- Etichete LLM/embedding — bootstrap; citita DOAR de suggest_codes/pending_unmapped, -- NICIODATA de load_mapping/resolve_prestatii (separare structurala #13). -- Cheia = denumire normalizata (fara diacritice, uppercase, spatii colapsate). @@ -265,7 +264,7 @@ CREATE TABLE IF NOT EXISTS mapping_suggestions ( CREATE INDEX IF NOT EXISTS idx_mapping_suggestions_cod ON mapping_suggestions(cod_prestatie) WHERE cod_prestatie IS NOT NULL; --- Mapari validate de oameni (strat GOLD partajat cross-account, L14-S3 PRD 5.14). +-- Mapari validate de oameni (strat GOLD partajat cross-account). -- Confirmarile umane din ORICE cont contribuie la acest store (#8). -- cross-account = suggestion-only (pre-completeaza editorul, F-A/#11), NU auto-send. -- Auto-send DOAR din operations_mapping (GOLD propriu per-cont, Eng-F2). diff --git a/app/shared_store.py b/app/shared_store.py index 1bc8169..cf8ddd7 100644 --- a/app/shared_store.py +++ b/app/shared_store.py @@ -1,17 +1,17 @@ """Store partajat pentru sugestii (SILVER) si mapari validate de oameni (GOLD cross-account). -Straturi (L14-S3, PRD 5.14): +Straturi: - mapping_suggestions (SILVER): sugestii LLM/embedding, citite DOAR de suggest/pending_unmapped, - NICIODATA de load_mapping/resolve_prestatii (separare structurala #13). + NICIODATA de load_mapping/resolve_prestatii (separare structurala). - shared_mappings (GOLD partajat): mapari validate de om din orice cont; pot pre-completa - editorul (suggestion-only cross-account, F-A/#11); auto-send DOAR GOLD propriu - (operations_mapping per-cont, Eng-F2). + editorul (suggestion-only cross-account); auto-send DOAR GOLD propriu + (operations_mapping per-cont). Invariante: -- INSERT OR IGNORE la seed: nu suprascrie randuri existente (#2). -- NUL = is_nul=1, cod_prestatie NULL; NU se promoveaza la cod RAR (#4). -- source/confidence pe fiecare rand (provenienta + rollback batch model prost, #5). -- Wiring in resolve_prestatii/load_mapping vine in L14-S6; modulul de fata e API pur. +- INSERT OR IGNORE la seed: nu suprascrie randuri existente. +- NUL = is_nul=1, cod_prestatie NULL; NU se promoveaza la cod RAR. +- source/confidence pe fiecare rand (provenienta + rollback batch model prost). +- Wiring in resolve_prestatii/load_mapping nu e cablat inca; modulul de fata e API pur. """ from __future__ import annotations diff --git a/app/submissions_admin.py b/app/submissions_admin.py index 2e648f6..921fc2f 100644 --- a/app/submissions_admin.py +++ b/app/submissions_admin.py @@ -104,7 +104,7 @@ def requeue_submission(conn, account_id: int, sid: int) -> dict: valid_codes = load_nomenclator_codes(conn) or None cl = classify_prezentare(content, mapping, mapping_meta, valid_codes) - # PRD 5.19 US-002: re-snapshot `held` din comutatorul contului la re-punere in coada + # Re-snapshot `held` din comutatorul contului la re-punere in coada # (paritate cu create_prezentari/reresolve_account). Fara asta un rand repus pastra # `held` VECHI si s-ar auto-trimite desi contul e Auto OFF (sau ar ramane blocat cand e ON). held_val = held_for_account(conn, account_or_default(account_id)) diff --git a/app/validation.py b/app/validation.py index ff66aba..f8f0115 100644 --- a/app/validation.py +++ b/app/validation.py @@ -1,4 +1,4 @@ -"""Validare de domeniu pentru prezentari (T3). +"""Validare de domeniu pentru prezentari. Replica regulile RAR (docs/api-rar-contract.md sect. "Reguli de validare") ÎNAINTE de enqueue, ca sa nu primim 4xx de la RAR. Spre deosebire de validarea de SHAPE diff --git a/app/web/admin_routes.py b/app/web/admin_routes.py index ced99d7..b1fc996 100644 --- a/app/web/admin_routes.py +++ b/app/web/admin_routes.py @@ -127,7 +127,7 @@ def _apply_lifecycle(conn, ids: list[int], action: str) -> None: for aid in ids: try: if action == "activate": - # Gate US-002: nu activam conturi fara identitate completa (companie+email+CUI) + # Nu activam conturi fara identitate completa (companie+email+CUI) acct_row = conn.execute( "SELECT id, name, cui, email FROM accounts WHERE id=?", (aid,) ).fetchone() diff --git a/app/web/auth_routes.py b/app/web/auth_routes.py index 22a62ac..84473d4 100644 --- a/app/web/auth_routes.py +++ b/app/web/auth_routes.py @@ -79,7 +79,6 @@ async def signup_post( name=name, cui=cui, email=email, plan=requested_plan, ), status_code=422) - # CUI obligatoriu la signup (US-001, PRD 5.12) cui_norm = cui.strip().upper() if cui else "" if not cui_norm: return _TMPL.TemplateResponse(request, "signup.html", _ctx( diff --git a/app/web/labels.py b/app/web/labels.py index 80c728f..bc03819 100644 --- a/app/web/labels.py +++ b/app/web/labels.py @@ -72,7 +72,7 @@ ETICHETE_SCURTE: dict[str, str] = { # --------------------------------------------------------------------------- -# Randuri tinute manual (held) — US-006, PRD 5.19 +# Randuri tinute manual (held). # # `held` NU e o stare noua in masina de stari: un rand cu `status='queued' AND # held=1` este in coada, dar tinut pana la eliberarea umana. Il afisam distinct @@ -96,9 +96,8 @@ def eticheta_scurta(status: str, held: bool = False) -> str: Returneaza eticheta compacta (pill) pentru o stare de submission. Cand `status='queued'` si `held` este adevarat, randul e tinut manual si - intoarce "Manual" (distinct de "In coada") — vezi PRD 5.19 D5. Pentru orice - alta stare `held` este ignorat, iar apelul fara `held` ramane identic cu cel - dinainte de US-006. + intoarce "Manual" (distinct de "In coada"). Pentru orice alta stare `held` + este ignorat. Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda stari noi adaugate in schema fara mapare corespunzatoare. @@ -120,8 +119,7 @@ def eticheta_stare(status: str, held: bool = False) -> Eticheta: Cand `status='queued'` si `held` este adevarat, randul e tinut manual si intoarce eticheta "In asteptare (manual)" cu clasa de avertizare (amber / - --warn) — vezi PRD 5.19 US-006/D5. Pentru orice alta stare `held` este - ignorat, iar apelul fara `held` ramane identic cu cel dinainte de US-006. + --warn). Pentru orice alta stare `held` este ignorat. Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda stari noi adaugate in schema fara mapare corespunzatoare. @@ -385,7 +383,7 @@ def nota_umana_preview(status: str, errors: list, flags: list) -> str: Primeste ``errors`` ca lista Python (nu JSON string) — NU pasa la motiv_uman sau parse_erori care asteapta un JSON string si ar produce repr Python brut - prin fallback ``raw[:160]`` (bug documentat in PRD 5.11 US-003). + prin fallback ``raw[:160]``. Logica de prioritate: - already_sent / duplicate_in_file -> "" (template le afiseaza separat) @@ -436,7 +434,7 @@ def nota_umana_preview(status: str, errors: list, flags: list) -> str: # --------------------------------------------------------------------------- -# Etichete mediu RAR (Test / Productie) — US-010 PRD 5.20 +# Etichete mediu RAR (Test / Productie) # # Conventia de culori (semantica risc L.142): # prod = env-badge-prod (fill atentie, text alb) — declaratie REALA, ireversibila. diff --git a/app/web/templates/_acasa.html b/app/web/templates/_acasa.html index 82000d9..4876b64 100644 --- a/app/web/templates/_acasa.html +++ b/app/web/templates/_acasa.html @@ -1,6 +1,6 @@
- {# === Banner ne-blocant: cont incomplet (US-002) === + {# === Banner ne-blocant: cont incomplet === Apare cand accounts.name / email / CUI sunt necompletate (conturi legacy sau create din CLI). NU blocheaza importul sau uploadul — doar orienteaza operatorul sa completeze datele. Dispare automat dupa ce contul devine complet (re-render la urmatoarea navigare/reload). @@ -13,7 +13,7 @@
{% endif %} - {# === Container colapsabil: stepper + upload intr-un singur element
(US-006). + {# === Container colapsabil: stepper + upload intr-un singur element
. Serverul seteaza atributul `open` din are_trimiteri: are_trimiteri=False (first-run) → open (importul e vizibil imediat, fara JS) are_trimiteri=True (returning) → colapsat (nu ocupa ecranul, dar e accesibil la click) diff --git a/app/web/templates/_auto_send_toggle.html b/app/web/templates/_auto_send_toggle.html index 5608035..6110e91 100644 --- a/app/web/templates/_auto_send_toggle.html +++ b/app/web/templates/_auto_send_toggle.html @@ -1,10 +1,10 @@ {# - _auto_send_toggle.html — comutatorul "Trimite automat la RAR" (PRD 5.19 US-004). + _auto_send_toggle.html — comutatorul "Trimite automat la RAR". - Plasat in clusterul de header (base.html, langa .rar-chip) — D1. NU face parte din - swap-ul periodic al #status-bar (every 15s) -> fara flicker / pierdere focus (D3). + Plasat in clusterul de header (base.html, langa .rar-chip). NU face parte din + swap-ul periodic al #status-bar (every 15s) -> fara flicker / pierdere focus. - Non-optimist (D2): la esec POST bifa revine + toast eroare (hx-on::response-error). + Non-optimist: la esec POST bifa revine + toast eroare (hx-on::response-error). account_id vine din SESIUNE server-side (ruta /auto-send), niciodata din formular. Raspunsul POST re-randeaza acest fragment (outerHTML pe #auto-send-toggle-wrap): diff --git a/app/web/templates/_bulk_held_banner.html b/app/web/templates/_bulk_held_banner.html index 752829a..ef7e9be 100644 --- a/app/web/templates/_bulk_held_banner.html +++ b/app/web/templates/_bulk_held_banner.html @@ -1,10 +1,10 @@ {# - _bulk_held_banner.html — banner bulk "Trimite toate (N)" (PRD 5.19 US-005). + _bulk_held_banner.html — banner bulk "Trimite toate (N)". N = randuri tinute ale contului (queued AND held=1). Randat: - initial in _coada.html (banner_oob nesetat -> fara OOB); - RE-RANDAT prin OOB (banner_oob=true) din _submissions.html la FIECARE reincarcare a - listei, ca sa nu ramana stale dupa eliberari (fix /code-review #3). + listei, ca sa nu ramana stale dupa eliberari. Wrapper cu id STABIL #bulk-held-banner-wrap: OOB outerHTML il inlocuieste mereu (chiar si gol la N=0), ca id-ul sa ramana tinta pentru urmatorul OOB. Confirmare tipata prin modal diff --git a/app/web/templates/_chips_prestatii.html b/app/web/templates/_chips_prestatii.html index 728fca8..9d0db05 100644 --- a/app/web/templates/_chips_prestatii.html +++ b/app/web/templates/_chips_prestatii.html @@ -20,7 +20,7 @@ {% set _sec_id = chips_section_id or 'chips-section' %} {% set _chips = prestatii_chips or [] %} {% set _has_ops = _chips | selectattr('cod_op_service') | list | length > 0 %} -{# US-009: chips_submission_id e setat din _detaliu_ctx cand chips sunt randate in modalul de detaliu. +{# chips_submission_id e setat din _detaliu_ctx cand chips sunt randate in modalul de detaliu. Lipseste cand _chips_prestatii.html e rerandat via /form-chips (stateless, fara submission). #} {% set _sub_id = chips_submission_id if chips_submission_id is defined else none %} @@ -72,7 +72,7 @@ × - {# US-009: "salveaza ca regula op->cod" — apare doar cand submission_id e cunoscut + {# "salveaza ca regula op->cod" — apare doar cand submission_id e cunoscut (in modalul de detaliu, nu la re-randarea stateless via /form-chips). Reuse EXACT save_mapping + reresolve_account via endpoint dedicat. hx-include="closest form" propaga csrf_token din form-ul parinte. #} @@ -117,9 +117,9 @@ {% endif %} {% endfor %} - {# ===== US-005 (5.16): Chips extra + picker '+ Adauga alta operatie / cod RAR' in mod operatii ===== #} + {# ===== Chips extra + picker '+ Adauga alta operatie / cod RAR' in mod operatii ===== #} {# Chips extra: cod_op_service gol, cod_prestatie setat — afisate flat cu × (reuse remove_flat). - T-7 (5.16): containerul .chips se randeaza DOAR cand exista chips extra — altfel ramanea + Containerul .chips se randeaza DOAR cand exista chips extra — altfel ramanea un chenar gol nefinisat sub randurile de operatie. #} {% set _extra_chips = _chips | rejectattr('cod_op_service') | selectattr('cod_prestatie') | list %} {% if _extra_chips %} diff --git a/app/web/templates/_coada.html b/app/web/templates/_coada.html index ece74ec..565bba2 100644 --- a/app/web/templates/_coada.html +++ b/app/web/templates/_coada.html @@ -6,13 +6,13 @@ style="margin-top:22px; padding-top:18px; border-top:2px solid var(--line);" {% if oob %}hx-swap-oob="outerHTML"{% endif %}>
- {# US-002 (5.16): titlul de sectiune vizibil ("Trimiterile tale") a fost eliminat — - lista incepe direct sub filtre. Heading pastrat sr-only pentru a11y (section + {# Titlul de sectiune vizibil ("Trimiterile tale") a fost eliminat — lista incepe + direct sub filtre. Heading pastrat sr-only pentru a11y (section aria-labelledby). Badge-ul de atentie + export CSV stau intr-un rand discret. #}

Trimiterile tale

- {# US-005 (PRD 5.19): bulk "Trimite toate (N)". Randat initial aici; re-randat prin OOB - din _submissions.html la fiecare reincarcare a listei (fix staleness /code-review #3). #} + {# Bulk "Trimite toate (N)". Randat initial aici; re-randat prin OOB + din _submissions.html la fiecare reincarcare a listei (evita staleness). #} {% include '_bulk_held_banner.html' %} {% if blocate_total %} @@ -44,8 +44,8 @@ {# Pagina curenta — actualizata prin OOB swap din _submissions.html; inclusa la reincarcari. #} - {# US-007 (5.19): filtrul "doar tinute" — persistat prin OOB din _submissions.html, ca sa - nu se piarda la reincarcare (trimiteriChanged) sau paginare (fix /code-review #2). #} + {# Filtrul "doar tinute" — persistat prin OOB din _submissions.html, ca sa + nu se piarda la reincarcare (trimiteriChanged) sau paginare. #} {# === STANGA: Quick-pills de data (preset interval) + buton Custom === diff --git a/app/web/templates/_editare_preview_modal.html b/app/web/templates/_editare_preview_modal.html index 5418b00..34035ae 100644 --- a/app/web/templates/_editare_preview_modal.html +++ b/app/web/templates/_editare_preview_modal.html @@ -1,7 +1,7 @@ {# _editare_preview_modal.html — fragment de editare rand preview in modalul global. - US-006 (PRD 5.12): swap-uit in #detaliu-modal-body de butonul Editeaza din preview. - US-007 (PRD 5.12): butonul 'Confirma valorile' apare DOAR pe randurile needs_review - (T2): trimite CSRF POST la /confirma-review, inchide modalul via HX-Trigger-After-Settle. + Swap-uit in #detaliu-modal-body de butonul Editeaza din preview. + Butonul 'Confirma valorile' apare DOAR pe randurile needs_review si trimite CSRF + POST la /confirma-review, inchide modalul via HX-Trigger-After-Settle. Necesita din context: import_id — id batch import @@ -52,7 +52,7 @@ {% if is_needs_review %} - {# T2 (US-007): Butonul 'Confirma valorile' apare DOAR pe randurile needs_review. + {# Butonul 'Confirma valorile' apare DOAR pe randurile needs_review. POST separat (form propriu) la /confirma-review cu CSRF. Raspunsul inchide modalul via HX-Trigger-After-Settle: inchideModal + swap OOB randul si countorii. #}
element al template-ului parinte. Acel parinte pune form-ul, CSRF-ul si orice campuri @@ -36,7 +36,7 @@ err_map=err_map, fix_map=fix_map, vin_context=vin_context) }}
-{# === 3. Observatii (obs) — textarea liber, US-005 === #} +{# === 3. Observatii (obs) — textarea liber === #}
-{# === 4. Prestatii chips (E4 server-driven, US-007) === #} +{# === 4. Prestatii chips (server-driven) === #} {% set form_chips_url = form_chips_url or '/form-chips' %} {% set chips_section_id = 'chips-section' %} {% include "_chips_prestatii.html" %} diff --git a/app/web/templates/_jurnal.html b/app/web/templates/_jurnal.html index fbc5fba..dc5a976 100644 --- a/app/web/templates/_jurnal.html +++ b/app/web/templates/_jurnal.html @@ -10,7 +10,7 @@ {% else %} doar evenimentele contului tau {% endif %} - {# Badge mediu RAR activ (US-010 PRD 5.20) — mediul implicit al contului #} + {# Badge mediu RAR activ — mediul implicit al contului #} {% if env_default | default('') %} {% set _eb = eticheta_env(env_default) %} {{ _eb[0] }} diff --git a/app/web/templates/_macros.html b/app/web/templates/_macros.html index 03d8b05..02e9920 100644 --- a/app/web/templates/_macros.html +++ b/app/web/templates/_macros.html @@ -1,12 +1,12 @@ {# Macro-uri partajate intre template-urile de import si mapari. #} -{# US-002 (PRD 5.11): autosend_toggle neutralizat — auto_send nu mai tine randuri (US-001). +{# autosend_toggle neutralizat — auto_send nu mai tine randuri. Simbolul pastrat (apelat in _mapari.html, _preview_import.html, _trimitere_detaliu.html) dar intoarce string gol. Coloanele DB raman (default 1, ne-citite pentru hold). #} {% macro autosend_toggle(form_id='', checked=True, label='') -%}{%- endmacro %} -{# US-005 (PRD 5.12): macro `camp` partajat — extras din _trimitere_detaliu.html si - _preview_rand.html. Suporta tip='date' (calendar nativ, D#10/R3) si tip='text' (default). +{# macro `camp` partajat — extras din _trimitere_detaliu.html si + _preview_rand.html. Suporta tip='date' (calendar nativ) si tip='text' (default). Parametri: nome — name="" al input-ului (si cheie in err_map/fix_map) @@ -19,14 +19,14 @@ id_prefix — prefix pentru id="" al input-ului (default 'c'; preview poate folosi 'e-N') #} {% macro camp(nome, eticheta, valoare, tip='text', err_map={}, fix_map={}, vin_context='', id_prefix='c', slim=False, mono=False) %} -{# slim=False: randare clasica (neschimbata). slim=True: varianta compacta (.camp-slim) din US-002 PRD 5.15: +{# slim=False: randare clasica (neschimbata). slim=True: varianta compacta (.camp-slim): label 11px muted deasupra, input ~30px, fundal --card2. mono=True (valid numai cu slim=True): adauga clasa 'camp-mono' pe input pentru campuri VIN/odometru/nr (IBM Plex Mono, prin .camp-slim .camp-mono din base.html). #}
{% if tip == 'date' %} - {# D#10/R3: degradare grijulie pentru valori ne-YYYY-MM-DD. + {# Degradare grijulie pentru valori ne-YYYY-MM-DD. Daca valoarea nu e in formatul corect, inputul ramane gol + hint + hidden cu valoarea bruta (ca sa nu se piarda tacut la submit). #} {%- set _dp_ok = (valoare and valoare|length == 10 and valoare[4:5] == '-' and valoare[7:8] == '-') -%} @@ -57,7 +57,7 @@
{% endmacro %} -{# PRD 5.13 — sistem butoane de actiune responsive. +{# Sistem butoane de actiune responsive. CSS-ul aferent (.act, .act-tx, .act-ic, .act-save, .act-del, .act-group) este definit in base.html. Desktop: se afiseaza textul (.act-tx); mobil: se afiseaza iconita (.act-ic). #} diff --git a/app/web/templates/_mapari.html b/app/web/templates/_mapari.html index 4d631a3..ff75cc1 100644 --- a/app/web/templates/_mapari.html +++ b/app/web/templates/_mapari.html @@ -54,7 +54,7 @@
{{ e.denumire or '(fara denumire)' }}
- {# 5.18 US-007: badge sursa pe sugestia sistemului — confirmat (GOLD) / similar + {# Badge sursa pe sugestia sistemului — confirmat (GOLD) / similar (SILVER+embedding k-NN) / non-operatie (pre-filtru NUL). Suggestion-only. #} {% if e.sugestie_principala %} {% if e.sugestie_principala.sursa == 'gold_partajat' %} diff --git a/app/web/templates/_mapcoloane.html b/app/web/templates/_mapcoloane.html index 3344ac5..f227caf 100644 --- a/app/web/templates/_mapcoloane.html +++ b/app/web/templates/_mapcoloane.html @@ -82,7 +82,7 @@ - {# Tabel mapare: coloana din fisier | exemplu | camp RAR (mockup 5.16 / US-013) #} + {# Tabel mapare: coloana din fisier | exemplu | camp RAR #}
diff --git a/app/web/templates/_modal_confirma_trimitere.html b/app/web/templates/_modal_confirma_trimitere.html index e848fd3..f0ecefb 100644 --- a/app/web/templates/_modal_confirma_trimitere.html +++ b/app/web/templates/_modal_confirma_trimitere.html @@ -1,5 +1,5 @@ {# - _modal_confirma_trimitere.html — modal REAL de confirmare tipata (PRD 5.19 D4/F4/F5). + _modal_confirma_trimitere.html — modal REAL de confirmare tipata. `hx-confirm` nativ (doar OK/Cancel) NU e suficient pentru o actiune ireversibila care trimite N prezentari catre RAR. Acest modal arata numarul + mediul tinta (Testare / diff --git a/app/web/templates/_preview_import.html b/app/web/templates/_preview_import.html index afe38bb..b2c6438 100644 --- a/app/web/templates/_preview_import.html +++ b/app/web/templates/_preview_import.html @@ -14,7 +14,7 @@ Preview — {{ filename or ("import #" ~ import_id) }} - {# Badge mediu RAR (US-009): vizibil intotdeauna in preview (claritate tinta) #} + {# Badge mediu RAR: vizibil intotdeauna in preview (claritate tinta) #} {% if rar_env %}

Operatii de mapat la cod RAR

@@ -98,7 +98,7 @@ {% if e.suggestions or e.sugestie_principala or (e.surse_sugestie and e.surse_sugestie.nul) %}
sugestii: - {# 5.18 US-007: badge sursa pe sugestia sistemului — confirmat (GOLD) / similar + {# Badge sursa pe sugestia sistemului — confirmat (GOLD) / similar (SILVER+embedding k-NN) / non-operatie (pre-filtru NUL). Suggestion-only. #} {% if e.sugestie_principala %} {% if e.sugestie_principala.sursa == 'gold_partajat' %} @@ -134,7 +134,7 @@
{% endif %} - @@ -213,7 +213,7 @@ hx-target="#import-section" hx-swap="outerHTML"> - {# Mediu RAR ales la upload — propagar la commit (US-009) #} + {# Mediu RAR ales la upload — propagar la commit #} {% endif %} - {# US-006 (5.17) — Banner one-time trial->Gratuit (T-DES-1): afisat la prima incarcare + {# Banner one-time trial->Gratuit: afisat la prima incarcare dupa expirarea trial-ului. Discret, non-blocant; dismissibil via sessionStorage. Nu acopera stripul de sanatate (apare inainte de health strip, la acelasi nivel). #} {% if trial_expirat_recent|default(false) and not _held_aged_activ %} @@ -60,7 +60,7 @@ {% endif %} - {# === US-003 (PRD 5.16): Banda de stare RAR — NUMAI cand BLOCAT (rosu, lat de 100%). + {# === Banda de stare RAR — NUMAI cand BLOCAT (rosu, lat de 100%). OK = dot verde in antet (base.html); banda nu mai apare cand totul e ok. Elementul id="strip-sanatate" ramane in DOM mereu, dar goleste continutul cand OK, astfel "hidden" + fara continut eroare in sursa = nu pica testele de prezenta id-ului. @@ -84,7 +84,7 @@ {% endif %} - {# === US-002 (PRD 5.16): 5 carduri-contor separate (desktop) + bara compacta (mobil <=560px). + {# === 5 carduri-contor separate (desktop) + bara compacta (mobil <=560px). Total / Luna asta / Azi / In coada / De corectat. #} {# Desktop: 5 carduri side-by-side. display:flex + layout stau in CSS (.contoare-desktop in @@ -109,7 +109,7 @@
Azi
- {# In coada — contorul "In asteptare (manual)" se pliaza aici (US-006/D6): nu adaugam a 6-a + {# In coada — contorul "In asteptare (manual)" se pliaza aici: nu adaugam a 6-a celula, ci o sub-linie amber cand exista randuri tinute (queued AND held=1). #}
{{ counts_queued }}
@@ -156,11 +156,11 @@
- {# === US-011 (5.20): Indicator mediu RAR activ per cont. + {# === Indicator mediu RAR activ per cont. La 0 medii: nu afisa (cont fara configuratie RAR). La 1 mediu: eticheta statica (fara toggle). La 2 medii: buton de comutare HTMX (toggle conditionat). - US-010: culori prin eticheta_env/clase .env-badge-* (consistenta cu badge-urile din liste). + Culori prin eticheta_env/clase .env-badge-* (consistenta cu badge-urile din liste). #} {% set _medii = medii_disponibile | default([]) %} {% set _env = env_default | default('prod') %} @@ -194,7 +194,7 @@ {% endif %} {# === Navigatie rapida: Trimiteri + Mapari cu badge needs_mapping === - Pastrata exact ca inainte (US-005): tab_activ determina marcajul activ. + Pastrata exact ca inainte: tab_activ determina marcajul activ. #} {% set _tab = tab_activ | default('acasa') %} - {# US-006 (5.17) + T-6 (5.16): linia de plan in CORP apare DOAR in starea de avertizare + {# Linia de plan in CORP apare DOAR in starea de avertizare (>=80% -> --warn; limita atinsa -> --err). Consumul normal (N/60) traieste in badge-ul din antet + linia din meniul burger, nu ca rand permanent in corp (densitate redusa). Ierarhie: nu concureaza cu stripul de sanatate (zero-silent-failures pastrat). #} diff --git a/app/web/templates/_stepper.html b/app/web/templates/_stepper.html index 45d53f6..df793cf 100644 --- a/app/web/templates/_stepper.html +++ b/app/web/templates/_stepper.html @@ -1,5 +1,5 @@ {# - _stepper.html — Antet wizard import COMPACT (PUR vizual). PRD 5.13. + _stepper.html — Antet wizard import COMPACT (PUR vizual). Parametru: `pas` (integer 1-4). Clasele .stepper-* sunt definite in base.html. >=1024px: bara slim orizontala (.stepper-track). <1024px: forma colapsata "Pasul N din 4 - Titlu" + bara de progres (.stepper-collapsed). diff --git a/app/web/templates/_submissions.html b/app/web/templates/_submissions.html index a46336f..8ade4b6 100644 --- a/app/web/templates/_submissions.html +++ b/app/web/templates/_submissions.html @@ -6,7 +6,7 @@ {# OOB: sincronizeaza filtrul "doar tinute" (#f-held) cu starea randata acum, ca reincarcarile - (trimiteriChanged / paginare) sa-l pastreze in loc sa revina la lista completa (/code-review #2). #} + (trimiteriChanged / paginare) sa-l pastreze in loc sa revina la lista completa. #} {# OOB: banner bulk "Trimite toate (N)" re-randat la fiecare reincarcare a listei (fix staleness). #} @@ -20,7 +20,7 @@ {% if bulk_message %} -{# Sumar actiune bulk (US-010 PRD 5.15): afisat dupa bulk-fix, disparut la urmatoarea reincarcare. #} +{# Sumar actiune bulk: afisat dupa bulk-fix, disparut la urmatoarea reincarcare. #}
- {# Bulk-fix: input cod + buton aplica (US-010 PRD 5.15) #} + {# Bulk-fix: input cod + buton aplica #}
- {# Lista slim trimiteri (US-004, PRD 5.15). + {# Lista slim trimiteri. Inlocuieste tabelul cu randuri compacte: VIN mono + operatie·ora + pill. Nr. inmatriculare, data prestatie si nr. prezentare RAR raman accesibile pe linia meta discreta (linia 3) si in modalul de detaliu. #} @@ -160,14 +160,14 @@ {% endif %}
- {# Zona dreapta: pill stare + badge mediu RAR (US-010 PRD 5.20) + buton Trimite (US-005) #} + {# Zona dreapta: pill stare + badge mediu RAR + buton Trimite #}
{{ r.stare_scurt }} {% if r.rar_env %} {% set _eb = eticheta_env(r.rar_env) %} {{ _eb[0] }} {% endif %} - {# US-005 (PRD 5.19 / D6): buton "Trimite" DOAR pe randurile tinute (queued AND held=1). + {# Buton "Trimite" DOAR pe randurile tinute (queued AND held=1). Afordanta dedicata cu stopPropagation (randul e role=button) — NU declanseaza modalul de detaliu. Confirmare 1-linie prin hx-confirm (per rand); ireversibilitate in microcopy. #} {% if r.tinut %} diff --git a/app/web/templates/_trimitere_detaliu.html b/app/web/templates/_trimitere_detaliu.html index 8f53d7c..618f5c8 100644 --- a/app/web/templates/_trimitere_detaliu.html +++ b/app/web/templates/_trimitere_detaliu.html @@ -7,7 +7,7 @@ {% set cod_afis = prez.cod_rar if (prez.cod_rar and prez.cod_rar != '—') else 'nemapat' %}
- {# === Header — #id + pill stare + badge mediu RAR (US-010 PRD 5.20) + motiv === #} + {# === Header — #id + pill stare + badge mediu RAR + motiv === #}

Detaliu trimitere #{{ id }}

{{ stare_text }} @@ -110,14 +110,13 @@ hx-disabled-elt="find button"> - {# Cleanup B (US-009 PRD 5.15): vechiul eliminat. Chips din _form_editare.html + (via _chips_prestatii.html) il inlocuiesc complet: emit hidden inputs + name="cod_prestatie" + picker per-operatie. post_corectie_trimitere foloseste form.getlist("cod_prestatie") → compatibil. #} {# Operatie service (cod intern + denumire venita prin API/import), distinct de - operatia RAR mapata. op_service_cod="" cand lipseste → randul absent. - RAMANE in _trimitere_detaliu.html (D#5). #} + operatia RAR mapata. op_service_cod="" cand lipseste → randul absent. #} {% if prez.op_service_cod %}
Operatie service
@@ -125,7 +124,7 @@
{% endif %} - {# === Campurile vehicul/data/odo + erori/fix + buton — partial DRY (US-005). === #} + {# === Campurile vehicul/data/odo + erori/fix + buton — partial DRY. === #} {% include "_form_editare.html" %} {% else %} @@ -150,7 +149,7 @@
{% endif %} - {# === Rand tinut manual (queued AND held=1) — afordanta de eliberare (US-005/US-010, 5.19). + {# === Rand tinut manual (queued AND held=1) — afordanta de eliberare. Buton dedicat „Trimite acum": elibereaza randul (held 1->0); worker-ul il preia la urmatorul poll. Reincarca lista + inchide modalul. Confirmare 1-linie (ireversibil). === #} {% if tinut %} diff --git a/app/web/templates/landing.html b/app/web/templates/landing.html index 5044f6d..8cf21f5 100644 --- a/app/web/templates/landing.html +++ b/app/web/templates/landing.html @@ -7,7 +7,7 @@ `.""" i = html.find("SENTINEL-TESTE-MOBIL") - assert i != -1, "Lipseste SENTINEL-TESTE-MOBIL in base.html (vezi PRD 5.13 Wave 0)" + assert i != -1, "Lipseste SENTINEL-TESTE-MOBIL in base.html" j = html.find("", i) return html[i:(j if j != -1 else len(html))] @@ -143,7 +143,7 @@ def test_nav_colapsabil_sub_breakpoint(client): # ============================================================ -# PRD 5.9 US-007: responsive paginile de continut +# Responsive paginile de continut # (Mapari, Cont, Nomenclator, Integrare, Jurnal, Admin) # ============================================================ @@ -173,7 +173,7 @@ def _seed_event(account_id, tip="test", mesaj="eveniment de test"): def test_tabele_continut_au_clasa_responsive(client): - """R12 politica per-tabel: + """Politica per-tabel: - Mapari (actionabil) = CARD per rand: clasa proprie `.tabel-card` + `data-eticheta` pe `
`-uri. - Jurnal / Nomenclator (dense read-only) = `.tablewrap` (scroll orizontal CONTAINED), FARA `.tabel-card`. @@ -231,14 +231,14 @@ def test_formulare_full_width_mobil(client): def test_carduri_trimiteri_5_8_supravietuiesc(client): - """Regresie R12: scoparea `.tabel-card` (US-007) NU trebuie sa atinga blocul - `.tabel-trimiteri @media(max-width:767px)` din 5.8 — cardurile de trimiteri raman.""" + """Regresie: scoparea `.tabel-card` nu trebuie sa atinga blocul + `.tabel-trimiteri @media(max-width:767px)` — cardurile de trimiteri raman.""" _create_account_user("r58@test.com") _login(client, "r58@test.com") html = client.get("/?tab=acasa").text mobil = html[html.find("@media (max-width:767px)"):] - # Cardurile de trimiteri 5.8 (clasa proprie, separata de `.tabel-card`). + # Cardurile de trimiteri (clasa proprie, separata de `.tabel-card`). assert ".tabel-trimiteri thead { display:none; }" in mobil assert ".tabel-trimiteri td::before" in mobil # Cele doua mecanisme coexista, scopate distinct. @@ -246,15 +246,15 @@ def test_carduri_trimiteri_5_8_supravietuiesc(client): # ============================================================ -# PRD 5.9 US-008: responsive Acasa (upload, status, filtre) + login/signup +# Responsive Acasa (upload, status, filtre) + login/signup # ============================================================ def test_acasa_fara_scroll_orizontal_mobil(client): - """US-008: pe Acasa sub 767px zona de upload, bara de status si bara de filtre + """Pe Acasa sub 767px zona de upload, bara de status si bara de filtre stiveaza pe O coloana, cu inputuri/butoane full-width >=44px. Verificam ancorele de scope (id-uri) + regulile CSS mobil din base.html (toate sub `@media (max-width:767px)`), - fara sa atingem cardurile de trimiteri 5.8 (verificate separat).""" + fara sa atingem cardurile de trimiteri (verificate separat).""" acct = _create_account_user("a8@test.com") _insert_submission(acct) # sectiunea Trimiteri (filtre + wrap) apare doar cu randuri _login(client, "a8@test.com") @@ -282,7 +282,7 @@ def test_acasa_fara_scroll_orizontal_mobil(client): def test_login_signup_full_width_mobil(client): - """US-008: login.html si signup.html randeaza un card centrat (`.auth-card`, margin auto) + """login.html si signup.html randeaza un card centrat (`.auth-card`, margin auto) care nu depaseste latimea pe mobil (max-width:100% sub 767px), cu inputuri full-width. Rutele `/login` si `/signup` sunt publice (fara autentificare).""" for ruta in ("/login", "/signup"): @@ -298,12 +298,12 @@ def test_login_signup_full_width_mobil(client): # ============================================================ -# PRD 5.12 US-008: responsive tableta+mobil + header fara suprapuneri +# Responsive tableta+mobil + header fara suprapuneri # ============================================================ def test_header_are_breakpoint_tableta(client): - """US-008 RED: exista reguli @media intre 768 si 1024 pentru header. + """Exista reguli @media intre 768 si 1024 pentru header. Desktop: grid 3-coloane + min-height:92px. Pe tableta (768-1024px) nu exista inca breakpoint — logo+titlu+badge+tema+versiune+hamburger se inghesuie si se suprapun.""" _create_account_user("bt@test.com") @@ -322,7 +322,7 @@ def test_header_are_breakpoint_tableta(client): def test_header_elemente_nu_au_min_height_fix_pe_mobil(client): - """US-008 RED: header-ul nu forteaza min-height:92px pe tableta si mobil. + """Header-ul nu forteaza min-height:92px pe tableta si mobil. Regula de baza (desktop) are min-height:92px; pe tableta (768-1024px) lipseste resetarea -> inghesuire garantata la ~820px.""" _create_account_user("mh@test.com") @@ -345,9 +345,8 @@ def test_header_elemente_nu_au_min_height_fix_pe_mobil(client): def test_modal_full_screen_pe_mobil(client): - """US-008 D#13 verificare: regula full-screen mobil pentru modal exista in base.html - (@media max-width:767px) si se aplica modalului global prin clasa modal-overlay. - VERIFICA prezenta regulii, NU re-implementa.""" + """Regula full-screen mobil pentru modal exista in base.html + (@media max-width:767px) si se aplica modalului global prin clasa modal-overlay.""" _create_account_user("mfp@test.com") _login(client, "mfp@test.com") html = client.get("/?tab=acasa").text @@ -362,18 +361,18 @@ def test_modal_full_screen_pe_mobil(client): # Modalul global din HTML foloseste clasa modal-overlay -> prinde regula CSS. assert 'class="modal-overlay"' in html, \ "Modalul global nu are class=modal-overlay (nu prinde regula full-screen)" - # Target swap pentru editare preview (US-006) exista in DOM. + # Target swap pentru editare preview exista in DOM. assert 'id="detaliu-modal-body"' in html, \ "Target #detaliu-modal-body lipseste din base.html" # ============================================================ -# PRD 5.13: guard-uri responsive card mobil + sistem actiuni + stepper compact +# Guard-uri responsive card mobil + sistem actiuni + stepper compact # ============================================================ def test_card_mobil_fara_break_vertical_120px(client): - """P0: blocul mobil principal NU mai forteaza min-width:120px pe eticheta + """Blocul mobil principal NU mai forteaza min-width:120px pe eticheta (cauza break-ului vertical caracter-cu-caracter). Eticheta stivuita deasupra valorii: td::before cu display:block. Checkbox + # ascunse pe card.""" _create_account_user("card120@test.com") @@ -457,7 +456,7 @@ def test_act_btn_aria_label_invariant(client): def test_stepper_compact_clase(client): - """Stepper compact (5.13): clasele stepper-track, stepper-collapsed, + """Stepper compact: clasele stepper-track, stepper-collapsed, stepper-progress prezente; textul 'Pasul N din 4' randat; vechile clase stepper-pas-- absente; comutarea la <1024px declarata in CSS.""" _create_account_user("stepper@test.com") @@ -482,7 +481,7 @@ def test_stepper_compact_clase(client): def test_liste_actionabile_o_coloana_pana_1024(client): - """Guard scope (decizie 5.13): listele actionabile raman O COLOANA + """Guard scope: listele actionabile raman O COLOANA pana la 1024px — fara grila 2/rand (repeat(2)). Blocul tableta 768-1024px cardifica (thead ascuns, card per rand).""" _create_account_user("ocoloana@test.com") @@ -503,7 +502,7 @@ def test_liste_actionabile_o_coloana_pana_1024(client): # ============================================================ -# PRD 5.15 US-002: componente de design slim (CSS, fara consumatori) +# Componente de design slim (CSS, fara consumatori) # ============================================================ @@ -513,9 +512,9 @@ def _bloc_componente_slim(html: str) -> str: """ sentinel = "SENTINEL-COMPONENTE-SLIM" i = html.find(sentinel) - assert i != -1, "Lipseste SENTINEL-COMPONENTE-SLIM in base.html (US-002 PRD 5.15)" + assert i != -1, "Lipseste SENTINEL-COMPONENTE-SLIM in base.html" i2 = html.find(sentinel, i + len(sentinel)) - assert i2 != -1, "Lipseste al doilea SENTINEL-COMPONENTE-SLIM (sfarsit bloc US-002)" + assert i2 != -1, "Lipseste al doilea SENTINEL-COMPONENTE-SLIM (sfarsit bloc)" return html[i : i2 + len(sentinel)] @@ -618,19 +617,19 @@ def test_fara_hex_in_componente_noi(client): bloc, ) assert not hex_in_props, ( - f"Hex literal gasit in componente noi US-002 — folositi var(--token): {hex_in_props}" + f"Hex literal gasit in componente noi — folositi var(--token): {hex_in_props}" ) # ============================================================ -# PRD 5.15 US-004: lista slim trimiteri — layout consistent desktop + <=1024px +# Lista slim trimiteri — layout consistent desktop + <=1024px # ============================================================ def test_lista_slim_randeaza_si_are_tinta_touch(client): - """US-004: lista slim randeaza cu .trimitere-slim; tinta touch >=44px + """Lista slim randeaza cu .trimitere-slim; tinta touch >=44px e garantata de CSS (min-height:44px din blocul SENTINEL-COMPONENTE-SLIM). - Cardurile .tabel-trimiteri din 5.8 nu regreseaza: regula tabel-trimiteri + Cardurile .tabel-trimiteri nu regreseaza: regula tabel-trimiteri thead display:none (card pe mobil) exista in continuare in base.html. """ acct = _create_account_user("slim_resp@test.com") @@ -643,7 +642,7 @@ def test_lista_slim_randeaza_si_are_tinta_touch(client): # -> testam ca clasele CSS sunt prezente in base.html, gata sa fie consumate) bloc = _bloc_componente_slim(html) assert "lista-trimiteri-slim" in bloc, \ - ".lista-trimiteri-slim lipseste din blocul CSS slim (US-002 prerequisite)" + ".lista-trimiteri-slim lipseste din blocul CSS slim (prerechizita)" assert "trimitere-slim" in bloc, \ ".trimitere-slim lipseste din blocul CSS slim" assert "min-height:44px" in bloc, \ @@ -656,7 +655,7 @@ def test_lista_slim_randeaza_si_are_tinta_touch(client): def test_lista_slim_layout_tableta_1024(client): - """US-004: blocul tableta (768-1024px) nu rupe lista slim. + """Blocul tableta (768-1024px) nu rupe lista slim. .trimitere-slim e o lista stivuita (o coloana), fara grila 2/rand. Regula tableta cardifica listele existente (thead display:none) fara a elimina slim. """ @@ -664,7 +663,7 @@ def test_lista_slim_layout_tableta_1024(client): _login(client, "slim_tab@test.com") html = client.get("/?tab=acasa").text - # Blocul tableta exista (PRD 5.12/5.13 — pastrat) + # Blocul tableta exista (pastrat) assert "@media (min-width:768px) and (max-width:1024px)" in html, \ "Lipseste blocul @media tableta — regresia 5.12" @@ -682,12 +681,12 @@ def test_lista_slim_layout_tableta_1024(client): # ============================================================ -# PRD 5.15 US-008: regresie componente slim + fara overflow orizontal +# Regresie componente slim + fara overflow orizontal # ============================================================ def test_slim_list_fara_overflow_orizontal_css(client): - """US-008: lista slim (.trimitere-slim) nu forteaza overflow orizontal pe 390px / 1280px. + """Lista slim (.trimitere-slim) nu forteaza overflow orizontal pe 390px / 1280px. Verifica la nivel CSS / markup (nu browser): display:flex garanteaza adaptarea naturala la latimea containerului; niciun min-width fix mai mare de 390px pe elementele @@ -716,7 +715,7 @@ def test_slim_list_fara_overflow_orizontal_css(client): def test_strip_sanatate_fara_hex_hardcodat(): - """US-008: _status.html (strip sanatate D6 + contoare-contor) nu contine hex literal de culoare. + """_status.html (strip sanatate + contoare-contor) nu contine hex literal de culoare. Garanteaza ca strip-ul adapteaza la temele luminoase (hartie/light) si intunecate (grafit/dark) exclusiv prin var(--token), NU prin valori hex hardcodate care ar ramane aceleasi @@ -741,39 +740,38 @@ def test_strip_sanatate_fara_hex_hardcodat(): # ============================================================ -# PRD 5.16 US-010: Titlu ROA AUTOPASS + account_name in antet +# Titlu ROA AUTOPASS + account_name in antet # ============================================================ def test_titlu_romfast_autopass(client): - """US-010 (PRD 5.16): titlul din antet si tag-ul sunt 'ROA AUTOPASS', + """Titlul din antet si tag-ul <title> sunt 'ROA AUTOPASS', nu 'Gateway RAR AUTOPASS'.""" _create_account_user("titlutest@test.com", name="Service Titlu") _login(client, "titlutest@test.com") html = client.get("/?tab=acasa").text assert "ROA AUTOPASS" in html, \ - "Titlul 'ROA AUTOPASS' lipseste din antet (US-010 PRD 5.16)" + "Titlul 'ROA AUTOPASS' lipseste din antet" assert "Gateway RAR AUTOPASS" not in html, \ "Titlul vechi 'Gateway RAR AUTOPASS' inca prezent — inlocuieste cu 'ROA AUTOPASS'" def test_header_arata_nume_service_logat(client): - """US-010 (PRD 5.16): cand utilizatorul e logat, antetul afiseaza numele service-ului + """Cand utilizatorul e logat, antetul afiseaza numele service-ului (accounts.name) ca sub-titlu cu clasa .h-sub.""" _create_account_user("numeservice@test.com", name="Service Auto Cluj SRL") _login(client, "numeservice@test.com") html = client.get("/?tab=acasa").text assert "Service Auto Cluj SRL" in html, \ - "Numele service-ului nu apare in antet (US-010 PRD 5.16) — verifica .h-sub" + "Numele service-ului nu apare in antet — verifica .h-sub" assert "h-sub" in html, \ - "Clasa .h-sub lipseste din antet (US-010 PRD 5.16) — sub-titlul account_name lipseste" + "Clasa .h-sub lipseste din antet — sub-titlul account_name lipseste" def test_login_branded_nu_schelet(client): """/login e branduit (card .login-shell, titlu 'ROA AUTOPASS') cu formular POST /login - (CSRF intact), NU schelet gol. Loginul a fost simplificat la o coloana (commit - 'simplifica login') — nu mai cerem coloana de brand .login-aside.""" + (CSRF intact), NU schelet gol. Login simplificat la o coloana, fara .login-aside.""" resp = client.get("/login") assert resp.status_code == 200 html = resp.text @@ -789,8 +787,7 @@ def test_login_branded_nu_schelet(client): # ============================================================ -# PRD 5.17 T7 (US-007): landing copy — limita 60 + trial Pro -# PRD 5.16 US-012: Autentificare → /login +# Landing copy — limita plan Gratuit + trial Pro + link autentificare # ============================================================ def _citeste_landing() -> str: @@ -803,9 +800,9 @@ def _citeste_landing() -> str: def test_landing_limita_60(): - """5.17 T7 (US-007): limita planului Gratuit este 60/luna in landing, nu 100. - Terminologia landing-ului a trecut de la 'prestații' la 'trimiteri' (rework landing - ff9d0f4): copy-ul de limita e acum '60 de trimiteri/lună' (hero + cardul Gratuit).""" + """Limita planului Gratuit este 60/luna in landing, nu 100. + Terminologia landing-ului e 'trimiteri' (nu 'prestații'): copy-ul de limita + e '60 de trimiteri/lună' (hero + cardul Gratuit).""" html = _citeste_landing() assert "100 de prestații" not in html, \ @@ -817,7 +814,7 @@ def test_landing_limita_60(): def test_landing_trial_pro_nu_premium(): - """5.17 T7 (US-007): trial-ul de 30 de zile este pe Pro, NU pe Premium. + """Trial-ul de 30 de zile este pe Pro, NU pe Premium. Verifica sectiunea PRICING (subtitle) si sectiunea AUTH (lista beneficii).""" html = _citeste_landing() @@ -828,28 +825,28 @@ def test_landing_trial_pro_nu_premium(): def test_landing_autentificare_link_login(): - """5.16 US-012: butonul 'Autentificare' din header-ul landing este un link <a href='/login'> + """Butonul 'Autentificare' din header-ul landing este un link <a href='/login'> cu clasa auth-login-link, NU un buton care deschide modalul de login. - CSS-ul responsive (.lp-hactions) trebuie sa foloseasca noul selector, nu cel vechi.""" + CSS-ul responsive (.lp-hactions) foloseste acest selector.""" html = _citeste_landing() # Link real catre /login in header (cu clasa de identificare) assert 'href="/login"' in html, \ - "href='/login' lipseste din landing — 'Autentificare' din header trebuie sa fie link (5.16 US-012)" + "href='/login' lipseste din landing — 'Autentificare' din header trebuie sa fie link" assert "auth-login-link" in html, \ - "Clasa auth-login-link lipseste — header 'Autentificare' nu a fost convertit la <a> (5.16 US-012)" + "Clasa auth-login-link lipseste — header 'Autentificare' nu a fost convertit la <a>" # CSS-ul responsive ascunde linkul pe <430px prin noul selector (nu cel vechi cu atribute) assert "a.auth-login-link" in html, \ - "Selectorul CSS 'a.auth-login-link' lipseste — CSS responsive neactualizat (5.16 US-012)" + "Selectorul CSS 'a.auth-login-link' lipseste — CSS responsive neactualizat" # Selectorul CSS vechi cu [data-act="auth"][data-tab="login"] nu mai exista in CSS assert '[data-act="auth"][data-tab="login"]' not in html, \ - "Selectorul CSS vechi [data-act='auth'][data-tab='login'] inca prezent (5.16 US-012)" + "Selectorul CSS vechi [data-act='auth'][data-tab='login'] inca prezent" def test_contoare_desktop_ascunse_pe_mobil_fara_inline_display(): - """US-002 (PRD 5.16): pe <=560px se vad DOAR contoarele compacte, nu si cele 5 carduri mari. + """Pe <=560px se vad DOAR contoarele compacte, nu si cele 5 carduri mari. - Regresie prinsa la VERIFY E2E (390px): un inline `style="display:flex"` pe `.contoare-desktop` + Regresie: un inline `style="display:flex"` pe `.contoare-desktop` batea regula `@media (max-width:560px) { .contoare-desktop { display:none } }` (inline > stylesheet) -> contoare DUPLICATE pe mobil. Lock: `display:flex` sta in CSS (nu inline pe element), iar media query-ul ascunde cardurile mari pe mobil. diff --git a/tests/test_web_scope.py b/tests/test_web_scope.py index 59d5d32..9ac4c10 100644 --- a/tests/test_web_scope.py +++ b/tests/test_web_scope.py @@ -1,4 +1,4 @@ -"""US-011 (PRD 5.15): account-scope pe GET-urile de listare web (securitate). +"""Teste pentru account-scope pe GET-urile de listare web (securitate). Verifica: - /_fragments/submissions: un cont nu vede randurile altui cont @@ -6,10 +6,6 @@ Verifica: - /_fragments/nomenclator: necesita autentificare (fragment dashboard, nu endpoint public) - /_fragments/trimiteri-versiune: necesita autentificare - Unauthenticated access -> redirect 303 pe ORICE fragment cu date - -Legatura cu implementare: mecanismul de scope existent (require_login + -account_scope_clause) se reutilizeaza fara logica noua. Nomenclatorul primeste -require_login din US-011 (fragment dashboard, nu endpoint public). """ from __future__ import annotations @@ -104,24 +100,21 @@ def test_get_listare_scoped_cont(client): """ NR_A = "BV01SCO" NR_B = "BV02SCO" - VIN_A = "WVWZZZ1JZXW111AAA" # vin_scurt va fi '...111AAA' - VIN_B = "WVWZZZ1JZXW222BBB" # vin_scurt va fi '...222BBB' + VIN_A = "WVWZZZ1JZXW111AAA" + VIN_B = "WVWZZZ1JZXW222BBB" acct_a = _create_account_user("scope-a@test.com", name="ContA") acct_b = _create_account_user("scope-b@test.com", name="ContB") _insert_submission(acct_a, vin=VIN_A, nr=NR_A) _insert_submission(acct_b, vin=VIN_B, nr=NR_B) - # Login ca cont B _login(client, "scope-b@test.com") resp = client.get("/_fragments/submissions") assert resp.status_code == 200 html = resp.text - # Contul B vede propriul nr inmatriculare assert NR_B in html, "Contul B ar trebui sa vada propriul nr inmatriculare" - # Contul B NU vede nr inmatriculare si VIN (vin_scurt) ale contului A assert NR_A not in html, "Scurgere cross-account: nr_inmatriculare contului A vizibil contului B" assert "111AAA" not in html, "Scurgere cross-account: VIN (vin_scurt) contului A vizibil contului B" @@ -152,31 +145,26 @@ def test_get_detaliu_scoped_404(client): _create_account_user("detscope-b@test.com", name="DetB") sid_a = _insert_submission(acct_a, vin="WVWZZZ1JZXWAAA111") - # Login ca cont B _login(client, "detscope-b@test.com") - # Cerere detaliu pe submission-ul contului A resp = client.get(f"/_fragments/trimitere/{sid_a}") assert resp.status_code == 404, ( f"Asteptat 404, primit {resp.status_code}. " "Nu trebuie confirmata existenta unui submission al altui cont." ) - # Id inexistent -> acelasi 404 (nu confirmam existenta) resp2 = client.get("/_fragments/trimitere/999999") assert resp2.status_code == 404 # --------------------------------------------------------------------------- -# Test 4: nomenclator necesita autentificare (RED inainte de fix) +# Test 4: nomenclator necesita autentificare # --------------------------------------------------------------------------- def test_get_nomenclator_neautentificat_redirect(client): """/_fragments/nomenclator este un fragment al dashboard-ului autentificat. Fara sesiune, trebuie sa returneze 303 redirect la /login. - RED inainte de fix: in prezent fragmentul nu apeleaza require_login - si returneaza 200 chiar fara autentificare. """ resp = client.get("/_fragments/nomenclator") assert resp.status_code == 303, ( diff --git a/tests/test_web_selector_tema.py b/tests/test_web_selector_tema.py index 4d49360..20f110d 100644 --- a/tests/test_web_selector_tema.py +++ b/tests/test_web_selector_tema.py @@ -1,6 +1,4 @@ -"""Teste US-014 (PRD 5.10): Selector de tema ciclic Light/Dark/Petrol/Auto. - -TDD: testele se scriu INAINTE de implementare (RED), dupa implementare trec (GREEN). +"""Teste selector de tema ciclic Light/Dark/Petrol/Auto. Testeaza: - test_petrol_theme_definit: [data-theme="petrol"] definit cu valorile din DESIGN.md @@ -162,18 +160,18 @@ def test_buton_cicleaza_temele(client): "Necesara pentru a anunta schimbarea temei catre screen-readers." ) - # 6. US-014b (decizie user): title-ul vizual al butonului = DOAR numele temei, NU ciclul. - # Verificam in sursa JS ca variabila TOOLTIP_CICLU (sirul cu "Ciclu: Light → Dark → ...") nu - # mai e folosita in title. aria-label-ul informativ (curenta+urmatoarea) RAMANE neschimbat. + # 6. Title-ul vizual al butonului = DOAR numele temei, NU ciclul. + # Verificam in sursa JS ca variabila TOOLTIP_CICLU (sirul cu "Ciclu: Light -> Dark -> ...") + # nu mai e folosita in title. aria-label-ul informativ (curenta+urmatoarea) RAMANE neschimbat. assert 'TOOLTIP_CICLU' not in html, ( "Variabila TOOLTIP_CICLU inca prezenta in JS — title-ul trebuie simplificat. " - "US-014b: btn.title trebuie sa fie DOAR LABELS[s] (ex. 'Light'), " + "btn.title trebuie sa fie DOAR LABELS[s] (ex. 'Light'), " "nu 'Tema: X. Ciclu: Light -> Dark -> Petrol -> Auto'. " "aria-label-ul informativ RAMANE neschimbat." ) # Verifica ca JS-ul seteaza btn.title la doar LABELS[s] (fara prefix 'Tema:' sau ciclul) assert re.search(r"btn\.title\s*=\s*LABELS\[", html), ( "btn.title nu e setat la LABELS[s] in JS. " - "US-014b: formatul asteptat: btn.title = LABELS[s] (ex. rezultat: 'Petrol'). " + "formatul asteptat: btn.title = LABELS[s] (ex. rezultat: 'Petrol'). " "Aria-label-ul informativ ramane separat: 'Tema: X, apasa pentru Y'." ) diff --git a/tests/test_web_session.py b/tests/test_web_session.py index ba79f76..462ce6a 100644 --- a/tests/test_web_session.py +++ b/tests/test_web_session.py @@ -1,4 +1,4 @@ -"""Teste US-002 (PRD 3.3): SessionMiddleware + helper-e sesiune in app/web/session.py.""" +"""Teste pentru SessionMiddleware + helper-e sesiune in app/web/session.py.""" from __future__ import annotations @@ -111,10 +111,8 @@ def test_sesiune_seteaza_si_citeste_cont(client_auth): def test_logout_curata_sesiunea(client_auth): """clear_session sterge account_id si user_id din sesiune.""" client_auth.get("/set-session?account_id=3&user_id=4") - # Verifica ca sesiunea e setata data_before = client_auth.get("/get-session").json() assert data_before["account_id"] == 3 - # Logout client_auth.get("/logout") data_after = client_auth.get("/get-session").json() assert data_after["account_id"] is None @@ -129,12 +127,10 @@ def test_dev_bypass_cont_1(client_dev, monkeypatch): def test_set_session_curata_sesiunea_anterioara(client_auth): - """set_session face clear() inainte de a seta (C3 anti-fixare sesiune).""" - # Seteaza sesiune initiala cu cont 10 + """set_session face clear() inainte de a seta (anti-fixare sesiune).""" client_auth.get("/set-session?account_id=10&user_id=10") data_initial = client_auth.get("/get-session").json() assert data_initial["account_id"] == 10 - # Re-login cu cont nou 20 — sesiunea veche trebuie stearsa inainte client_auth.get("/set-session?account_id=20&user_id=20") data_nou = client_auth.get("/get-session").json() assert data_nou["account_id"] == 20 diff --git a/tests/test_web_signup.py b/tests/test_web_signup.py index 5b90606..ba77f1c 100644 --- a/tests/test_web_signup.py +++ b/tests/test_web_signup.py @@ -1,8 +1,4 @@ -"""Teste US-003 (PRD 3.3): GET/POST /signup. - -TDD: testele se scriu INAINTE de implementarea auth_routes.py; la inceput pica (RED), -dupa implementare trec (GREEN). -""" +"""Teste GET/POST /signup.""" from __future__ import annotations @@ -145,8 +141,8 @@ def test_signup_parola_scurta_eroare(client): def test_signup_fara_consent_eroare(client): """Consimtamant GDPR lipsa -> 422, fara creare cont; mesaj despre Termeni/GDPR. - Checkbox-ul de consimtamant trebuie validat server-side (functional, nu doar client-side): - fara el contul nu se creeaza si planul/datele introduse se pastreaza in re-render. + Checkbox-ul de consimtamant e validat server-side: fara el contul nu se creeaza + si planul/datele introduse se pastreaza in re-render. """ from tests.conftest import make_test_cui resp = client.get("/signup") diff --git a/tests/test_web_status.py b/tests/test_web_status.py index f2d6307..ab09f35 100644 --- a/tests/test_web_status.py +++ b/tests/test_web_status.py @@ -1,12 +1,9 @@ -"""Teste US-003 (PRD 5.15): strip sanatate mereu-vizibil + carduri-contor pe dashboard. +"""Teste strip sanatate mereu-vizibil + carduri-contor pe dashboard. -D6 (strip sanatate): linie colorata DEASUPRA contoarelor — verde "declaratiile curg" / +Strip sanatate: linie colorata DEASUPRA contoarelor — verde "declaratiile curg" / rosu "Blocat: worker oprit / RAR inaccesibil", cu glifa accesibila (✓/✗). -D4 (contoare): In coada / Trimise (all-time + luna/azi) / De corectat. -E7 (timezone): azi/luna bucketate in timp local RO (UTC+3), nu UTC. - -Actualizat in US-003 (PRD 5.15): bara veche cu bife individuale worker/RAR -inlocuita de strip unificat de sanatate + carduri-contor. +Contoare: In coada / Trimise (all-time + luna/azi) / De corectat. +Timezone: azi/luna bucketate in timp local RO, nu UTC. """ from __future__ import annotations @@ -77,11 +74,11 @@ def client(monkeypatch): # --------------------------------------------------------------------------- -# Teste existente — actualizate pentru US-003 D6 (strip unificat in loc de bife individuale) +# Strip unificat de sanatate (in loc de bife individuale) # --------------------------------------------------------------------------- def test_status_are_bife_verzi_cand_totul_ok(client): - """US-003 PRD 5.16: worker viu + RAR login recent -> strip-sanatate in DOM dar ASCUNS (hidden). + """Worker viu + RAR login recent -> strip-sanatate in DOM dar ASCUNS (hidden). Banda rosie apare DOAR cand BLOCAT. Starea OK e indicata de dot-ul verde din antet (base.html). Elementul id=strip-sanatate ramane in DOM pentru compatibilitate (nu dispare complet). """ @@ -94,7 +91,7 @@ def test_status_are_bife_verzi_cand_totul_ok(client): resp = client.get("/_fragments/status") assert resp.status_code == 200 html = resp.text - # US-003: elementul strip-sanatate e prezent in DOM dar ascuns cand totul e ok + # Elementul strip-sanatate e prezent in DOM dar ascuns cand totul e ok assert 'id="strip-sanatate"' in html, f"id=strip-sanatate lipseste complet din fragment. HTML: {html[:600]}" # Cand OK, banda nu trebuie sa afiseze ✗ (eroare) — ✓ nu mai apare (banda e ascunsa) assert "✗" not in html, \ @@ -112,7 +109,7 @@ def test_status_are_bife_rosii_cand_worker_oprit(client): assert resp.status_code == 200 html = resp.text assert "✗" in html, f"Lipseste glifa ✗ cand worker oprit. HTML: {html[:600]}" - # US-003 D6: mesaj explicit (nu text vag "oprita") + # Mesaj explicit (nu text vag "oprita") assert "blocat" in html.lower(), f"Cuvantul 'blocat' lipseste la worker oprit. HTML: {html[:600]}" assert "nu pleaca" in html.lower(), f"Avertismentul 'nu pleaca' lipseste. HTML: {html[:600]}" @@ -133,7 +130,7 @@ def test_status_data_formatata_romaneste(client): def test_status_fara_fonturi_minuscule(client): - """Niciun text din bara nu mai foloseste font-size literal sub 13px (US-001 AC).""" + """Niciun text din bara nu mai foloseste font-size literal sub 13px.""" _create_account_user("bifefont@test.com") _login(client, "bifefont@test.com", "parolasecreta10") @@ -146,11 +143,11 @@ def test_status_fara_fonturi_minuscule(client): # --------------------------------------------------------------------------- -# Teste NOI pentru US-003 (RED inainte de implementare) +# Strip de sanatate mereu vizibil # --------------------------------------------------------------------------- def test_strip_sanatate_mereu_vizibil(client): - """D6: strip de sanatate e prezent in fragment, indiferent de starea worker/RAR.""" + """Strip de sanatate e prezent in fragment, indiferent de starea worker/RAR.""" _create_account_user("stripviz@test.com") _login(client, "stripviz@test.com", "parolasecreta10") @@ -167,7 +164,7 @@ def test_strip_sanatate_mereu_vizibil(client): def test_strip_rosu_worker_oprit(client): - """D6: worker oprit → strip rosu cu glifа ✗ + text 'Blocat: worker oprit — declaratiile NU pleaca'.""" + """Worker oprit → strip rosu cu glifа ✗ + text 'Blocat: worker oprit — declaratiile NU pleaca'.""" _create_account_user("stroprosu@test.com") _login(client, "stroprosu@test.com", "parolasecreta10") @@ -185,7 +182,7 @@ def test_strip_rosu_worker_oprit(client): def test_trei_contoare_card(client): - """US-002 PRD 5.16: fragment status contine 5 carduri .contor-card separate: + """Fragment status contine 5 carduri .contor-card separate: Total / Luna asta / Azi / In coada / De corectat.""" _create_account_user("treicont@test.com") _login(client, "treicont@test.com", "parolasecreta10") @@ -196,19 +193,19 @@ def test_trei_contoare_card(client): count = html.count("contor-card") assert count >= 5, ( - f"Trebuie minim 5 elemente contor-card (US-002 PRD 5.16: Total/Luna/Azi/Coada/Corectat), " + f"Trebuie minim 5 elemente contor-card (Total/Luna/Azi/Coada/Corectat), " f"gasit: {count}. HTML: {html[:800]}" ) - # Etichete asteptate (US-002 PRD 5.16: 5 carduri separate) - assert "Total" in html, "Eticheta 'Total' lipseste din contoare (US-002 PRD 5.16)." - assert "Luna asta" in html, "Eticheta 'Luna asta' lipseste din contoare (US-002 PRD 5.16)." - assert "Azi" in html, "Eticheta 'Azi' lipseste din contoare (US-002 PRD 5.16)." + # Etichete asteptate (5 carduri separate) + assert "Total" in html, "Eticheta 'Total' lipseste din contoare." + assert "Luna asta" in html, "Eticheta 'Luna asta' lipseste din contoare." + assert "Azi" in html, "Eticheta 'Azi' lipseste din contoare." assert "In coada" in html, "Eticheta 'In coada' lipseste din contoare." assert "De corectat" in html, "Eticheta 'De corectat' lipseste din contoare." def test_trimise_all_time_luna_azi(client): - """D4: cardul Trimise afiseaza all-time ca cifra principala + sub-linie 'luna N · azi N'.""" + """Cardul Trimise afiseaza all-time ca cifra principala + sub-linie 'luna N · azi N'.""" acct_id, _ = _create_account_user("trimisetime@test.com") _login(client, "trimisetime@test.com", "parolasecreta10") @@ -239,7 +236,7 @@ def test_trimise_all_time_luna_azi(client): def test_fara_bara_veche(client): - """US-003: contoarele vechi inline ('In asteptare:' / 'Declarate la RAR:') nu mai apar.""" + """Contoarele vechi inline ('In asteptare:' / 'Declarate la RAR:') nu mai apar.""" _create_account_user("faraveche@test.com") _login(client, "faraveche@test.com", "parolasecreta10") @@ -247,7 +244,7 @@ def test_fara_bara_veche(client): assert resp.status_code == 200 html = resp.text - # Stilul vechi: etichete inline cu colon (bara de la PRD 3.5) + # Stilul vechi: etichete inline cu colon assert "In asteptare:" not in html, ( f"Contorul vechi 'In asteptare:' inca prezent. HTML: {html[:600]}" ) @@ -257,7 +254,7 @@ def test_fara_bara_veche(client): def test_banda_apare_doar_cand_blocat(client): - """US-003 (PRD 5.16): banda rosie completa apare NUMAI cand BLOCAT. + """Banda rosie completa apare NUMAI cand BLOCAT. Cand totul e ok, strip-sanatate are atributul 'hidden' (ascuns, nu disparut). Cand worker e oprit, strip-sanatate NU are 'hidden' (e vizibil, rosu). """ @@ -284,7 +281,7 @@ def test_banda_apare_doar_cand_blocat(client): def test_rar_dot_in_antet_ok(client): - """US-003 (PRD 5.16): cand logat si sanatate_ok, antetul contine chip-ul RAR cu clasa rar-ok. + """Cand logat si sanatate_ok, antetul contine chip-ul RAR cu clasa rar-ok. Starea ok se vede din header (dot verde pulsant), nu din banda de stare (care e ascunsa). """ _create_account_user("rardot@test.com") @@ -297,13 +294,13 @@ def test_rar_dot_in_antet_ok(client): assert resp.status_code == 200 html = resp.text # Chip RAR in antet (nu in banda de stare) - assert "rar-chip" in html, "Clasa rar-chip lipseste din HTML (dot RAR in antet, US-003)" - assert "rar-ok" in html, "Clasa rar-ok lipseste — dot verde cand sanatate ok (US-003)" - assert "rar-dot" in html, "Clasa rar-dot lipseste din chip (US-003)" + assert "rar-chip" in html, "Clasa rar-chip lipseste din HTML (dot RAR in antet)" + assert "rar-ok" in html, "Clasa rar-ok lipseste — dot verde cand sanatate ok" + assert "rar-dot" in html, "Clasa rar-dot lipseste din chip" def test_rar_in_meniu_burger(client): - """US-003/010 (PRD 5.16): meniul burger contine starea RAR ca prima intrare (RAR online / RAR indisponibil).""" + """Meniul burger contine starea RAR ca prima intrare (RAR online / RAR indisponibil).""" _create_account_user("rarmeniu@test.com") _login(client, "rarmeniu@test.com", "parolasecreta10") @@ -316,13 +313,13 @@ def test_rar_in_meniu_burger(client): # Meniul burger (cont-menu) contine indicatorul RAR assert "cont-menu" in html, "Meniu burger (cont-menu) lipseste din HTML" assert "RAR online" in html or "RAR indisponibil" in html, \ - "Starea RAR nu apare in meniu burger (US-003/010)" + "Starea RAR nu apare in meniu burger" # Prima intrare e starea RAR — prezenta class menu-rar-line - assert "menu-rar-line" in html, "Clasa menu-rar-line lipseste din burger (US-003)" + assert "menu-rar-line" in html, "Clasa menu-rar-line lipseste din burger" def test_anuleaza_are_data_modal_close(client): - """US-007 (PRD 5.16): overlay-ul modal si butonul de inchidere au atributul data-modal-close.""" + """Overlay-ul modal si butonul de inchidere au atributul data-modal-close.""" # Butonul si overlay-ul trebuie sa aiba data-modal-close pentru ca handler-ul cu .closest() sa functioneze # Verificam in baza template-ului base.html (modal e definit acolo, randat pe toate paginile) # Testam pe dashboard dupa login (unde baza e incarcata) @@ -332,12 +329,12 @@ def test_anuleaza_are_data_modal_close(client): assert resp.status_code == 200 html = resp.text assert "data-modal-close" in html, \ - "data-modal-close lipseste din template — modalul nu se poate inchide (US-007)" + "data-modal-close lipseste din template — modalul nu se poate inchide" def test_modal_close_pe_element_interior(client): - """US-007 (PRD 5.16): handler-ul modal foloseste .closest('[data-modal-close]') nu - .hasAttribute directe — astfel click pe un element interior al backdrop-ului functioneaza.""" + """Handler-ul modal foloseste .closest('[data-modal-close]') nu .hasAttribute + direct — astfel click pe un element interior al backdrop-ului functioneaza.""" _create_account_user("modalclosest@test.com") _login(client, "modalclosest@test.com", "parolasecreta10") resp = client.get("/", follow_redirects=True) @@ -345,7 +342,7 @@ def test_modal_close_pe_element_interior(client): html = resp.text # Verificam ca JS-ul foloseste closest, nu hasAttribute assert "closest('[data-modal-close]')" in html, \ - "Handler-ul modal foloseste hasAttribute in loc de closest (US-007) — click pe copil nu va inchide modalul" + "Handler-ul modal foloseste hasAttribute in loc de closest — click pe copil nu va inchide modalul" def _set_tz_bucuresti(monkeypatch, request): @@ -360,7 +357,7 @@ def _set_tz_bucuresti(monkeypatch, request): def test_granita_miez_noapte_local_ro(monkeypatch, request): - """E7: trimitere cu updated_at = ieri UTC 22:00 = azi Romania (UTC+2/+3) se numara 'azi'. + """Trimitere cu updated_at = ieri UTC 22:00 = azi Romania (UTC+2/+3) se numara 'azi'. Cu date(updated_at) simplu (UTC) ar aparea pe ziua precedenta — GRESIT. Cu date(updated_at, 'localtime') + TZ=Europe/Bucharest apare pe ziua de azi RO — CORECT @@ -418,8 +415,8 @@ def test_granita_miez_noapte_local_ro(monkeypatch, request): def test_iarna_nu_bleed_in_ziua_urmatoare(monkeypatch, request): - """Bug fix (code-review 5.15): iarna (UTC+2), o trimitere la 21:30 UTC = 23:30 RO AZI - NU trebuie sa cada pe ziua de MAINE. + """Iarna (UTC+2), o trimitere la 21:30 UTC = 23:30 RO AZI NU trebuie sa cada + pe ziua de MAINE. Vechiul offset fix '+3 hours' o impingea la 00:30 maine -> sent_today gresit. 'localtime' (DST-aware) o pastreaza corect pe azi. Testul fixeaza o data de iarna @@ -467,7 +464,7 @@ def test_iarna_nu_bleed_in_ziua_urmatoare(monkeypatch, request): # =========================================================================== -# US-006 (PRD 5.17) — Afisaj plan curent: trial / consum / warn / banner +# Afisaj plan curent: trial / consum / warn / banner # =========================================================================== def _set_trial_until(account_id: int, trial_until_str: str | None) -> None: @@ -502,9 +499,9 @@ def _insert_submissions_sent(account_id: int, n: int) -> None: def test_afisaj_plan_si_zile_trial(client): - """US-006 + T-6 (5.16): cont in trial Pro -> linia de plan din meniul burger (pagina - completa) arata 'Plan: Pro · trial N zile ramase'. In starea normala (non-warn) plan_linie - NU mai e rand in corpul fragmentului status — traieste in badge antet + burger. + """Cont in trial Pro -> linia de plan din meniul burger (pagina completa) arata + 'Plan: Pro · trial N zile ramase'. In starea normala (non-warn) plan_linie NU mai + e rand in corpul fragmentului status — traieste in badge antet + burger. Contul nou primeste trial_until=now+30z automat la creare. """ acct_id, _ = _create_account_user("trialzile@test.com") @@ -525,8 +522,8 @@ def test_afisaj_plan_si_zile_trial(client): def test_afisaj_consum_lunar(client): - """US-006 + T-6 (5.16): cont free (fara trial) -> linia de plan din burger (pagina - completa) arata 'Gratuit · N/60 luna asta'. Consumul normal nu mai e rand in corp.""" + """Cont free (fara trial) -> linia de plan din burger (pagina completa) arata + 'Gratuit · N/60 luna asta'. Consumul normal nu mai e rand in corp.""" acct_id, _ = _create_account_user("consumlun@test.com") _login(client, "consumlun@test.com", "parolasecreta10") @@ -546,7 +543,7 @@ def test_afisaj_consum_lunar(client): def test_avertizare_aproape_de_limita(client): - """US-006: >=80% din 60 -> avertizare cu text 'aproape de limita' + culoare warn.""" + """>=80% din 60 -> avertizare cu text 'aproape de limita' + culoare warn.""" acct_id, _ = _create_account_user("aproapelim@test.com") _login(client, "aproapelim@test.com", "parolasecreta10") @@ -569,7 +566,7 @@ def test_avertizare_aproape_de_limita(client): def test_limita_atinsa(client): - """US-006: 60/60 -> text 'limita atinsa'.""" + """60/60 -> text 'limita atinsa'.""" acct_id, _ = _create_account_user("limitaatinsa@test.com") _login(client, "limitaatinsa@test.com", "parolasecreta10") @@ -586,7 +583,7 @@ def test_limita_atinsa(client): def test_copy_pluralizare_zi_zile(client): - """US-006: pluralizare RO corecta — 1 zi (nu '1 zile'), 18 zile (nu '18 zi').""" + """Pluralizare RO corecta — 1 zi (nu '1 zile'), 18 zile (nu '18 zi').""" acct_id, _ = _create_account_user("pluralzile@test.com") _login(client, "pluralzile@test.com", "parolasecreta10") @@ -594,7 +591,7 @@ def test_copy_pluralizare_zi_zile(client): future_18 = (datetime.now(timezone.utc) + timedelta(days=18, hours=12)).strftime("%Y-%m-%d %H:%M:%S") _set_trial_until(acct_id, future_18) - # T-6 (5.16): linia de plan (cu pluralizarea zilelor) traieste in burger pe pagina completa. + # Linia de plan (cu pluralizarea zilelor) traieste in burger pe pagina completa. resp = client.get("/", follow_redirects=True) assert resp.status_code == 200 html = resp.text @@ -617,7 +614,7 @@ def test_copy_pluralizare_zi_zile(client): def test_banner_one_time_trial_expirat(client): - """US-006 T-DES-1: dupa expirarea trial-ului, banner 'Trial Pro expirat' apare in _status.html.""" + """Dupa expirarea trial-ului, banner 'Trial Pro expirat' apare in _status.html.""" acct_id, _ = _create_account_user("trialexp@test.com") _login(client, "trialexp@test.com", "parolasecreta10") @@ -642,7 +639,7 @@ def test_banner_one_time_trial_expirat(client): def test_cont_arata_plan(client): - """US-006: tab-ul Cont (/tab=cont) afiseaza planul curent si explicatia de upgrade.""" + """Tab-ul Cont (/tab=cont) afiseaza planul curent si explicatia de upgrade.""" acct_id, _ = _create_account_user("contplan@test.com") _login(client, "contplan@test.com", "parolasecreta10") @@ -662,7 +659,7 @@ def test_cont_arata_plan(client): def test_plan_linie_in_burger(client): - """US-006: meniul burger contine linia de plan (Plan: Gratuit / Pro · trial N zile).""" + """Meniul burger contine linia de plan (Plan: Gratuit / Pro · trial N zile).""" acct_id, _ = _create_account_user("burgerplan@test.com") _login(client, "burgerplan@test.com", "parolasecreta10") @@ -679,7 +676,7 @@ def test_plan_linie_in_burger(client): def test_trial_pro_arata_zile_in_burger(client): - """US-006: cont in trial -> burger arata 'Plan: Pro · trial N zile ramase'.""" + """Cont in trial -> burger arata 'Plan: Pro · trial N zile ramase'.""" acct_id, _ = _create_account_user("burgertrial@test.com") _login(client, "burgertrial@test.com", "parolasecreta10") diff --git a/tests/test_web_status_fragment.py b/tests/test_web_status_fragment.py index 4d0c04d..4ba607d 100644 --- a/tests/test_web_status_fragment.py +++ b/tests/test_web_status_fragment.py @@ -1,7 +1,4 @@ -"""Teste US-002 (PRD 3.4): bara de status persistenta cu etichete umane. - -TDD: testele se scriu INAINTE de implementare; la inceput pica (RED), -dupa implementare trec (GREEN). +"""Teste pentru bara de status persistenta cu etichete umane. Rute testate: - GET /_fragments/status -> bara de status cu etichete umane, scoped pe cont @@ -81,7 +78,7 @@ def client(monkeypatch): from app.config import get_settings get_settings.cache_clear() from app.web import ratelimit - ratelimit._hits.clear() # izolare: limiterul login e global in-proces + ratelimit._hits.clear() # limiterul login e global in-proces from app.main import app with TestClient(app, follow_redirects=False) as c: yield c @@ -89,16 +86,8 @@ def client(monkeypatch): get_settings.cache_clear() -# ============================================================ -# test_status_fragment_text_uman -# ============================================================ - def test_status_fragment_text_uman(client): - """GET /_fragments/status (autentificat) -> contine text uman de sanatate, NU stari tehnice brute. - - Actualizat US-003 (PRD 5.15): strip sanatate unificat in loc de bife individuale worker/RAR. - Textul 'Trimitere automata' a fost inlocuit cu 'declaratiile curg normal' / 'Blocat: ...'. - """ + """GET /_fragments/status (autentificat) -> contine text uman de sanatate, NU stari tehnice brute.""" _create_account_user("status@test.com", "parolasecreta10") _login(client, "status@test.com", "parolasecreta10") @@ -106,11 +95,9 @@ def test_status_fragment_text_uman(client): assert resp.status_code == 200 html = resp.text - # US-003 D6: strip sanatate cu text uman compus (nu bife individuale) assert "declaratiile" in html.lower(), ( f"Fragmentul nu contine textul de sanatate ('declaratiile'). HTML (primele 500 ch): {html[:500]}" ) - # NU trebuie sa contina text tehnic brut assert "worker viu" not in html.lower(), ( f"Fragmentul contine 'worker viu' (text tehnic brut). HTML (primele 500 ch): {html[:500]}" ) @@ -119,10 +106,6 @@ def test_status_fragment_text_uman(client): ) -# ============================================================ -# test_status_blocate_defalcare -# ============================================================ - def test_status_blocate_defalcare(client): """Cu submissions blocate in DB, fragmentul arata defalcarea pe motiv (texte umane).""" acct_id, _ = _create_account_user("blocate@test.com", "parolasecreta10") @@ -134,23 +117,16 @@ def test_status_blocate_defalcare(client): _insert_submission("needs_data", acct_id) _insert_submission("error", acct_id) - # Pill-urile s-au mutat in bara de filtre din sectiunea Trimiteri (nu in bara de status). resp = client.get("/?tab=acasa") assert resp.status_code == 200 html = resp.text - # Pill-urile au etichetele scurte per categorie (nu etichetele lungi din eticheta_stare) assert "Lipsa cod" in html, "Acasa nu arata pill-ul pentru needs_mapping" assert "Date incomplete" in html, "Acasa nu arata pill-ul pentru needs_data" assert "Eroare" in html, "Acasa nu arata pill-ul pentru error" - # Pill-urile arata numarul total per categorie (2 needs_mapping, 1 needs_data, 1 error) assert 'class="pill-cat"' in html, "Pill-urile trebuie sa fie elemente cu clasa pill-cat" assert "<button" in html, "Pill-urile trebuie sa fie elemente <button>" -# ============================================================ -# test_status_se_reincarca_htmx -# ============================================================ - def test_status_se_reincarca_htmx(client): """Fragmentul contine atribut hx-trigger cu poll periodic (every 15s).""" _create_account_user("htmx@test.com", "parolasecreta10") @@ -160,27 +136,20 @@ def test_status_se_reincarca_htmx(client): assert resp.status_code == 200 html = resp.text - # Trebuie sa contina hx-trigger periodic assert "hx-trigger" in html, ( f"Fragmentul nu contine atribut hx-trigger. HTML: {html[:500]}" ) assert "every 15s" in html, ( f"Fragmentul nu contine poll 'every 15s'. HTML: {html[:500]}" ) - # Trebuie sa aiba endpoint corect pentru auto-refresh assert "/_fragments/status" in html, ( "Fragmentul nu contine referinta la /_fragments/status pentru hx-get" ) - # Trebuie sa aiba un id stabil pe containerul radacina assert 'id="status-bar"' in html, ( "Fragmentul nu are id='status-bar' pe containerul radacina" ) -# ============================================================ -# US-014: banner "Necesita atentia ta" actionabil -# ============================================================ - def _insert_submission_vehicul(status, account_id, vin, nr): from app.db import get_connection import json @@ -204,21 +173,21 @@ def test_categorie_blocata_linkeaza_la_trimiteri_filtrate(client): _insert_submission("error", acct_id) html = client.get("/?tab=acasa").text - # Pill-ul scrie campul de filtru si re-trimite form-ul (nu mai navigheaza prin deep-link) assert "filtreazaStare(this, 'error')" in html assert 'data-status="error"' in html assert "tab=acasa&status=error" not in html def test_status_nu_arata_identificator_rand_blocat(client): - """US-003 (PRD 5.10): VIN/nr inmatriculare nu mai apar in bara de status. - Lista de ID-uri a fost inlocuita cu pill-uri cu numar total (fara PII nominal).""" + """VIN/nr inmatriculare nu mai apar in bara de status. + + Lista de ID-uri a fost inlocuita cu pill-uri cu numar total (fara PII nominal). + """ acct_id, _ = _create_account_user("ident@test.com", "parolasecreta10") _login(client, "ident@test.com", "parolasecreta10") _insert_submission_vehicul("error", acct_id, "WVWZZZ1KZAW000123", "B123ABC") html = client.get("/_fragments/status").text - # Bara de status arata doar contoare, nu lista cu VIN/nr per rand (fara PII nominal) assert "B123ABC" not in html, "Nr inmatriculare nu trebuie sa mai apara in bara de status" assert "WVWZZZ1KZAW000123" not in html, "VIN integral nu trebuie expus" assert "0123" not in html, "VIN partial nu trebuie sa mai apara in bara de status" @@ -237,6 +206,5 @@ def test_scoped_pe_cont(client): _insert_submission_vehicul("error", other, "OTHERVIN000009999", "X999ZZZ") html = client.get("/_fragments/status").text - # randul altui cont NU apare in banner-ul meu assert "X999ZZZ" not in html assert "9999" not in html diff --git a/tests/test_web_submissions.py b/tests/test_web_submissions.py index 04d3aa4..7debd9c 100644 --- a/tests/test_web_submissions.py +++ b/tests/test_web_submissions.py @@ -1,4 +1,4 @@ -"""Teste US-004 (PRD 3.5): "Coada" -> "Trimiteri" tabel lizibil + detaliu la click. +"""Teste tabel "Trimiteri" lizibil + detaliu la click. Coloane umane (RO), stare via labels (nu "sent" brut), vehicul/operatie/data din payload, motiv uman. Detaliu scoped pe cont (404 cross-account). @@ -78,10 +78,9 @@ def client(monkeypatch): def test_submissions_coloane_umane(client): - """US-004 (PRD 5.15): layout slim — informatia umana vizibila, cod brut ascuns. - Anterior (pana la US-004): tabel cu antete <th>; dupa US-004: lista slim. - Informatia ramane accesibila: vehicul/operatie din payload, stare umana, - nr. prezentare RAR pe linia meta discreta. + """Layout slim — informatia umana vizibila, cod brut ascuns. + Lista slim (nu tabel cu antete <th>). Informatia ramane accesibila: + vehicul/operatie din payload, stare umana, nr. prezentare RAR pe linia meta discreta. """ acct = _create_account_user("col@test.com") _insert_submission(acct, "sent", id_prezentare=68516) @@ -91,7 +90,7 @@ def test_submissions_coloane_umane(client): assert resp.status_code == 200 html = resp.text - # Layout slim prezent (US-004 PRD 5.15) + # Layout slim prezent assert "lista-trimiteri-slim" in html, "lista slim lipseste" assert "trimitere-slim" in html, "rand slim lipseste" @@ -107,13 +106,13 @@ def test_submissions_coloane_umane(client): assert "B777ZZZ" in html, "Nr inmatriculare din payload lipseste" assert "Reparatie frane" in html, "Operatia din payload lipseste" - # 5.16: #id_prezentare nu mai e pe rand (randul are MAX 2 linii) — detaliul complet + # #id_prezentare nu mai e pe rand (randul are MAX 2 linii) — detaliul complet # (inclusiv nr. prezentare RAR) traieste in modalul de detaliu. assert "68516" not in html, "Nr. prezentare RAR nu trebuie sa mai apara pe randul slim" def test_tab_eticheta_trimiteri(client): - """US-003 (3.6): tab-ul 'Trimiteri' a fost eliminat; deep-link ?tab=coada ramane + """Tab-ul 'Trimiteri' a fost eliminat; deep-link ?tab=coada ramane valid (nu 404) si serveste continutul Acasa.""" _create_account_user("et@test.com") _login(client, "et@test.com") @@ -125,7 +124,7 @@ def test_tab_eticheta_trimiteri(client): def test_motiv_needs_data_afisat(client): - """Pentru needs_data, motivul apare in detaliu (PRD 5.8 US-007: Motiv a iesit din tabel).""" + """Pentru needs_data, motivul apare in detaliu (nu mai e coloana in tabel).""" acct = _create_account_user("motiv@test.com") sid = _insert_submission( acct, "needs_data", @@ -138,10 +137,10 @@ def test_motiv_needs_data_afisat(client): def test_tabel_nu_are_coloana_motiv(client): - """PRD 5.8 US-007: Motiv nu e o coloana separata in thead/tbody. + """Motiv nu e o coloana separata in thead/tbody. - Nota (PRD 5.9 US-002, R1): eticheta umana scurta a problemei apare acum sub pill-ul - de Stare ca text mic (NU intr-o coloana proprie) — vezi test_eticheta_umana_apare_sub_pill. + Eticheta umana scurta a problemei apare sub pill-ul de Stare ca text mic + (NU intr-o coloana proprie) — vezi test_eticheta_umana_apare_sub_pill. """ acct = _create_account_user("nomotiv@test.com") _insert_submission( @@ -158,7 +157,7 @@ def test_tabel_nu_are_coloana_motiv(client): def test_operatie_contine_cod_rar(client): - """PRD 5.9 US-002: coloana Operatie arata codul RAR simplu (FARA prefix) cand mapat, 'nemapat' cand nu.""" + """Coloana Operatie arata codul RAR simplu (FARA prefix) cand mapat, 'nemapat' cand nu.""" acct = _create_account_user("codrar@test.com") # mapat: are cod_prestatie -> cod RAR vizibil _insert_submission(acct, "sent", payload={ @@ -180,14 +179,14 @@ def test_operatie_contine_cod_rar(client): resp = client.get("/_fragments/submissions") assert resp.status_code == 200 html = resp.text - # US-002: codul ramane, dar prefixul textual "cod RAR:" a fost eliminat (R8) + # Codul ramane, dar prefixul textual "cod RAR:" a fost eliminat assert "OE-2" in html assert "cod RAR:" not in html assert "nemapat" in html def test_pill_eticheta_scurta(client): - """PRD 5.8 US-007/US-006: pill-ul de Stare foloseste eticheta scurta; textul lung in title.""" + """Pill-ul de Stare foloseste eticheta scurta; textul lung in title.""" acct = _create_account_user("pill@test.com") _insert_submission(acct, "sent", id_prezentare=70001) _login(client, "pill@test.com") @@ -215,7 +214,7 @@ def test_detaliu_trimitere(client): # --------------------------------------------------------------------------- -# US-002 (PRD 5.9): tabel trimiteri — eticheta umana sub pill, fara chevron, +# Tabel trimiteri — eticheta umana sub pill, fara chevron, # cod RAR simplu (fara prefix), randul declanseaza modalul. Teste de randare. # --------------------------------------------------------------------------- @@ -248,7 +247,7 @@ def test_eticheta_umana_absenta_pe_rand_ok(client): def test_fara_chevron_in_rand(client): - """R8: niciun chevron in coloana # / pe rand (eliminat impreuna cu CSS/JS asociat).""" + """Niciun chevron in coloana # / pe rand (eliminat impreuna cu CSS/JS asociat).""" acct = _create_account_user("chev@test.com") _insert_submission(acct, "sent") _login(client, "chev@test.com") @@ -276,7 +275,7 @@ def test_cod_rar_fara_prefix_text(client): def test_cod_rar_nemapat_muted(client): - """Cand nemapat, linia 2 arata 'nemapat' muted (comportament 5.8 pastrat).""" + """Cand nemapat, linia 2 arata 'nemapat' muted.""" acct = _create_account_user("crn@test.com") _insert_submission(acct, "needs_mapping", payload={ "vin": "WVWZZZ1JZXW000222", @@ -292,7 +291,7 @@ def test_cod_rar_nemapat_muted(client): def test_rand_deschide_modal(client): - """Randul tinteste corpul modalului (#detaliu-modal-body), NU un rand-sibling (US-003).""" + """Randul tinteste corpul modalului (#detaliu-modal-body), NU un rand-sibling.""" acct = _create_account_user("mod@test.com") sid = _insert_submission(acct, "sent") _login(client, "mod@test.com") @@ -301,12 +300,12 @@ def test_rand_deschide_modal(client): html = resp.text assert f'hx-get="/_fragments/trimitere/{sid}"' in html assert 'hx-target="#detaliu-modal-body"' in html - # randul-sibling de detaliu din 5.8 a fost eliminat + # randul-sibling de detaliu a fost eliminat assert "detaliu-rand" not in html def test_rand_a11y_si_keyboard_markup(client): - """R8 a11y: rand role=button, tabindex=0, aria-haspopup=dialog, FARA aria-expanded. + """A11y: rand role=button, tabindex=0, aria-haspopup=dialog, FARA aria-expanded. Limitare: deschiderea efectiva pe Enter/Space si readucerea focusului pe Esc sunt gestionate de JS in base.html (keydown delegat), netestabile in TestClient (fara DOM). @@ -321,7 +320,7 @@ def test_rand_a11y_si_keyboard_markup(client): assert 'role="button"' in html assert 'tabindex="0"' in html assert 'aria-haspopup="dialog"' in html - assert "aria-expanded" not in html # R8: nu mai e expand/collapse pe rand + assert "aria-expanded" not in html # nu mai e expand/collapse pe rand # Handler-ele de tastatura traiesc in base.html (pagina completa). base = client.get("/?tab=coada").text assert "Enter" in base and "Spacebar" in base # Enter/Space deschid modalul @@ -329,8 +328,8 @@ def test_rand_a11y_si_keyboard_markup(client): # --------------------------------------------------------------------------- -# US-001 (R1): eticheta umana scurta a problemei pe randul de tabel. -# Teste pure pe _submission_row_view — randarea sub pill e US-002. +# Eticheta umana scurta a problemei pe randul de tabel. +# Teste pure pe _submission_row_view — randarea sub pill e separata. # --------------------------------------------------------------------------- def _fake_row(status: str, *, rar_error=None, id_prezentare=None, payload=None) -> dict: @@ -352,7 +351,7 @@ def _fake_row(status: str, *, rar_error=None, id_prezentare=None, payload=None) def test_eticheta_umana_sub_pill(): - """R1: randul expune `eticheta_problema` umana, reutilizand motiv (nu un nou decoder).""" + """Randul expune `eticheta_problema` umana, reutilizand motiv (nu un nou decoder).""" from app.web.routes import _submission_row_view v = _submission_row_view(_fake_row( "needs_data", @@ -421,13 +420,11 @@ def test_detaliu_trimitere_404_cross_account(client): # --------------------------------------------------------------------------- -# US-004 (PRD 5.15): lista trimiteri rand slim -# RED (inainte de implementare): testele de mai jos ESUEAZA pe tabelul vechi. -# GREEN (dupa implementare): lista slim cu .trimitere-slim / .lista-trimiteri-slim. +# Lista trimiteri rand slim: .trimitere-slim / .lista-trimiteri-slim. # --------------------------------------------------------------------------- def test_rand_slim_vin_operatie_pill(client): - """5.16: fiecare rand slim are 2 linii — L1 placuta (nr. inmatriculare) in .slim-vin, + """Fiecare rand slim are 2 linii — L1 placuta (nr. inmatriculare) in .slim-vin, L2 cod RAR · operatie · data in .slim-meta, plus un pill de stare cu clasa stare_css si eticheta stare_scurt. Lista e inconjurata de .lista-trimiteri-slim. """ @@ -451,7 +448,7 @@ def test_rand_slim_vin_operatie_pill(client): assert "slim-meta" in html, "slim-meta lipseste — linia 2" assert "slim-rand2" in html, "slim-rand2 lipseste — linia 2 (cod RAR · operatie · data)" - # VIN integral nu mai e pe rand (5.16) — traieste in modalul de detaliu. + # VIN integral nu mai e pe rand — traieste in modalul de detaliu. assert "000777" not in html, "VIN scurt nu mai trebuie randat pe randul slim (2 linii)" # Pill de stare: clasa CSS + eticheta scurta @@ -460,7 +457,7 @@ def test_rand_slim_vin_operatie_pill(client): def test_filtre_paginare_pastrate(client): - """US-004 lock: filtrele si paginarea raman functionale dupa redesign slim. + """Filtrele si paginarea raman functionale dupa redesign slim. Lista slim afiseaza rezultatele filtrate corect. """ acct = _create_account_user("filtrepag@test.com") @@ -489,7 +486,7 @@ def test_filtre_paginare_pastrate(client): def test_bulk_doar_blocate(client): - """US-004 lock: form bulk-trimiteri exista; checkbox DOAR pe randuri gestionabile + """Form bulk-trimiteri exista; checkbox DOAR pe randuri gestionabile (needs_data/needs_mapping/error); randurile read-only (sent) nu au checkbox. """ acct = _create_account_user("bulk5@test.com") @@ -502,7 +499,7 @@ def test_bulk_doar_blocate(client): html = resp.text # Layout slim prezent - assert "lista-trimiteri-slim" in html, "lista slim lipseste — test nu acopera US-004" + assert "lista-trimiteri-slim" in html, "lista slim lipseste — lista slim neacoperita" # Form bulk exista cu actiunea corecta assert 'id="bulk-trimiteri"' in html, "form#bulk-trimiteri lipseste" @@ -519,7 +516,7 @@ def test_bulk_doar_blocate(client): def test_click_deschide_detaliu(client): - """US-004: click pe randul slim deschide /_fragments/trimitere/{id} in modalul global. + """Click pe randul slim deschide /_fragments/trimitere/{id} in modalul global. Randul are atributele HTMX si a11y necesare (role=button, aria-haspopup=dialog). """ acct = _create_account_user("clickdet@test.com") @@ -531,7 +528,7 @@ def test_click_deschide_detaliu(client): html = resp.text # Layout slim ca premisa (confirma ca testul acopera noul design) - assert "lista-trimiteri-slim" in html, "lista slim lipseste — test nu acopera US-004" + assert "lista-trimiteri-slim" in html, "lista slim lipseste" # Randul are hx-get catre fragmentul de detaliu assert f'hx-get="/_fragments/trimitere/{sid}"' in html, \ diff --git a/tests/test_web_submissions_layout.py b/tests/test_web_submissions_layout.py index fb2b28c..1151e56 100644 --- a/tests/test_web_submissions_layout.py +++ b/tests/test_web_submissions_layout.py @@ -1,8 +1,8 @@ -"""Teste US-005 (PRD 5.10): VIN pe rand separat sub numarul de inmatriculare. +"""Teste pentru VIN pe rand separat sub numarul de inmatriculare. -VIN-ul era randat ca <span> inline in aceeasi celula cu nr. Story-ul cere un -element block-level (div/small/p cu display:block) sub nr, in stil muted. -Testul asserteaza tipul elementului (block), nu doar prezenta textului. +VIN-ul se afiseaza intr-un element block-level (div, stil muted) sub nr. +inmatriculare, nu inline in aceeasi celula. Testul asserteaza tipul +elementului (block), nu doar prezenta textului. """ from __future__ import annotations @@ -82,12 +82,11 @@ def client(monkeypatch): def test_placuta_pe_rand_identificator_primar(client): - """Placuta (nr. inmatriculare) e identificatorul PRIMAR, linia 1 a randului slim - (5.16): in <div class="slim-vin"> (block-level, prominent). + """Placuta (nr. inmatriculare) e identificatorul primar, linia 1 a randului slim, + randata intr-un <div class="slim-vin"> (block-level, prominent). - PRD 5.15 (US-004): VIN era identificatorul primar pe linia 1. - 5.16 (directiva user): operatorul scaneaza placuta de pe comanda, nu VIN-ul de 17 - caractere — placuta devine linia 1, VIN integral se muta in modalul de detaliu. + Operatorul scaneaza placuta de pe comanda, nu VIN-ul de 17 caractere — + placuta e linia 1, VIN integral se muta in modalul de detaliu. """ acct = _create_account_user("vin_layout@test.com") _ins(acct, vin="WVWZZZ1JZXW000001", nr="B123XYZ") @@ -97,10 +96,8 @@ def test_placuta_pe_rand_identificator_primar(client): assert resp.status_code == 200 html = resp.text - # Placuta trebuie sa apara in HTML assert "B123XYZ" in html, "placuta (nr. inmatriculare) trebuie sa apara in lista slim" - # Placuta e intr-un element block-level (div cu clasa slim-vin) plac = "B123XYZ" found_slim_vin = re.search( rf'<div[^>]*class="slim-vin[^"]*"[^>]*>[^<]*{re.escape(plac)}[^<]*</div>', @@ -112,19 +109,17 @@ def test_placuta_pe_rand_identificator_primar(client): + html[max(0, html.find(plac) - 80):html.find(plac) + 80] ) - # VIN integral NU mai e pe rand (max 2 linii) — traieste in modalul de detaliu. - assert "000001" not in html, "VIN-ul nu mai trebuie randat pe randul slim (5.16)" + assert "000001" not in html, "VIN-ul nu mai trebuie randat pe randul slim" def test_placuta_lipsa_nu_genereaza_rand_gol(client): """Cand placuta SI VIN-ul lipsesc, slim-vin nu afiseaza '—' izolat ca identificator. - Fallback (5.16): VIN scurt daca exista, altfel mesaj neutru ('fara numar') — niciodata + + Fallback: VIN scurt daca exista, altfel mesaj neutru ('fara numar') — niciodata un em-dash singur ca identificator primar. """ acct = _create_account_user("vin_gol@test.com") - # Placuta prezenta -> e identificatorul primar pe linia 1. sid1 = _ins(acct, vin="", nr="B999TST") - # Placuta SI VIN absente -> fallback 'fara numar' (nu '—' izolat). sid2 = _ins(acct, vin="", nr="") _login(client, "vin_gol@test.com") @@ -132,16 +127,12 @@ def test_placuta_lipsa_nu_genereaza_rand_gol(client): assert resp.status_code == 200 html = resp.text - # Ambele randuri exista assert f'id="trimitere-row-{sid1}"' in html assert f'id="trimitere-row-{sid2}"' in html - # Placuta vizibila cand exista assert "B999TST" in html, "placuta (nr. inmatriculare) lipseste de pe rand" - # Niciun slim-vin nu contine '—' izolat for m in re.finditer(r'<div[^>]*class="slim-vin[^"]*"[^>]*>([^<]*)</div>', html): assert m.group(1).strip() != "—", "slim-vin afiseaza '—' izolat ca identificator" - # Fallback neutru cand placuta + VIN lipsesc assert "fara numar" in html, "fallback 'fara numar' lipseste cand placuta+VIN absente" diff --git a/tests/test_web_tabs.py b/tests/test_web_tabs.py index 9fec1c8..3d6716a 100644 --- a/tests/test_web_tabs.py +++ b/tests/test_web_tabs.py @@ -1,7 +1,4 @@ -"""Teste US-003 (PRD 3.4): navigare cu tab-uri (shell dashboard). - -TDD: testele se scriu INAINTE de implementare; la inceput pica (RED), -dupa implementare trec (GREEN). +"""Teste navigare cu tab-uri (shell dashboard). Rute testate: - GET / -> dashboard cu tab-bar si panou activ randat server-side @@ -73,7 +70,7 @@ def client(monkeypatch): # ============================================================ def test_dashboard_are_tabbar(client): - """US-009 (5.10): tab-bar-ul eliminat; Mapari mutat in meniul ☰; rutele raman valide.""" + """Tab-bar-ul e eliminat; Mapari mutat in meniul hamburger; rutele raman valide.""" _create_account_user("tabbar@test.com", "parolasecreta10") _login(client, "tabbar@test.com", "parolasecreta10") @@ -81,8 +78,8 @@ def test_dashboard_are_tabbar(client): assert resp.status_code == 200 html = resp.text - # US-009: tab-bar-ul (role="tablist") a fost eliminat - assert 'role="tablist"' not in html, "Tab-bar-ul (role=tablist) trebuie eliminat (US-009)" + # Tab-bar-ul (role="tablist") a fost eliminat + assert 'role="tablist"' not in html, "Tab-bar-ul (role=tablist) trebuie eliminat" # Cont/Integrare/Nomenclator raman in meniu, nu ca tab-uri for label in ("Cont", "Integrare", "Nomenclator", "Import"): assert not re.search(rf'role="tab"[^>]*>\s*{label}\s*<', html), \ @@ -98,7 +95,7 @@ def test_dashboard_are_tabbar(client): # ============================================================ def test_tab_implicit_acasa(client): - """US-009: fara ?tab=, pagina principala randeaza continutul Acasa (upload + sectiuni).""" + """Fara ?tab=, pagina principala randeaza continutul Acasa (upload + sectiuni).""" _create_account_user("implicit@test.com", "parolasecreta10") _login(client, "implicit@test.com", "parolasecreta10") @@ -106,8 +103,8 @@ def test_tab_implicit_acasa(client): assert resp.status_code == 200 html = resp.text - # US-009: tab-bar eliminat, deci nu mai exista aria-selected pe tab-uri - assert 'role="tablist"' not in html, "Tab-bar-ul trebuie eliminat (US-009)" + # Tab-bar eliminat, deci nu mai exista aria-selected pe tab-uri + assert 'role="tablist"' not in html, "Tab-bar-ul trebuie eliminat" # Continutul Acasa (status-bar + tab-panel cu continut Acasa) e randat direct assert 'id="status-bar"' in html, "Status-bar-ul trebuie sa fie prezent" assert 'id="tab-panel"' in html, "Panoul de continut (tab-panel) trebuie sa fie prezent" @@ -145,9 +142,9 @@ def test_tab_activ_randat_server_side(client): resp = client.get("/") assert resp.status_code == 200 html = resp.text - # US-009: role="tabpanel" eliminat; continutul e in div#tab-panel fara rol ARIA de tabpanel + # role="tabpanel" eliminat; continutul e in div#tab-panel fara rol ARIA de tabpanel assert 'id="tab-panel"' in html, "Containerul de continut tab-panel trebuie sa existe" - assert 'role="tabpanel"' not in html, "role=tabpanel trebuie eliminat (US-009)" + assert 'role="tabpanel"' not in html, "role=tabpanel trebuie eliminat" # Import tab server-side: ?tab=import randeaza direct continutul Import resp2 = client.get("/?tab=import") @@ -161,7 +158,7 @@ def test_tab_activ_randat_server_side(client): # ============================================================ def test_fragmentele_inactive_lazy(client): - """US-003 (3.6): Trimiterile sunt sectiune pe Acasa, nu un tab separat. + """Trimiterile sunt sectiune pe Acasa, nu un tab separat. First-run (zero trimiteri): sectiunea Trimiteri (si poll-ul ei) e suprimata. Dupa ce contul are trimiteri, sectiunea apare pe Acasa cu poll-ul ei. @@ -201,7 +198,7 @@ def test_fragmentele_inactive_lazy(client): # ============================================================ def test_tabbar_aria(client): - """US-009: schela ARIA orfana (role=tablist/tab/tabpanel/aria-selected) a fost eliminata.""" + """Schela ARIA orfana (role=tablist/tab/tabpanel/aria-selected) a fost eliminata.""" _create_account_user("aria@test.com", "parolasecreta10") _login(client, "aria@test.com", "parolasecreta10") @@ -209,8 +206,8 @@ def test_tabbar_aria(client): assert resp.status_code == 200 html = resp.text - # US-009: un role="tablist" cu un singur tab e violare ARIA → eliminat - assert 'role="tablist"' not in html, "role=tablist trebuie eliminat (US-009)" + # Un role="tablist" cu un singur tab e violare ARIA -> eliminat + assert 'role="tablist"' not in html, "role=tablist trebuie eliminat" assert 'role="tab"' not in html, "role=tab trebuie eliminat (tab-bar eliminat)" assert 'role="tabpanel"' not in html, "role=tabpanel trebuie eliminat (tab-bar eliminat)" assert 'aria-selected=' not in html, "aria-selected trebuie eliminat (fara tab-uri)" @@ -220,11 +217,11 @@ def test_tabbar_aria(client): # ============================================================ -# test_fragmentele_mutate_raman_accesibile (US-007) +# test_fragmentele_mutate_raman_accesibile # ============================================================ def test_fragmentele_mutate_raman_accesibile(client): - """US-007 (5.5): Cont/Integrare/Nomenclator s-au mutat in meniu, dar rutele de fragment + """Cont/Integrare/Nomenclator s-au mutat in meniu, dar rutele de fragment si deep-link-ul ?tab= raman valide (zero rute moarte / 404).""" _create_account_user("frag@test.com", "parolasecreta10") _login(client, "frag@test.com", "parolasecreta10") diff --git a/tests/test_web_tema_culori.py b/tests/test_web_tema_culori.py index a0387dd..936c6d2 100644 --- a/tests/test_web_tema_culori.py +++ b/tests/test_web_tema_culori.py @@ -1,10 +1,8 @@ -"""Teste US-013 (PRD 5.10): Tema de culori ROMFAST (accent azur) + tipografie IBM Plex. - -TDD: testele se scriu INAINTE de implementare (RED), dupa implementare trec (GREEN). +"""Teste tema de culori ROMFAST (accent azur) + tipografie. Testeaza: -- test_paleta_accent_azur_definita: accentul azur ROMFAST definit corect in :root si [data-theme="light"] -- test_font_ibm_plex_aplicat: IBM Plex Sans + Mono declarate in font-family si @font-face +- test_paleta_accent_azur_definita: accentul azur definit corect in :root si [data-theme="light"] +- test_font_system_stack_aplicat: stiva de fonturi sistem aplicata (fara IBM Plex) - test_contrast_aa_pe_text_principal: contrast text principal >= 4.5:1 in dark si light """ @@ -91,7 +89,7 @@ def test_paleta_accent_azur_definita(client): assert resp.status_code == 200 style = _get_style_block(resp.text) - # Paleta dark (:root) + # Paleta dark (implicita, :root) dark_vars = { "--accent": "#2E74D6", "--bg": "#0f1218", @@ -140,12 +138,12 @@ def test_paleta_accent_azur_definita(client): # ── test_font_system_stack_aplicat ─────────────────────────────────────────── def test_font_system_stack_aplicat(client): - """US-001 (PRD 5.16): IBM Plex eliminat; body foloseste stiva de fonturi sistem. + """Body foloseste stiva de fonturi sistem, fara IBM Plex. Verifica: - body font-family foloseste var(--font-ui) (CSS custom property) - --font-ui este definit in :root si contine un system font stack (system-ui / -apple-system) - - ZERO @font-face cu 'IBM Plex' in <style> (IBM Plex eliminat complet) + - ZERO @font-face cu 'IBM Plex' in <style> - ZERO referinte catre /static/fonts/ in HTML (nu se mai servesc fisiere woff2) """ resp = client.get("/login") @@ -176,18 +174,17 @@ def test_font_system_stack_aplicat(client): f"Valoare gasita: {font_ui_m.group(1).strip()}" ) - # 3. ZERO @font-face cu IBM Plex (eliminat in US-001) + # 3. ZERO @font-face cu IBM Plex font_face_blocks = re.findall(r"@font-face\s*\{([^}]+)\}", style, re.DOTALL) ibm_face = [b for b in font_face_blocks if "IBM Plex" in b or "ibm-plex" in b.lower()] assert not ibm_face, ( - f"@font-face cu IBM Plex trebuia eliminat (US-001 PRD 5.16). " - f"Blocat gasit: {ibm_face}" + f"@font-face cu IBM Plex trebuia eliminat. Bloc gasit: {ibm_face}" ) # 4. ZERO referinte /static/fonts/ in HTML randat (nu mai servim woff2) html = resp.text assert "/static/fonts/" not in html, ( - "Referinte catre /static/fonts/ gasite in HTML — trebuie eliminate (US-001 PRD 5.16)." + "Referinte catre /static/fonts/ gasite in HTML — trebuie eliminate." ) diff --git a/tests/test_web_uniformizare.py b/tests/test_web_uniformizare.py index 2b2c503..9fb94d0 100644 --- a/tests/test_web_uniformizare.py +++ b/tests/test_web_uniformizare.py @@ -1,5 +1,5 @@ -"""Teste PRD 5.5 — uniformizare UI: US-001 (Acasa fara Ajutor), US-002 (Nomenclator grila -standard), US-003 (macro autosend) — actualizat dupa PRD 5.11 US-002 (macro neutralizat). +"""Teste uniformizare UI: Acasa fara sectiunea Ajutor, Nomenclator ca grila +standard, macro autosend_toggle neutralizat. """ from __future__ import annotations @@ -21,7 +21,7 @@ def _env(): # ============================================================ -# US-002: Nomenclator ca tabel standard (grila Trimiteri) +# Nomenclator ca tabel standard (grila Trimiteri) # ============================================================ def test_nomenclator_grila_standard_cu_randuri(): @@ -43,7 +43,7 @@ def test_nomenclator_empty_state(): # ============================================================ -# US-003: macro autosend_toggle compact (Auto/Manual) +# macro autosend_toggle compact (Auto/Manual) # ============================================================ def _render_macro(form_id="map-1", checked=True): @@ -52,21 +52,21 @@ def _render_macro(form_id="map-1", checked=True): def test_autosend_pastreaza_name_si_prezenta(): - """US-002 (PRD 5.11): macro autosend_toggle neutralizat — output gol, fara checkbox.""" + """Macro autosend_toggle neutralizat — output gol, fara checkbox.""" html = _render_macro(checked=True) - assert 'name="auto_send"' not in html, "US-002: checkbox auto_send scos din macro" + assert 'name="auto_send"' not in html, "checkbox auto_send scos din macro" assert html.strip() == "", f"macro neutralizat trebuie sa intoarca string gol, got: {html!r}" def test_autosend_nebifat_fara_checked(): - """US-002: macro neutralizat intoarce gol indiferent de checked.""" + """Macro neutralizat intoarce gol indiferent de checked.""" html = _render_macro(checked=False) assert 'name="auto_send"' not in html assert html.strip() == "" def test_autosend_compact_fara_proza_inline(): - """US-002: macro neutralizat nu contine nicio proza inline.""" + """Macro neutralizat nu contine nicio proza inline.""" html = _render_macro() assert "La fisierele viitoare" not in html assert "Tine pentru verificare" not in html @@ -75,7 +75,7 @@ def test_autosend_compact_fara_proza_inline(): # ============================================================ -# US-001: Acasa fara sectiunea Ajutor +# Acasa fara sectiunea Ajutor # ============================================================ @pytest.fixture() @@ -121,7 +121,7 @@ def test_acasa_fara_sectiune_ajutor(client): # ============================================================ -# US-005: Tabel Mapari standardizat + panou Ajutor +# Tabel Mapari standardizat + panou Ajutor # ============================================================ def _seed_needs_mapping(acct_id, cod_op="OP-NM", denumire="Operatie test"): @@ -146,18 +146,18 @@ def test_mapari_ajutor_disclosure_si_fara_proza_inline(client): resp = client.get("/_fragments/mapari") assert resp.status_code == 200 html = resp.text - # US-010: sectiunea de ajutor (<details class="ajutor-mapari">) eliminata + # Sectiunea de ajutor (<details class="ajutor-mapari">) eliminata assert "ajutor-mapari" not in html - # US-002: coloana In coada scoasa din tabel - assert "In coada" not in html, "US-002: coloana 'In coada' scoasa din tabelul Mapari" + # Coloana In coada scoasa din tabel + assert "In coada" not in html, "Coloana 'In coada' scoasa din tabelul Mapari" # proza inline veche eliminata de pe sectiuni assert "sugestia fuzzy e preselectata) si salveaza" not in html assert "Maparile operatie -> cod RAR retinute pentru contul tau" not in html def test_mapari_comutator_compact_in_tabel(client): - """US-002: tabul Mapari nu mai contine checkbox auto_send.""" + """Tabul Mapari nu mai contine checkbox auto_send.""" acct = _login(client) _seed_needs_mapping(acct) html = client.get("/_fragments/mapari").text - assert 'name="auto_send"' not in html, "US-002: checkbox auto_send scos din UI" + assert 'name="auto_send"' not in html, "checkbox auto_send scos din UI" diff --git a/tests/test_worker_active_gate.py b/tests/test_worker_active_gate.py index 5eb3829..b30813e 100644 --- a/tests/test_worker_active_gate.py +++ b/tests/test_worker_active_gate.py @@ -1,9 +1,6 @@ -"""Teste US-008 — gate worker: claim_one sare submission-urile conturilor inactive. +"""Teste gate worker: claim_one sare submission-urile conturilor inactive. -TDD: testele se scriu INAINTE de modificarea claim_one; la inceput pica (RED), -dupa modificare trec (GREEN). - -C14: LEFT JOIN accounts + COALESCE(a.active, 1) = 1 +LEFT JOIN accounts + COALESCE(a.active, 1) = 1: (a) cont legacy fara active -> COALESCE(NULL,1)=1 -> tratat ca activ (b) submissions.account_id IS NULL (ON DELETE SET NULL) -> LEFT JOIN lasa a.active NULL -> COALESCE(NULL,1)=1 -> tratat ca activ/default @@ -126,7 +123,7 @@ def test_claim_account_null_tratat_activ(env): def test_claim_sare_cont_blocat(env): - """5.5: cont blocked -> claim_one nu ridica submission-ul.""" + """Cont blocked -> claim_one nu ridica submission-ul.""" from app.accounts import create_account, set_status from app.worker.__main__ import claim_one @@ -140,7 +137,7 @@ def test_claim_sare_cont_blocat(env): def test_claim_sare_cont_arhivat(env): - """5.5: cont archived -> claim_one nu ridica submission-ul.""" + """Cont archived -> claim_one nu ridica submission-ul.""" from app.accounts import create_account, set_status from app.worker.__main__ import claim_one @@ -154,7 +151,7 @@ def test_claim_sare_cont_arhivat(env): def test_deblocare_reia_trimiterea(env): - """5.5: blocked -> set_status('active') -> claim_one ridica din nou.""" + """Blocked -> set_status('active') -> claim_one ridica din nou.""" from app.accounts import create_account, set_status from app.worker.__main__ import claim_one diff --git a/tests/test_worker_held.py b/tests/test_worker_held.py index 2216574..305a5d4 100644 --- a/tests/test_worker_held.py +++ b/tests/test_worker_held.py @@ -1,12 +1,10 @@ -"""Teste PRD 5.19 US-003 + US-008 — worker respecta flag `held`. +"""Teste worker: respecta flag `held`. -US-003: claim_one sare peste randurile `queued AND held=1` (tinute manual). -US-008: expire_held expira randurile tinute mai vechi de `held_retention_days` - (default 90) -> status 'error', rar_error='TINUT_EXPIRAT', purge_after - DIRECT la momentul expirarii (NU +30z retentie blocati -> nu 120z total), - apoi purge_expired le sterge (PII purjat). - -TDD: testele se scriu INAINTE de modificarea worker-ului. +claim_one sare peste randurile `queued AND held=1` (tinute manual). +expire_held expira randurile tinute mai vechi de `held_retention_days` +(default 90) -> status 'error', rar_error='TINUT_EXPIRAT', purge_after +DIRECT la momentul expirarii (NU +30z retentie blocati -> nu 120z total), +apoi purge_expired le sterge (PII purjat). """ from __future__ import annotations @@ -65,7 +63,7 @@ def _row_status(conn, sid): return r["status"] if r else None -# --- US-003: claim_one sare peste randurile tinute --- +# --- claim_one sare peste randurile tinute --- def test_claim_sare_rand_tinut(env): """queued, held=1, cont activ, send on -> claim_one intoarce None; ramane queued.""" @@ -116,7 +114,7 @@ def test_eliberarea_deblocheaza_claim(env): assert _row_status(conn, sid) == "sending" -# --- US-008: expirare randuri tinute --- +# --- expirare randuri tinute --- def test_expire_held_expira_rand_vechi(env): """queued, held=1, created_at vechi (>90z) -> error + TINUT_EXPIRAT + purge_after ~now.""" @@ -179,7 +177,7 @@ def test_expire_held_ignora_rand_neted_vechi(env): def test_expire_held_apoi_purge_sterge_randul(env): """Lant complet ACELASI CICLU: expire_held (purge_after=now-1s) -> purge_expired - sterge randul imediat (PII purjat), fara sa astepte ciclul urmator (fix /code-review #6).""" + sterge randul imediat (PII purjat), fara sa astepte ciclul urmator.""" from app.accounts import create_account from app.worker.__main__ import expire_held, purge_expired diff --git a/tests/test_worker_keepalive_rar.py b/tests/test_worker_keepalive_rar.py index 7b3335e..102635a 100644 --- a/tests/test_worker_keepalive_rar.py +++ b/tests/test_worker_keepalive_rar.py @@ -67,13 +67,12 @@ def _set_last_login(conn, *, ago_s: float | None): def _account_cu_creds(conn) -> int: - """Creeaza cont cu creds in slotul per-env (US-013 — legacy rar_creds_enc dropata).""" + """Creeaza cont cu creds in slotul per-env.""" from app.accounts import create_account from app.crypto import encrypt_creds acct = create_account(conn, "Service Cu Creds", email="svc@example.com") enc = encrypt_creds({"email": "svc@example.com", "password": "secret"}) - # US-013: scrie in slotul per-env; rar_env din fixture = valoarea default (test sau prod). - # Folosim rar_creds_test_enc si rar_creds_prod_enc (ambele) pentru robustete. + # Scrie in ambele sloturi per-env (test si prod) pentru robustete. conn.execute( "UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1, " "rar_creds_prod_enc=?, rar_prod_enabled=1 WHERE id=?", @@ -162,7 +161,7 @@ def test_target_sare_creds_nedecriptabile(env): settings.worker_use_test_creds = False # Cont cu creds GUNOI (nedecriptabile sub cheia curenta), id mai mic. bad = create_account(conn, "Cont Cheie Veche", email="old@example.com") - # US-013: scrie in slotul per-env (rar_env = settings.rar_env, implicit in fixture) + # Scrie in slotul per-env (rar_env = settings.rar_env, implicit in fixture) bad_slot = f"rar_creds_{settings.rar_env}_enc" conn.execute(f"UPDATE accounts SET {bad_slot}='gAAAAA-token-invalid' WHERE id=?", (bad,)) # Cont cu creds valide, id mai mare. diff --git a/tests/test_worker_observ.py b/tests/test_worker_observ.py index 8ba0ded..226f41f 100644 --- a/tests/test_worker_observ.py +++ b/tests/test_worker_observ.py @@ -1,4 +1,4 @@ -"""Teste US-005 (PRD 5.6): audit login RAR + ciclu de viata trimiteri (worker).""" +"""Teste audit login RAR + ciclu de viata trimiteri (worker).""" from __future__ import annotations diff --git a/tests/test_worker_rar_env.py b/tests/test_worker_rar_env.py index 4c17f5a..922fd5f 100644 --- a/tests/test_worker_rar_env.py +++ b/tests/test_worker_rar_env.py @@ -1,4 +1,4 @@ -"""Teste US-006 (PRD 5.20) — sesiuni si trimitere worker per (cont, env). +"""Teste sesiuni si trimitere worker per (cont, env). Verifica: - AccountSessions re-cheiat pe (account_id, rar_env): doua env ale aceluiasi cont @@ -7,9 +7,9 @@ Verifica: prod -> rar_base_url_prod), nu ancora globala. - Creds extrase din slotul accounts.rar_creds_{env}_enc corect per env. - Purjarea creds efemere scoped pe (account_id, rar_env): login pe test NU sterge - creds efemere ale submission-urilor PROD ale aceluiasi cont (auto-fix E1/1a). + creds efemere ale submission-urilor PROD ale aceluiasi cont. - recover_orphans per (cont, env): orfanii prod reconciliati contra endpoint prod, - nu contra test (auto-fix 1b/E6). + nu contra test. """ from __future__ import annotations @@ -180,7 +180,7 @@ def test_base_url_dupa_submission(env, monkeypatch): def test_creds_din_slotul_env(env, monkeypatch): """Cand submissions.rar_creds_enc lipseste, worker ia din accounts.rar_creds_{env}_enc. - Prod ia din rar_creds_prod_enc, nu din slotul test (auto-fix 1c/E8 + fallback per-env). + Prod ia din rar_creds_prod_enc, nu din slotul test (fallback per-env). """ import app.worker.__main__ as w from app.crypto import encrypt_creds @@ -212,14 +212,14 @@ def test_creds_din_slotul_env(env, monkeypatch): # --------------------------------------------------------------------------- -# test_purge_creds_doar_pe_env (auto-fix E1/1a) +# test_purge_creds_doar_pe_env # --------------------------------------------------------------------------- def test_purge_creds_doar_pe_env(env, monkeypatch): """Dupa login pe env=test, creds efemere ale submission-urilor PROD raman neatinse. Scopul purjarii: WHERE account_id=? AND rar_env=?. Altfel un login TEST sterge - creds ale submission-urilor PROD -> prod blocat (auto-fix E1/1a). + creds ale submission-urilor PROD -> prod blocat. """ import app.worker.__main__ as w from app.crypto import encrypt_creds @@ -251,13 +251,13 @@ def test_purge_creds_doar_pe_env(env, monkeypatch): # --------------------------------------------------------------------------- -# test_reconcile_pe_env_corect (auto-fix 1b/E6) +# test_reconcile_pe_env_corect # --------------------------------------------------------------------------- def test_reconcile_pe_env_corect(env, monkeypatch): """Un orfan env=prod e reconciliat contra endpoint PROD, nu contra test. - auto-fix 1b/E6: recover_orphans filtreaza pe rar_env si foloseste clientul/token-ul + recover_orphans filtreaza pe rar_env si foloseste clientul/token-ul env-ului corect. Orfanii prod contra endpoint test -> no-match -> re-POST prod = DUPLICAT real ireversibil. """ diff --git a/tests/test_worker_rar_errors.py b/tests/test_worker_rar_errors.py index bbd13a1..e2ed3ca 100644 --- a/tests/test_worker_rar_errors.py +++ b/tests/test_worker_rar_errors.py @@ -1,4 +1,4 @@ -"""Teste US-004 — erorile RAR (400/401) imbracate pe 3 niveluri in worker. +"""Teste erori RAR (400/401) imbracate pe 3 niveluri in worker. process_one cu 400 -> rar_error superset [{field,message,cod,problema,cauza,fix}] run() loop cu 401 la login -> submission error, rar_error JSON 3-niveluri RAR_CREDS_INVALIDE @@ -78,7 +78,7 @@ class FakeRar: return self.post_result -# --- Teste US-004 --- +# --- Teste erori RAR --- def test_rar_400_stocheaza_3niveluri(env): """RAR 400 cu field_errors -> rar_error superset array cu cod/problema/fix/message.""" diff --git a/tools/backup.py b/tools/backup.py index c7d90ed..c323712 100644 --- a/tools/backup.py +++ b/tools/backup.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Backup online SQLite (T7) — copie consistenta a bazei pe volum persistent. +"""Backup online SQLite — copie consistenta a bazei pe volum persistent. SQLite in WAL NU se copiaza sigur cu `cp` (WAL-ul poate avea tranzactii necheckpoint-ate). Folosim API-ul `Connection.backup` (online, consistent, fara oprirea worker-ului). diff --git a/tools/carantina_held.py b/tools/carantina_held.py index 951cd80..9a7db05 100644 --- a/tools/carantina_held.py +++ b/tools/carantina_held.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""CLI carantina randuri tinute — atenuare hazard de rollback (PRD 5.19 Riscul R4). +"""CLI carantina randuri tinute — atenuare hazard de rollback. Daca se face revert pe cod DUPA ce randuri au primit `held=1`, worker-ul pierde filtrul `AND held=0` din `claim_one` -> ar trimite TOATE randurile tinute la RAR @@ -40,7 +40,7 @@ def carantina(conn: sqlite3.Connection, dry_run: bool = False) -> int: Seteaza si `purge_after` (retentia randurilor blocate, `blocked_retention_days`): fara el un rand `error` iesit din `queued` NU primeste niciodata purge_after -> PII criptat ar sta la nesfarsit (`purge_expired` sterge doar randuri cu purge_after setat). Aliniat - cu calea normala worker `error` (/code-review #4). + cu calea normala worker `error`. """ n = _count(conn) if dry_run or n == 0: @@ -56,7 +56,7 @@ def carantina(conn: sqlite3.Connection, dry_run: bool = False) -> int: def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( - description="Carantineaza randurile tinute (queued AND held=1) inainte de un revert de cod (PRD 5.19 R4)." + description="Carantineaza randurile tinute (queued AND held=1) inainte de un revert de cod." ) parser.add_argument( "--dry-run", action="store_true", diff --git a/tools/import_dbf.py b/tools/import_dbf.py index b114f32..3db20e4 100644 --- a/tools/import_dbf.py +++ b/tools/import_dbf.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Import DBF ROAAUTO -> SQLite gateway (T5). +"""Import DBF ROAAUTO -> SQLite gateway. Plan.md sect. 7: dry-run + raport intai (randuri valide, mapari orfane, coduri necunoscute in nomenclator), apoi scrie in SQLite. Surse: @@ -217,7 +217,7 @@ def run( def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description="Import DBF ROAAUTO -> SQLite gateway (T5)") + parser = argparse.ArgumentParser(description="Import DBF ROAAUTO -> SQLite gateway") parser.add_argument("--commit", action="store_true", help="scrie in SQLite (implicit: doar raport)") parser.add_argument("--account-id", type=int, default=DEFAULT_ACCOUNT_ID, help="cont tinta pentru mapari") parser.add_argument("--mapare", type=Path, default=MAPARE_DBF, help="cale mapare_prestatii.DBF") diff --git a/tools/mapare-llm/eticheteaza.py b/tools/mapare-llm/eticheteaza.py index d030ee1..c4d3f23 100644 --- a/tools/mapare-llm/eticheteaza.py +++ b/tools/mapare-llm/eticheteaza.py @@ -1,8 +1,7 @@ -"""Etichetator offline operatii service -> coduri RAR (US-002, PRD 5.18). +"""Etichetator offline operatii service -> coduri RAR. -Backend implicit = **LM Studio local** (Qwen3-4B, GPU RX 6600M via Tailscale), -backend-ul APROBAT pentru bootstrap-ul v1 (decizia D4). Groq / OpenRouter raman -fallback-uri interschimbabile, dar NU sunt calea aprobata pentru v1. +Backend implicit = **LM Studio local** (Qwen3-4B, GPU RX 6600M via Tailscale). +Groq / OpenRouter raman fallback-uri interschimbabile, dar nu backend-ul implicit. Particularitati care justifica un tool NOU (nu reuse de `or_common.call`): - LM Studio RESPINGE `response_format: json_object` (eroare 400). Cere envelope @@ -12,12 +11,12 @@ Particularitati care justifica un tool NOU (nu reuse de `or_common.call`): - Qwen3 emite `<think>...` daca nu dezactivam thinking-ul -> umfla tokeni/latenta sub structured output strict. Punem `/no_think` in promptul de sistem. -Setari conservatoare OBLIGATORII pe GPU-box (a facut shutdown sub sarcina 2026-06-29, +Setari conservatoare OBLIGATORII pe GPU-box (a facut shutdown sub sarcina, probabil termic/alimentare): in LM Studio incarca modelul cu `n_parallel=1`, `n_ctx=4096`, batch 32-40, monitorizeaza temperatura. NU mari batch/context fara -headroom termic. Vezi memorie `lmstudio-gpu-etichetare`. +headroom termic. -Reutilizeaza din `or_common`: scrub-ul PII (F3) si lista de coduri. +Reutilizeaza din `or_common`: scrub-ul PII si lista de coduri. """ from __future__ import annotations diff --git a/tools/mapare-llm/genereaza_seed.py b/tools/mapare-llm/genereaza_seed.py index 9b38ea8..b166f4b 100644 --- a/tools/mapare-llm/genereaza_seed.py +++ b/tools/mapare-llm/genereaza_seed.py @@ -1,20 +1,20 @@ -"""Generare seed etichetat operatie->cod (US-003, PRD 5.18). +"""Generare seed etichetat operatie->cod. Produce artefactul `app/data/operatii-etichetate.json` (comis in repo), consumat de -seeder (US-004) si de corpusul embeddings (US-005). NU cheama LLM la runtime — o -singura data, offline, pe LM Studio (backend implicit, D4). +seeder si de corpusul embeddings. NU cheama LLM la runtime — o singura data, +offline, pe LM Studio (backend implicit). -Pipeline dedup OBLIGATORIU, in ordine, INAINTE de orice apel LLM (D5): - 1. Agrega cele N CSV-uri -> freq pe denumire RAW (NR ne-numeric -> skip rand, F9). +Pipeline dedup OBLIGATORIU, in ordine, INAINTE de orice apel LLM: + 1. Agrega cele N CSV-uri -> freq pe denumire RAW (NR ne-numeric -> skip rand). 2. `cheie = normalize_for_match(denumire)` (ACEEASI functie ca DB/k-NN, NU strip exact). - Arunca randurile cu `cheie == ""` inainte de dedup (coliziune pe slot UNIQUE gol, F6). + Arunca randurile cu `cheie == ""` inainte de dedup (coliziune pe slot UNIQUE gol). 3. Dedup pe cheie: un reprezentant per cheie, `freq = suma NR`. 4. Harta `cheie -> cod` din TOATE etichetele existente: `labels-groq-partial.json` (cheiat brut) + seedul comis anterior (cheiat normalizat). Conflict (acelasi cheie, coduri diferite - pe variante raw) -> castiga codul cu freq-max, tie-break pe cod sortat (F3). + pe variante raw) -> castiga codul cu freq-max, tie-break pe cod sortat. 5. `de_etichetat = corpus(in prag) - harta`. Sortat desc pe freq = SINGURUL input la LLM. -Idempotenta cross-run (F2/F7): seedul comis = cache de etichete -> re-run = 0 apeluri LLM. +Idempotenta cross-run: seedul comis = cache de etichete -> re-run = 0 apeluri LLM. """ from __future__ import annotations diff --git a/tools/mapare-llm/heldout_eval.py b/tools/mapare-llm/heldout_eval.py index 1a5ca2c..6340e68 100644 --- a/tools/mapare-llm/heldout_eval.py +++ b/tools/mapare-llm/heldout_eval.py @@ -1,6 +1,6 @@ """Harness de evaluare held-out pentru sistemul de mapare operatii->coduri RAR. -Scop (L14-S5, Decision #19 PRD 5.14): +Scop: Masurarea ACURATETEI REALE a clasificatorului inainte de a permite orice tier auto-send peste GOLD propriu. @@ -19,12 +19,11 @@ Continut: exclusiv responsabilitatea operatorului. 3. eval_predictions() — date (predictii, gold) -> precizie globala + per-cod + matrice confuzie + rata cod-gresit. - 4. kill_criterion() — evalueaza daca sistemul indeplineste pragul de acceptanta - (F-E, PRD 5.14). + 4. kill_criterion() — evalueaza daca sistemul indeplineste pragul de acceptanta. Ce NU face: NU eticheteaza ground-truth-ul. Etichetarea de cod ar fi exact "antrenare pe test" - si ar invalida precizia raportata (Decision #19). Fisierul exportat se completeaza + si ar invalida precizia raportata. Fisierul exportat se completeaza MANUAL de operatorul uman. CLI: @@ -71,10 +70,10 @@ DEFAULT_SEED = 42 _STRAT_HEAD_END_PCT = 0.20 _STRAT_MID_END_PCT = 0.50 # head+mid = 50%, deci mid = 30% -# Kill-criterion (F-E, PRD 5.14): +# Kill-criterion: # # DEFAULT_WRONG_CODE_THRESHOLD = 0.005 (0.5%) -# Justificare: un cod gresit = FINALIZATA ireversibila la RAR (Premisa 3). +# Justificare: un cod gresit = FINALIZATA ireversibila la RAR. # La 200 operatii/zi auto-rezolvate cu 0.5% rata gresita = 1 FINALIZATA # gresita/zi, ceea ce depaseste toleranta operationala acceptabila. # Pragul poate fi RELAXAT empiric pe baza de date reale; NU inasprit post-hoc. @@ -98,7 +97,7 @@ def sample_stratified( ) -> list[dict]: """Esantionare stratificata aleatorie pe trei strate Zipf: cap/mijloc/coada. - Determinista cu seed; NU apeleaza LLM (PRD L14-S5). + Determinista cu seed; NU apeleaza LLM. rows: lista de (denumire, nr) — frecventele absolute. Nu trebuie sortata in prealabil. @@ -182,7 +181,7 @@ def export_for_labeling(sample: list[dict], path: str) -> None: Coloana `cod_gold` ramane GOALA in fisierul exportat. NU o completa cu etichete LLM sau automate: ar fi "antrenare pe test" - si ar invalida precizia raportata (Decision #19, PRD 5.14). + si ar invalida precizia raportata. sample: lista de {denumire, nr, strat} returnata de sample_stratified() path: fisierul CSV de scris (suprascrie daca exista) @@ -323,7 +322,7 @@ def eval_predictions( # --------------------------------------------------------------------------- -# Kill-criterion (F-E, PRD 5.14) +# Kill-criterion # --------------------------------------------------------------------------- def kill_criterion( @@ -331,7 +330,7 @@ def kill_criterion( wrong_code_threshold: float = DEFAULT_WRONG_CODE_THRESHOLD, coverage_threshold: float = DEFAULT_COVERAGE_THRESHOLD, ) -> dict: - """Evalueaza daca sistemul de clasificare indeplineste pragul de acceptanta (F-E). + """Evalueaza daca sistemul de clasificare indeplineste pragul de acceptanta. Sistemul TRECE daca: wrong_code_rate < wrong_code_threshold (implicit 0.5%) @@ -339,7 +338,7 @@ def kill_criterion( coverage_rate > coverage_threshold (implicit 50%) Un sistem care nu trece kill-criterion NU trebuie folosit pentru auto-send - dincolo de GOLD propriu (Decision #19, #17, PRD 5.14). + dincolo de GOLD propriu. metrics: dict returnat de eval_predictions() sau compatibil (must have keys: wrong_code_rate, coverage_rate). @@ -375,7 +374,7 @@ def kill_criterion( elif not cond_wrong_code: reason = ( f"ESUEAZA: rata cod-gresit {wcr:.2%} >= {wrong_code_threshold:.2%}. " - f"Un cod gresit = FINALIZATA ireversibila la RAR (Premisa 3, PRD 5.14). " + f"Un cod gresit = FINALIZATA ireversibila la RAR. " f"Auto-send dincolo de GOLD dezactivat pana la recalibrat." ) else: @@ -442,7 +441,7 @@ def _load_corpus_from_csvs(data_dir: str) -> list[tuple[str, int]]: def _print_report(metrics: dict) -> None: sep = "=" * 70 print(sep) - print("RAPORT EVALUARE HELD-OUT (L14-S5, PRD 5.14)") + print("RAPORT EVALUARE HELD-OUT") print(sep) print(f" Total intrari evaluate: {metrics['total']}") print(f" Corecte: {metrics['correct']}") @@ -480,7 +479,7 @@ def main() -> None: import argparse p = argparse.ArgumentParser( - description="Harness eval held-out L14-S5 (PRD 5.14).", + description="Harness eval held-out pentru mapare operatii->coduri RAR.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Moduri de utilizare: @@ -559,7 +558,7 @@ Moduri de utilizare: print(" '?' = incert (clasificatorul nu poate decide)") print() print(" ATENTIE: NU folosi etichete LLM drept cod_gold!") - print(" Asta ar fi 'antrenare pe test' (Decision #19, PRD 5.14) si ar") + print(" Asta ar fi 'antrenare pe test' si ar") print(" invalida orice masurare de acuratete.") diff --git a/tools/mapare-llm/or_label.py b/tools/mapare-llm/or_label.py index d46ee8d..6fdccac 100644 --- a/tools/mapare-llm/or_label.py +++ b/tools/mapare-llm/or_label.py @@ -1,19 +1,19 @@ -"""Etichetator batch offline OpenRouter (Layer 1) — L14-S1. +"""Etichetator batch offline OpenRouter (Layer 1). Clasifica denumirile de operatii service in cele 18 coduri RAR + NUL. -Cerinte implementate (PRD 5.14 / Decision Audit Trail): +Cerinte implementate: 1. Prioritizare pe FRECVENTA (desc): corpus_by_freq() din or_common - 2. Grupare pe similaritate (rapidfuzz token_sort_ratio, threshold conservator - Eng-F7): LLM eticheteaza doar reprezentantul, codul se propaga la grup - 3. Ensemble NVIDIA (super-120b + nano-9b, PRD #9): acord unanim -> high; + 2. Grupare pe similaritate (rapidfuzz token_sort_ratio, threshold conservator): + LLM eticheteaza doar reprezentantul, codul se propaga la grup + 3. Ensemble NVIDIA (super-120b + nano-9b): acord unanim -> high; dezacord (orice divergenta) -> needs_mapping. Vot pe coduri, nu pe self-confidence. ultra-550b EXCLUS (4-5x mai lent, zero castig) - 4. Scrub PII (F3): integrat in or_common.call() (regex nr inmatriculare/VIN) + 4. Scrub PII: integrat in or_common.call() (regex nr inmatriculare/VIN) 5. Resumabil: scrie *-partial.json incremental, reia de unde a ramas; retry/backoff pe 429 gestionat de or_common.call() 6. Output: {denumire, cod, sursa, confidence, grup_rep} - NUL = ancore negativa + supresie, NU promovat la cod RAR (#4) + NUL = ancore negativa + supresie, NU promovat la cod RAR CLI: python3 tools/mapare-llm/or_label.py [N] [--out path] [--partial path] [--threshold 85] [--batch 20] [--pace 4.0] @@ -28,7 +28,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import or_common as oc from rapidfuzz import fuzz -# Modele NVIDIA (decizie PRD #9: pastram super-120b + nano-9b; aruncam ultra-550b) +# Modele NVIDIA: pastram super-120b + nano-9b; aruncam ultra-550b MODELS = [ "nvidia/nemotron-3-super-120b-a12b:free", "nvidia/nemotron-nano-9b-v2:free", @@ -85,8 +85,8 @@ def ensemble_vote(votes): - Toate N modele cu acelasi cod valid -> (cod, "high", "ensemble-unanim") - Toate N modele cu "NUL" -> ("NUL", "high", "ensemble-unanim-nul") - Orice divergenta / parse-fail partial -> ("?", "needs_mapping", "ensemble-dezacord") - Vot pe coduri, NU pe self-confidence (PRD #10, Eng-F7). - NUL tratat SEPARAT: ancore negativa, nu e cod RAR (#4). + Vot pe coduri, NU pe self-confidence. + NUL tratat SEPARAT: ancore negativa, nu e cod RAR. Intoarce: (cod_final, confidence, sursa) cod_final: cod RAR valid | "NUL" | "?" (needs human review)