Compare commits

...

3 Commits

Author SHA1 Message Date
Claude Agent
e2627c1aa4 test(web): actualizeaza 4 teste pentru meniul grupat + pagina Setari
- badge: cauta elementul randat (class="tab-badge"), nu substringul brut —
  CSS-ul nou din base.html contine mereu selectorul .tab-badge
- meniu mapari: fereastra de context largita — iconita SVG sta intre href si text
- setari: eticheta sub-tabului e "Cheie API", nu "Cheia mea API"

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 13:50:33 +00:00
Claude Agent
44f261e269 refactor: comentarii strict functionale, fara referinte PRD/stories
Curatare globala a comentariilor si docstring-urilor (app, tools, teste,
scripturi): eliminate referintele la PRD-uri, US-xxx, task-uri istorice si
review-uri; pastrata doar informatia functionala, formulata scurt. Regula
adaugata in CLAUDE.md (sectiunea Stil). Fara modificari de cod sau comportament.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 13:48:38 +00:00
Claude Agent
86408887e5 feat(setari): meniu grupat + pagina Setari cu sub-taburi + schimbare parola
Meniu hamburger reorganizat pe grupuri cu etichete (LUCRU / CONT) cu iconite
SVG discrete; "Cont" redenumit "Setari" (URL ?tab=cont pastrat, zero rute moarte).

Pagina Setari restructurata in sub-taburi interne: Date firma / Credentiale RAR /
Cheie API / Cont (plan + schimbare parola), cu indicator de stare per tab. Sub-tabul
activ e pastrat server-side (activ_subtab) dupa fiecare swap HTMX; comutarea intre
taburi e client-side, delegata pe document (supravietuieste swap-urilor).

Credentiale RAR: mediile Testare/Productie ca doua carduri paralele.

Schimbare parola de acces din sub-tabul Cont (functie noua, lipsea):
- users.change_password (scrypt): verifica parola actuala constant-time, valideaza
  noua (10..128, diferita), rescrie hash+salt nou;
- ruta POST /cont/schimba-parola, scoped pe user_id din sesiune, CSRF enforce,
  parola niciodata re-pusa in raspuns, jurnal 'parola_schimbata'.

Referinte copy "Meniu cont > Cont" -> "Setari".

Teste: schimbare parola (succes / parola actuala gresita / confirmare nepotrivita /
lungime) + izolare rate-limit login in fixture-ul clientului.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 13:40:38 +00:00
232 changed files with 2055 additions and 2149 deletions

112
CLAUDE.md
View File

@@ -1,104 +1,86 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Ghid Claude Code pentru acest repo.
## Ce este
Gateway web (Python / FastAPI) care preia prezentari de service-auto si le declara la **RAR AUTOPASS** (Legea 142/2023, OM 210/2024). Inlocuieste integrarea Visual FoxPro `RarAutoPass` (ROAAUTO), arhivata in `legacy-vfp/` (doar referinta).
Gateway web (Python/FastAPI) care preia prezentari service-auto si le declara la **RAR AUTOPASS** (L.142/2023, OM 210/2024). Inlocuieste integrarea VFP `RarAutoPass` (ROAAUTO), arhivata in `legacy-vfp/` (doar referinta).
Limba proiectului este **romana**: cod, comentarii, commit-uri, documentatie. Fara emoji in raspunsuri sau fisiere noi (preferinta proiect).
## Stil
## Surse de adevar (citeste-le inainte sa modifici comportament)
- Limba proiectului: **romana** (cod, comentarii, commit-uri, docs). Fara emoji.
- **Comentarii scurte, strict functionale.** Doar ce nu reiese din cod. FARA referinte la PRD-uri, stories (US-xxx), review-uri sau istoricul deciziei.
- `docs/api-rar-contract.md` — contractul RAR. **Unde un plan/cod difera de contract, contractul are dreptate.**
- `docs/ROADMAP.md` — singura sursa de progres + procesul de lucru. O sesiune noua porneste de aici. Doar sectiunea "Stadiu Implementare" se modifica pe parcurs; detaliile stau in PRD-uri (`docs/prd/`).
## Surse de adevar
- `docs/api-rar-contract.md` — contractul RAR; la divergenta, contractul are dreptate.
- `docs/ROADMAP.md` — progres + proces de lucru; sesiunile noi pornesc de aici. Se modifica doar "Stadiu Implementare"; detaliile stau in `docs/prd/`.
## Comenzi
```bash
pip3 install -r requirements.txt # Python 3.12+
# Rulare locala (dev): API + worker sunt PROCESE SEPARATE
uvicorn app.main:app --reload --port 8010 # API: dashboard /, Swagger /docs, /healthz, /metrics
python3 -m app.worker # worker (necesar doar pentru a procesa coada)
# Dev: API + worker = procese SEPARATE
uvicorn app.main:app --reload --port 8010 # dashboard /, /docs, /healthz, /metrics
python3 -m app.worker # proceseaza coada
# Wrapper-ul start.sh ambaleaza mediu (test/prod) + rol (api/worker/both/finalizate)
./start.sh test both --send # API + worker, trimite efectiv la RAR test (loguri in .run/)
./start.sh test finalizate # listeaza prezentarile inregistrate la RAR (verificare independenta)
./start.sh status # stare procese + /healthz
./start.sh stop # opreste procesele pornite cu "both"
# start.sh = mediu (test/prod) + rol (api/worker/both/finalizate); loguri in .run/
./start.sh test both --send # trimite efectiv la RAR test
./start.sh test finalizate # listeaza prezentarile inregistrate la RAR
./start.sh status | stop
./start-test.sh / ./start-prod.sh # fixeaza mediul, forwardeaza rolul
# Teste (pytest; folosesc FastAPI TestClient + SQLite temporar). Testele live RAR sunt
# skip implicit (marker `live`) — `pytest -q` nu atinge endpointul real.
# Teste (TestClient + SQLite temporar); testele live RAR sunt skip implicit (marker `live`)
python3 -m pytest -q
python3 -m pytest tests/test_worker_reconcile.py -q # un fisier
python3 -m pytest tests/test_worker_reconcile.py::test_x -q # un singur test
python3 -m pytest -q -m "not live" # exclude explicit testele live
python3 -m pytest tests/test_worker_reconcile.py::test_x -q
AUTOPASS_LIVE_RAR=1 python3 -m pytest tests/test_live_rar.py -q # LIVE opt-in: creeaza FINALIZATA; cere settings.xml cu creds <test>
# Test LIVE pe RAR test (opt-in, skip implicit; atinge endpointul real -> creeaza FINALIZATA):
# reproduce lantul mapare inline -> queued -> worker -> sent -> verificare in finalizate.
AUTOPASS_LIVE_RAR=1 python3 -m pytest tests/test_live_rar.py -q # necesita settings.xml cu creds <test>
# Chei API (admin, doar CLI); cheia rfak_... se afiseaza O SINGURA DATA
python3 -m tools.apikey create --account 2 | list | rotate | revoke
# Lifecycle chei API (admin, doar din CLI — nu exista suprafata HTTP)
python3 -m tools.apikey create --account 2 # cheie afisata O SINGURA DATA (rfak_...)
python3 -m tools.apikey list|rotate|revoke
# Docker (deploy): api + worker + autoheal, acelasi image + volum SQLite
docker compose up --build
docker compose up --build # deploy: api + worker + autoheal, acelasi image + volum SQLite
```
## Arhitectura
**Doua procese peste acelasi SQLite (WAL) persistent**, care comunica EXCLUSIV prin tabela `submissions`:
**Doua procese peste acelasi SQLite (WAL)**, comunica EXCLUSIV prin tabela `submissions`:
- **API** (`app.main:app`) — API v1 (`app/api/v1/router.py` + `import_router.py`), dashboard web HTMX (`app/web/routes.py` + `templates/`), `/healthz`, `/metrics`. Worker-ul **nu** ruleaza ca task aici: un worker mort nu trebuie sa lase containerul "sanatos".
- **Worker** (`app/worker/__main__.py`) — bucla: heartbeat → recupereaza orfane → claim atomic (`BEGIN IMMEDIATE`) → login RAR (per cont)`postPrezentare` → update. Retry/backoff exponential, reconciliere anti-duplicat, lease pe randuri `sending` orfane, re-login la JWT expirat (TTL 30h). Send DEZACTIVAT implicit (`AUTOPASS_WORKER_SEND_ENABLED=false`) — sigur pentru probe.
- **API** (`app.main:app`) — API v1 (`app/api/v1/`), dashboard HTMX (`app/web/` + `templates/`), `/healthz`, `/metrics`. Worker-ul NU ruleaza aici: un worker mort nu trebuie sa lase containerul "sanatos".
- **Worker** (`app/worker/__main__.py`) — bucla: heartbeat → recuperare orfane → claim atomic (`BEGIN IMMEDIATE`) → login RAR per cont → `postPrezentare` → update. Retry/backoff exponential, reconciliere anti-duplicat, lease pe `sending` orfane, re-login la JWT expirat (TTL 30h). Send OFF implicit (`AUTOPASS_WORKER_SEND_ENABLED=false`).
**Doua canale de intrare** convergent in coada `submissions`:
- Canal API (Treapta 1): `POST /v1/prezentari` pentru ROAAUTO / soft propriu.
- Import web (Treapta 2): upload xlsx/csv → mapare coloane → preview → commit (`app/import_parse.py`, `import_router.py`, dashboard).
**Doua canale de intrare** in coada: `POST /v1/prezentari` (ROAAUTO/soft propriu) si import web xlsx/csv → mapare coloane → preview → commit (`app/import_parse.py`, `import_router.py`).
Flux: validare (`validation.py`) → mapare operatie→cod (`mapping.py`) → enqueue cu PII criptat → worker trimite → dashboard monitorizeaza live.
Flux: validare (`validation.py`) → mapare operatie→cod (`mapping.py`) → enqueue cu PII criptat → worker trimite → dashboard live.
### Invariante critice (usor de stricat)
### Invariante critice
- **`AUTOPASS_CREDS_KEY` trebuie sa fie ACEEASI intre API si worker.** API cripteaza creds RAR (Fernet), worker le decripteaza. Chei diferite → worker nu poate decripta → trimiterile esueaza. `start.sh both` genereaza o cheie efemera partajata; pentru prod pune una persistenta in `.env`. (`crypto.py`)
- **Idempotenta = hash de continut canonic** server-side (`idempotency.py`), pentru ca RAR accepta duplicate si nu are nr. comanda. `build_key` normalizeaza INTOTDEAUNA `account_id` la `account_or_default` (None == 1) INAINTE de hash altfel acelasi rand logic primeste chei diferite pe canalele API vs import (OV-2). `canonicalize_row` normeaza VIN/nr/odometru (strip ".0" din coercion Excel) inainte de validare si de cheie.
- **`FINALIZATA` e terminal la RAR** fara anulare/corectie prin API. De aceea reconcilierea anti-duplicat: pe eroare **ambigua** (timeout / TransportError / 502/503/504 / 429 / 408) sau rand `sending` orfan, worker-ul cauta in finalizate (match pe vin+dataPrestatie+odometruFinal) si marcheaza `sent` fara a re-trimite (`reconcile.py`). **EXCEPTIE: un RAR 500 cu mesaj** (`RarError.rar_message`, ex. `ORA-12899`) e un esec DEFINITIV (RAR a raspuns „am esuat", nu o pierdere de raspuns) → worker-ul NU reconciliaza si NU reincearca, marcheaza `error` cu mesajul RAR (`RAR_EROARE_SERVER`). Altfel ar marca fals `sent` pe un record PARTIAL pe care RAR (ne-tranzactional) il lasa la esec.
- **Creds RAR per cont**: durabile in `accounts.rar_creds_enc` (canal web, fallback re-login) SAU efemere in `submissions.rar_creds_enc` (canal API, sterse dupa primul login reusit). Worker incearca submission-ul intai, apoi fallback la cont. Purjarea sterge DOAR `submissions.rar_creds_enc`, NU `accounts.rar_creds_enc`.
- **Auth API-key** (`auth.py`): identifica CONTUL ROAAUTO, separat de credentialele RAR. Stocam doar SHA-256 al cheii. Enforcement prin `AUTOPASS_REQUIRE_API_KEY`: `false` (dev) → fara cheie merge pe cont id=1, cheie invalida → 401; `true` (prod) → cheie obligatorie pe `/v1/*` protejat. POST-urile + rutele de import sunt account-scoped; GET-urile de listare sunt si ele account-scoped (5.15/US-011: fragmentele `_fragments/submissions|trimitere|mapari|status|jurnal|nomenclator|trimiteri-versiune` sub `require_login` + scope, 404-before-leak pe id strain; `GET /v1/prezentari(/{id})`/`/v1/mapari`/`/v1/audit/export` filtrate pe cont). `GET /v1/nomenclator` ramane public intentionat (coduri RAR publice, fara PII).
- **Admin bootstrap fara hardcodare**: la `/signup`, primul user din TOATA baza (`count_admins()==0`, verificat in tranzactie) primeste `is_admin=1` automat; orice signup ulterior e neadmin. `is_admin` da acces la `/admin` (panou global, toate conturile) — verificat pe `account_id`-ul sesiunii, dar neconditionat DE care `account_id` a primit userul (semnul de "cine e admin" nu are legatura cu contul `id=1`). Contul `id=1` e doar bucket-ul de fallback pentru trafic API neautentificat in dev (`AUTOPASS_REQUIRE_API_KEY=false`) — separat de conceptul de admin. Fix manual daca primul signup s-a consumat deja gresit: `python3 -m tools.account set-admin --account N`.
- **Mapare coloane retinuta per `(account_id, signature_coloane)`** (`column_mappings`): la urmatorul fisier cu aceleasi coloane, pentru acelasi cont, maparea se reaplica automat. Un cont poate avea mai multe formate memorate simultan.
- **Mapare operatie→cod**: prestatie poate veni cu `cod_prestatie` (cod RAR direct) sau `cod_op_service` (cod intern) + `denumire`. Nerezolvat → submission `needs_mapping` (nu se trimite), apare in editorul web cu sugestie fuzzy; la salvarea maparii se re-rezolva automat submission-urile blocate.
- **`cod_prestatie` e VALIDAT fata de nomenclator la ingestie** (`resolve_prestatii(..., valid_codes)`): un cod direct NECUNOSCUT in nomenclator NU se mai trimite raw — e promovat la `cod_op_service` (denumire=cod) si tratat ca operatie de mapat. Motiv (confirmat live 2026-06-23): RAR accepta NUMAI coduri din nomenclator (coloana `COD_PRESTATIE` max 5 car.); un cod necunoscut da **HTTP 500** (`ORA-12899`), iar RAR **NU e tranzactional** lasa un record PARTIAL `FINALIZATA` (terminal) chiar pe esec, pe care reconcilierea worker-ului l-ar marca fals `sent`. Comportamentul la cod necunoscut/nemapat: `on_unmapped_error` (camp boolean top-level pe `POST /v1/prezentari` + `/valideaza`) = `false` (intra in editor, `needs_mapping`) sau `true` (respinge fara enqueue `submission_id=null` + `erori`). Default = `accounts.on_unmapped_error_default` (implicit `false`/`0`); precedenta cerere > cont > `false`.
- **WAF RAR da 403 fara User-Agent de browser** — toate apelurile httpx trimit `User-Agent: Mozilla/5.0` (`config.py`, confirmat live).
- **422 fara echo de credentiale**: handler-ul global de validare in `main.py` pastreaza type/loc/msg dar DROP-a `input`/`ctx` (altfel ar reflecta `rar_credentials.password`).
- **Retentie**: `submissions` sent + `import_batches` primesc `purge_after = now + 90 zile`; worker-ul purjeaza odata pe ora (T16, GDPR/L.142).
- **`AUTOPASS_CREDS_KEY` identica intre API si worker** (Fernet, `crypto.py`): chei diferite → worker nu decripteaza creds → trimiterile esueaza. `start.sh both` genereaza cheie efemera partajata; in prod, cheie persistenta in `.env`.
- **Idempotenta = hash de continut canonic** server-side (`idempotency.py`) RAR accepta duplicate, fara nr. comanda. `build_key` normalizeaza `account_id` la `account_or_default` (None == 1) INAINTE de hash, altfel acelasi rand primeste chei diferite pe API vs import. `canonicalize_row` normeaza VIN/nr/odometru (strip ".0" din Excel) inainte de validare si cheie.
- **`FINALIZATA` e terminal la RAR** (fara anulare/corectie). Pe eroare **ambigua** (timeout/TransportError/502/503/504/429/408) sau `sending` orfan, worker-ul cauta in finalizate (vin+dataPrestatie+odometruFinal) si marcheaza `sent` fara re-trimitere (`reconcile.py`). **EXCEPTIE: RAR 500 cu mesaj** (`RarError.rar_message`, ex. `ORA-12899`) = esec DEFINITIV → fara reconciliere/retry, `error` cu mesajul RAR (`RAR_EROARE_SERVER`); altfel ar marca fals `sent` un record PARTIAL lasat de RAR (ne-tranzactional).
- **Creds RAR per cont**: durabile in `accounts` (canal web, fallback re-login) SAU efemere in `submissions.rar_creds_enc` (canal API, sterse dupa primul login reusit). Worker incearca submission-ul intai, apoi contul. Purjarea sterge DOAR creds efemere din `submissions`.
- **Auth API-key** (`auth.py`): identifica contul ROAAUTO (separat de creds RAR); stocam doar SHA-256. `AUTOPASS_REQUIRE_API_KEY`: `false` (dev) → fara cheie = cont id=1, cheie invalida → 401; `true` (prod) → obligatorie pe `/v1/*` protejat. POST-urile, importul si GET-urile de listare (API + fragmente dashboard) sunt account-scoped, 404-before-leak pe id strain. `GET /v1/nomenclator` public intentionat (coduri RAR publice, fara PII).
- **Admin bootstrap**: primul user din TOATA baza la `/signup` (`count_admins()==0`, in tranzactie) devine `is_admin=1`; restul nu. `is_admin` da acces la `/admin` (panou global) si e independent de contul id=1 (acela e doar fallback dev pentru API neautentificat). Fix manual: `python3 -m tools.account set-admin --account N`.
- **Mapare coloane memorata per `(account_id, signature_coloane)`** (`column_mappings`): reaplicata automat la fisiere cu aceleasi coloane; un cont poate avea mai multe formate.
- **Mapare operatie→cod**: prestatia vine cu `cod_prestatie` (cod RAR) sau `cod_op_service` + `denumire`. Nerezolvat → `needs_mapping` (nu se trimite), editor web cu sugestie fuzzy; salvarea maparii re-rezolva automat submission-urile blocate.
- **`cod_prestatie` VALIDAT fata de nomenclator la ingestie** (`resolve_prestatii(..., valid_codes)`): cod necunoscut NU se trimite raw — e promovat la `cod_op_service` (denumire=cod) si intra la mapat. Motiv (confirmat live): RAR accepta doar coduri din nomenclator (max 5 car.); cod necunoscut HTTP 500 `ORA-12899`, iar RAR ne-tranzactional lasa record PARTIAL `FINALIZATA` pe care reconcilierea l-ar marca fals `sent`. La cod nemapat: `on_unmapped_error` (boolean top-level pe `POST /v1/prezentari` + `/valideaza`) = `false` editor/`needs_mapping`; `true` respins fara enqueue (`submission_id=null` + `erori`). Precedenta: cerere > `accounts.on_unmapped_error_default` > `false`.
- **WAF RAR da 403 fara User-Agent de browser** — httpx trimite mereu `User-Agent: Mozilla/5.0` (`config.py`).
- **422 fara echo de credentiale**: handler-ul din `main.py` pastreaza type/loc/msg dar DROP-a `input`/`ctx` (altfel reflecta `rar_credentials.password`).
- **Retentie**: `submissions` sent + `import_batches` `purge_after = now + 90 zile`; worker purjeaza orar (GDPR/L.142).
### Masina de stari submissions
`queued → sending → sent` (succes, cu `id_prezentare` de la RAR). Ramuri: `needs_mapping` (cod nerezolvat), `needs_data` (RAR 400, validare continut), `error` (max retries / 4xx nerecuperabil / **RAR 500 cu mesaj — esec definitiv** / creds invalide / login 401 — NU se face retry pe creds gresite). Backoff: `next_attempt_at = now + base*2^retry`, plafonat. Schema completa: `app/schema.sql`.
`queued → sending → sent` (cu `id_prezentare` de la RAR). Ramuri: `needs_mapping`, `needs_data` (RAR 400), `error` (max retries / 4xx nerecuperabil / RAR 500 cu mesaj / creds invalide sau login 401 — fara retry pe creds gresite). Backoff: `next_attempt_at = now + base*2^retry`, plafonat. Schema: `app/schema.sql`.
## Mod non-interactiv
Vezi `/workspace/CLAUDE.md` (workspace-level): cand esti lansat cu `claude -p`, creeaza fisiere noi DOAR in `/workspace/.claude-work/<task>/`, nu in `/workspace`. Modificarile la fisiere existente se fac in locatia originala.
Vezi `/workspace/CLAUDE.md`: cu `claude -p`, fisierele noi merg DOAR in `/workspace/.claude-work/<task>/`; modificarile la fisiere existente raman in locatia originala.
## Skill routing
When the user's request matches an available skill, invoke it via the Skill tool. When in doubt, invoke the skill.
Cand cererea se potriveste unui skill, invoca-l prin Skill tool; la indoiala, invoca-l.
Key routing rules:
- Product ideas/brainstorming → invoke /office-hours
- Strategy/scope → invoke /plan-ceo-review
- Architecture → invoke /plan-eng-review
- Design system/plan review → invoke /design-consultation or /plan-design-review
- Full review pipeline → invoke /autoplan
- Bugs/errors → invoke /investigate
- QA/testing site behavior → invoke /qa or /qa-only
- Code review/diff check → invoke /review
- Visual polish → invoke /design-review
- Ship/deploy/PR → invoke /ship or /land-and-deploy
- Save progress → invoke /context-save
- Resume context → invoke /context-restore
- Author a backlog-ready spec/issue → invoke /spec
- Idei produs → /office-hours · Strategie → /plan-ceo-review · Arhitectura → /plan-eng-review
- Design system/plan → /design-consultation, /plan-design-review · Pipeline complet → /autoplan
- Bug-uri → /investigate · QA site → /qa, /qa-only · Code review → /review · Polish vizual → /design-review
- Ship/deploy/PR → /ship, /land-and-deploy · Salvare/reluare context → /context-save, /context-restore · Spec → /spec

View File

@@ -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(

View File

@@ -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

View File

@@ -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:

View File

@@ -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).

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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).

View File

@@ -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": (

View File

@@ -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.

View File

@@ -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.

View File

@@ -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).

View File

@@ -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=?",

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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

View File

@@ -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).

View File

@@ -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).

View File

@@ -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

View File

@@ -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))

View File

@@ -157,6 +157,53 @@ def verify_password(conn: sqlite3.Connection, email: str, password: str) -> int
return None
def change_password(
conn: sqlite3.Connection,
user_id: int,
current_password: str,
new_password: str,
) -> None:
"""Schimba parola de acces a userului identificat prin user_id.
Verifica parola actuala (scrypt, comparatie constant-time), valideaza parola
noua (10..128, diferita de cea actuala) si rescrie password_hash + salt nou cu
parametrii scrypt curenti. Parola NICIODATA stocata in clar.
Ridica ValueError la: user inexistent, parametri parola corupti, parola actuala
gresita, parola noua invalida. Nu invalideaza sesiunea (apelantul decide).
"""
row = conn.execute(
"SELECT password_hash, salt, scrypt_params FROM users WHERE id=?",
(user_id,),
).fetchone()
if row is None:
raise ValueError("utilizator inexistent")
salt = bytes.fromhex(row["salt"])
expected = bytes.fromhex(row["password_hash"])
params = _parse_scrypt_params(row["scrypt_params"] or "")
if params is None:
raise ValueError("parametri parola corupti")
n, r, p = params
actual = _scrypt_hash(current_password, salt, n=n, r=r, p=p)
if not hmac.compare_digest(actual, expected):
raise ValueError("Parola actuala este incorecta.")
if len(new_password) < _PASSWORD_MIN:
raise ValueError(f"Parola noua e prea scurta (minim {_PASSWORD_MIN} caractere).")
if len(new_password) > _PASSWORD_MAX:
raise ValueError(f"Parola noua e prea lunga (maxim {_PASSWORD_MAX} caractere).")
if new_password == current_password:
raise ValueError("Parola noua trebuie sa difere de cea actuala.")
new_salt = secrets.token_bytes(16)
new_hash = _scrypt_hash(new_password, new_salt)
conn.execute(
"UPDATE users SET password_hash=?, salt=?, scrypt_params=? WHERE id=?",
(new_hash.hex(), new_salt.hex(), SCRYPT_PARAMS, user_id),
)
def get_user_by_email(conn: sqlite3.Connection, email: str) -> dict | None:
"""Metadate user dupa email (FARA password_hash si salt)."""
row = conn.execute(

View File

@@ -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

View File

@@ -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()

View File

@@ -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(

View File

@@ -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.

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
<div id="acasa-section">
{# === 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 @@
</div>
{% endif %}
{# === Container colapsabil: stepper + upload intr-un singur element <details> (US-006).
{# === Container colapsabil: stepper + upload intr-un singur element <details>.
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)

View File

@@ -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):

View File

@@ -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

View File

@@ -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 @@
&times;
</button>
</span>
{# 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 %}

View File

@@ -6,13 +6,13 @@
style="margin-top:22px; padding-top:18px; border-top:2px solid var(--line);"
{% if oob %}hx-swap-oob="outerHTML"{% endif %}>
<div class="card">
{# 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. #}
<h2 id="trimiteri-heading" class="sr-only">Trimiterile tale</h2>
{# 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 @@
<input type="hidden" id="f-status" name="status" value="{{ status_filtru | default('', true) }}">
{# Pagina curenta — actualizata prin OOB swap din _submissions.html; inclusa la reincarcari. #}
<input type="hidden" id="f-page" name="page" value="1">
{# 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. #}
<input type="hidden" id="f-held" name="held" value="{{ f_held | default('') }}">
{# === STANGA: Quick-pills de data (preset interval) + buton Custom ===

View File

@@ -1,228 +1,235 @@
<div class="card" id="card-cont">
<h2 style="font-size:15px; margin:0 0 16px;">Contul meu</h2>
<!-- Sectiunea: Plan curent (US-006 PRD 5.17) -->
{% if plan_linie is defined %}
<div id="sectiune-plan" style="margin-bottom:20px; padding-bottom:20px; border-bottom:1px solid var(--line);">
<h3 style="font-size:var(--fs-sm); color:var(--muted); font-weight:500; margin:0 0 10px;
text-transform:uppercase; letter-spacing:.04em;">Plan curent</h3>
<div style="font-size:var(--fs-md); font-weight:600; margin-bottom:6px;
color:{% if plan_limita_atinsa|default(false) %}var(--err){% elif plan_warn|default(false) %}var(--warn){% else %}var(--ink){% endif %};">
{{ plan_linie }}
{# Pagina Setari: sub-taburi interne — Date firma / Credentiale RAR / Cheie API /
Cont (plan + parola). `activ_subtab` (server) alege panoul activ dupa fiecare
swap HTMX, ca re-randarea sa nu resete la primul tab. Comutarea intre taburi
fara server = JS delegat in base.html (#card-cont). #}
{% set _firma_ok = account_meta.name and account_meta.email and account_meta.cui %}
{% set _rar_ok = medii_disponibile and medii_disponibile|length > 0 %}
<div class="card" id="card-cont" data-activ-subtab="{{ activ_subtab|default('firma') }}">
<div class="setari-head">
<h2 class="setari-title">
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.6 4.6l2.1 2.1M17.3 17.3l2.1 2.1M4.6 19.4l2.1-2.1M17.3 6.7l2.1-2.1"/></svg>
Setari
</h2>
<p class="setari-sub">Datele firmei, credentialele RAR, cheia API si contul tau.</p>
</div>
{% if monthly_limit_val is defined and monthly_limit_val is not none and effective_tier_name|default('') == 'free' %}
<div style="font-size:var(--fs-sm); color:var(--muted); margin-bottom:8px;">
Planul Gratuit include {{ monthly_limit_val }} prestatii/luna prin dashboard-ul web.
{% if plan_limita_atinsa|default(false) %}
Limita lunara a fost atinsa — trimiterile noi sunt blocate pana la inceputul lunii urmatoare.
{% elif plan_warn|default(false) %}
Te apropii de limita lunara.
{% endif %}
<div class="setari-subtabs" role="tablist" aria-label="Sectiuni setari">
<button type="button" class="setari-subtab{{ ' is-active' if activ_subtab|default('firma') == 'firma' else '' }}" role="tab" data-subtab="firma"
aria-selected="{{ 'true' if activ_subtab|default('firma') == 'firma' else 'false' }}">
<span class="st-dot {{ 'ok' if _firma_ok else 'warn' }}" aria-hidden="true"></span>Date firma</button>
<button type="button" class="setari-subtab{{ ' is-active' if activ_subtab|default('firma') == 'rar' else '' }}" role="tab" data-subtab="rar"
aria-selected="{{ 'true' if activ_subtab|default('firma') == 'rar' else 'false' }}">
<span class="st-dot {{ 'ok' if _rar_ok else 'warn' }}" aria-hidden="true"></span>Credentiale RAR</button>
<button type="button" class="setari-subtab{{ ' is-active' if activ_subtab|default('firma') == 'api' else '' }}" role="tab" data-subtab="api"
aria-selected="{{ 'true' if activ_subtab|default('firma') == 'api' else 'false' }}">
<span class="st-dot {{ 'ok' if are_cheie else 'warn' }}" aria-hidden="true"></span>Cheie API</button>
<button type="button" class="setari-subtab{{ ' is-active' if activ_subtab|default('firma') == 'cont' else '' }}" role="tab" data-subtab="cont"
aria-selected="{{ 'true' if activ_subtab|default('firma') == 'cont' else 'false' }}">
<span class="st-dot info" aria-hidden="true"></span>Cont</button>
</div>
{% endif %}
<div style="font-size:var(--fs-sm); color:var(--muted); padding:8px 10px;
border:1px solid var(--line); border-radius:6px; margin-top:4px;">
Vrei sa treci pe Standard, Pro sau Premium?
Contacteaza-ne pentru alocare manuala — nu exista inca plata self-service.
<strong>Pro</strong> adauga import prin API; <strong>Standard</strong> si
<strong>Premium</strong> ridica limita de volum.
{# ============================ Sub-tab: Date firma ============================ #}
<div class="setari-pane" data-subpane="firma" role="tabpanel"
{% if activ_subtab|default('firma') != 'firma' %}hidden{% endif %}>
<div class="setari-zone-title">
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 21V4a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v17"/><path d="M15 9h4a1 1 0 0 1 1 1v11"/><path d="M2 21h20"/><path d="M8 7h3M8 11h3M8 15h3"/></svg>
Date firma
<span class="setari-badge {{ 'sb-ok' if _firma_ok else 'sb-warn' }}">{{ 'complet' if _firma_ok else 'incomplet' }}</span>
</div>
</div>
{% endif %}
<p class="setari-zone-desc">Apar pe declaratiile catre RAR si pe facturile de plan.</p>
<!-- Sectiunea: Date firma (US-002) -->
<div style="margin-bottom:20px; padding-bottom:20px; border-bottom:1px solid var(--line);">
<h3 style="font-size:13px; color:var(--muted); font-weight:500; margin:0 0 8px; text-transform:uppercase; letter-spacing:.04em;">Date firma</h3>
{% if date_firma_mesaj %}<div class="flash" style="margin-bottom:12px;">{{ date_firma_mesaj }}</div>{% endif %}
{% if date_firma_eroare %}<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ date_firma_eroare }}</div>{% endif %}
{% if date_firma_mesaj %}
<div class="flash" style="margin-bottom:12px;">{{ date_firma_mesaj }}</div>
{% endif %}
{% if date_firma_eroare %}
<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ date_firma_eroare }}</div>
{% endif %}
<form hx-post="/cont/date-firma"
hx-target="#card-cont"
hx-swap="outerHTML">
<form hx-post="/cont/date-firma" hx-target="#card-cont" hx-swap="outerHTML">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<p style="margin:0 0 8px;">
<label style="font-size:13px; color:var(--muted);">Companie</label><br>
<input type="text" name="companie" required
value="{{ account_meta.name or '' }}"
style="width:100%; max-width:340px;"
placeholder="Numele firmei (ex. Service Auto SRL)">
<input type="text" name="companie" required value="{{ account_meta.name or '' }}"
style="width:100%; max-width:340px;" placeholder="Numele firmei (ex. Service Auto SRL)">
</p>
<p style="margin:0 0 8px;">
<label style="font-size:13px; color:var(--muted);">Email contact</label><br>
<input type="email" name="email" required
value="{{ account_meta.email or '' }}"
style="width:100%; max-width:340px;"
placeholder="contact@firma.ro">
<input type="email" name="email" required value="{{ account_meta.email or '' }}"
style="width:100%; max-width:340px;" placeholder="contact@firma.ro">
</p>
<p style="margin:0 0 12px;">
<label style="font-size:13px; color:var(--muted);">CUI (cod unic de identificare)</label><br>
<input type="text" name="cui" required
value="{{ account_meta.cui or '' }}"
style="width:100%; max-width:340px;"
placeholder="RO12345678">
<input type="text" name="cui" required value="{{ account_meta.cui or '' }}"
style="width:100%; max-width:340px;" placeholder="RO12345678">
</p>
<button type="submit">Salveaza datele firmei</button>
</form>
</div>
<!-- Sectiunea: Cheia mea API -->
<div style="margin-bottom:20px; padding-bottom:20px; border-bottom:1px solid var(--line);">
<h3 style="font-size:13px; color:var(--muted); font-weight:500; margin:0 0 8px; text-transform:uppercase; letter-spacing:.04em;">Cheia mea API</h3>
{% if api_key %}
<div class="flash" style="margin-bottom:12px;">Cheia a fost rotita. Salveaz-o acum — nu o vei mai putea vedea.</div>
<div class="card" style="font-family:monospace; word-break:break-all; font-size:14px; background:#0f1115; margin:0 0 8px;">
{{ api_key }}
{# ========================= Sub-tab: Credentiale RAR ========================= #}
<div class="setari-pane" data-subpane="rar" role="tabpanel"
{% if activ_subtab|default('firma') != 'rar' %}hidden{% endif %}>
<div class="setari-zone-title">
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 11l1.5-4A2 2 0 0 1 8.4 5.6h7.2A2 2 0 0 1 17.5 7L19 11"/><path d="M4 11h16v6H4z"/><path d="M6 17v2M18 17v2"/><circle cx="7.5" cy="14" r="1"/><circle cx="16.5" cy="14" r="1"/></svg>
Credentiale RAR (portal AUTOPASS)
<span class="setari-badge {{ 'sb-ok' if _rar_ok else 'sb-warn' }}">{{ 'configurat' if _rar_ok else 'neconfigurat' }}</span>
</div>
<button type="button"
data-key="{{ api_key }}"
onclick="navigator.clipboard.writeText(this.dataset.key).then(()=>this.textContent='Copiat!')">
Copiaza cheia
</button>
<p style="font-size:13px; color:var(--warn); margin:10px 0 0;">
Atentie: la urmatoarea vizita aceasta cheie dispare. Daca o pierzi, roteste din nou.
</p>
{% endif %}
{% if rot_eroare %}
<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ rot_eroare }}</div>
{% endif %}
<form hx-post="/cont/roteste-cheie"
hx-target="#card-cont"
hx-swap="outerHTML"
style="margin-top:{% if api_key %}12px{% else %}0{% endif %};">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" style="background:var(--card); color:var(--warn); border-color:var(--warn);">
Roteste cheia API
</button>
<span style="font-size:12px; color:var(--muted); margin-left:8px;">Cheia veche se revoca imediat.</span>
</form>
</div>
<!-- Sectiunea: Credentiale RAR -->
<div>
<h3 style="font-size:13px; color:var(--muted); font-weight:500; margin:0 0 12px; text-transform:uppercase; letter-spacing:.04em;">Credentiale RAR (portal AUTOPASS)</h3>
<p class="muted" style="font-size:12px; margin:0 0 12px; line-height:1.5;">
Configureaza mai jos mediile RAR — Testare si/sau Productie — fiecare cu credentialele
lui, validate separat la salvare pe sistemul RAR corespunzator. Un mediu devine activ
cand e bifat si are credentiale; abia atunci importul web poate trimite pe el. Fara
niciun mediu activ, importul web nu are credentiale si trimiterile esueaza. (Canalul API
ramane pe mediul global al instantei, {{ mediu_instanta() }}, pentru ca isi cara singur
credentialele.)
<p class="setari-zone-desc">
Configureaza mediile RAR — Testare si/sau Productie — fiecare cu credentialele lui,
validate separat la salvare pe sistemul RAR corespunzator. Un mediu devine activ cand e
bifat si are credentiale; abia atunci importul web poate trimite pe el. (Canalul API ramane
pe mediul global al instantei, {{ mediu_instanta() }}, pentru ca isi cara singur credentialele.)
</p>
{% if creds_mesaj %}
<div class="flash" style="margin-bottom:12px;">{{ creds_mesaj }}</div>
{% endif %}
{% if creds_eroare %}
<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ creds_eroare }}</div>
{% endif %}
{% if creds_mesaj %}<div class="flash" style="margin-bottom:12px;">{{ creds_mesaj }}</div>{% endif %}
{% if creds_eroare %}<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ creds_eroare }}</div>{% endif %}
<form hx-post="/cont/rar-medii"
hx-target="#card-cont"
hx-swap="outerHTML">
<form hx-post="/cont/rar-medii" hx-target="#card-cont" hx-swap="outerHTML">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<!-- Subsectiunea: Testare -->
<div style="margin-bottom:16px; padding-bottom:16px; border-bottom:1px solid var(--line);">
<p style="margin:0 0 6px;">
<label style="font-size:13px; color:var(--muted); display:flex; align-items:center; gap:6px;">
<div class="env-cards">
<!-- Testare -->
<div class="env-card{% if test_disponibil %} active-test{% endif %}">
<h4>Testare
{% if test_disponibil %}<span class="setari-badge sb-ok">configurat</span>{% else %}<span class="setari-badge sb-info">inactiv</span>{% endif %}
</h4>
<label class="env-sw">
<input type="checkbox" name="test_enabled" value="1" {% if test_enabled %}checked{% endif %}>
Activare Testare
</label>
{% if test_disponibil %}
<span style="font-size:12px; color:var(--ok);">configurat</span>
{% endif %}
</p>
<p style="margin:0 0 8px;">
<label style="font-size:13px; color:var(--muted);">Email RAR Testare</label><br>
<input type="email" name="test_email" style="width:100%; max-width:340px;"
placeholder="email@service.ro">
</p>
<p style="margin:0 0 8px;">
<label style="font-size:13px; color:var(--muted);">Parola RAR Testare</label><br>
<input type="password" name="test_parola" style="width:100%; max-width:340px;"
autocomplete="new-password">
</p>
{% if creds_test_mesaj %}
<div class="flash" style="margin-top:6px;">{{ creds_test_mesaj }}</div>
{% endif %}
{% if creds_test_eroare %}
<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_test_eroare }}</div>
{% endif %}
<label class="env-lbl">Email RAR Testare</label>
<input type="email" name="test_email" placeholder="email@service.ro">
<label class="env-lbl">Parola RAR Testare</label>
<input type="password" name="test_parola" autocomplete="new-password">
{% if creds_test_mesaj %}<div class="flash" style="margin-top:6px;">{{ creds_test_mesaj }}</div>{% endif %}
{% if creds_test_eroare %}<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_test_eroare }}</div>{% endif %}
</div>
<!-- Subsectiunea: Productie -->
<div style="margin-bottom:16px; padding-bottom:16px; border-bottom:1px solid var(--line);">
<p style="margin:0 0 6px;">
<label style="font-size:13px; color:var(--muted); display:flex; align-items:center; gap:6px;">
<!-- Productie -->
<div class="env-card{% if not prod_disponibil %} inactive{% endif %}">
<h4>Productie
{% if prod_disponibil %}<span class="setari-badge sb-ok">configurat</span>{% else %}<span class="setari-badge sb-warn">inactiv</span>{% endif %}
</h4>
<label class="env-sw">
<input type="checkbox" name="prod_enabled" value="1" {% if prod_enabled %}checked{% endif %}>
Activare Productie
</label>
{% if prod_disponibil %}
<span style="font-size:12px; color:var(--ok);">configurat</span>
{% endif %}
</p>
<p style="margin:0 0 8px;">
<label style="font-size:13px; color:var(--muted);">Email RAR Productie</label><br>
<input type="email" name="prod_email" style="width:100%; max-width:340px;"
placeholder="email@service.ro">
</p>
<p style="margin:0 0 8px;">
<label style="font-size:13px; color:var(--muted);">Parola RAR Productie</label><br>
<input type="password" name="prod_parola" style="width:100%; max-width:340px;"
autocomplete="new-password">
</p>
<p style="margin:0 0 8px;">
<label style="font-size:13px; color:var(--muted); display:flex; align-items:flex-start; gap:6px;">
<input type="checkbox" name="prod_confirmare" value="1" style="margin-top:2px; flex-shrink:0;">
Inteleg ca trimiterile pe Productie sunt declaratii reale (L.142), finale si fara anulare.
<label class="env-lbl">Email RAR Productie</label>
<input type="email" name="prod_email" placeholder="email@service.ro">
<label class="env-lbl">Parola RAR Productie</label>
<input type="password" name="prod_parola" autocomplete="new-password">
<label class="env-confirm">
<input type="checkbox" name="prod_confirmare" value="1">
<span>Inteleg ca trimiterile pe Productie sunt declaratii reale (L.142), finale si fara anulare.</span>
</label>
</p>
{% if creds_prod_mesaj %}
<div class="flash" style="margin-top:6px;">{{ creds_prod_mesaj }}</div>
{% endif %}
{% if creds_prod_eroare %}
<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_prod_eroare }}</div>
{% endif %}
{% if creds_prod_mesaj %}<div class="flash" style="margin-top:6px;">{{ creds_prod_mesaj }}</div>{% endif %}
{% if creds_prod_eroare %}<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_prod_eroare }}</div>{% endif %}
</div>
</div>
<!-- Selector mediu implicit -->
<div style="margin-bottom:16px;">
<label style="font-size:13px; color:var(--muted);">Mediu implicit pentru trimiteri</label><br>
<div class="default-env">
<label style="font-size:13px; color:var(--muted);">Mediu implicit pentru trimiteri</label>
{% if medii_disponibile %}
<select name="rar_env_default" style="width:100%; max-width:340px; margin-top:4px;">
<select name="rar_env_default">
{% for env in medii_disponibile %}
<option value="{{ env }}"{% if env == rar_env_default %} selected{% endif %}>{{ "Testare" if env == "test" else "Productie" }}</option>
{% endfor %}
</select>
{% else %}
<p style="font-size:13px; color:var(--muted); margin:4px 0 0;">Activeaza si valideaza un mediu intai.</p>
{% endif %}
{% if creds_default_mesaj %}
<div class="flash" style="margin-top:6px;">{{ creds_default_mesaj }}</div>
{% endif %}
{% if creds_default_eroare %}
<div class="banner" style="margin-top:6px; padding:8px 12px;">{{ creds_default_eroare }}</div>
<span style="font-size:13px; color:var(--muted);">Activeaza si valideaza un mediu intai.</span>
{% endif %}
{% if creds_default_mesaj %}<div class="flash" style="margin-top:6px; flex-basis:100%;">{{ creds_default_mesaj }}</div>{% endif %}
{% if creds_default_eroare %}<div class="banner" style="margin-top:6px; padding:8px 12px; flex-basis:100%;">{{ creds_default_eroare }}</div>{% endif %}
</div>
<button type="submit">Salveaza mediile RAR</button>
<span style="font-size:12px; color:var(--muted); margin-left:8px;">Parolele stocate criptat, niciodata in clar.</span>
</form>
</div>
{# ============================ Sub-tab: Cheie API ============================ #}
<div class="setari-pane" data-subpane="api" role="tabpanel"
{% if activ_subtab|default('firma') != 'api' %}hidden{% endif %}>
<div class="setari-zone-title">
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="8" cy="15" r="4"/><path d="M11 12l9-9"/><path d="M17 6l2.5 2.5M14 9l2.5 2.5"/></svg>
Cheie API
<span class="setari-badge {{ 'sb-ok' if are_cheie else 'sb-warn' }}">{{ 'activa' if are_cheie else 'lipsa' }}</span>
</div>
<p class="setari-zone-desc">
O foloseste softul propriu / ROAAUTO ca sa trimita prezentari prin API. Se vede integral o
singura data, la creare sau rotire.
</p>
{% if api_key %}
<div class="flash" style="margin-bottom:12px;">Cheia a fost rotita. Salveaz-o acum — nu o vei mai putea vedea.</div>
<div class="keybox">{{ api_key }}</div>
<button type="button" class="btn-secondary" style="margin-top:8px;"
data-key="{{ api_key }}"
onclick="navigator.clipboard.writeText(this.dataset.key).then(()=>this.textContent='Copiat!')">
Copiaza cheia
</button>
<p style="font-size:13px; color:var(--warn); margin:10px 0 0;">
Atentie: la urmatoarea vizita aceasta cheie dispare. Daca o pierzi, roteste din nou.
</p>
{% endif %}
{% if rot_eroare %}<div class="banner" style="margin:12px 0; padding:8px 12px;">{{ rot_eroare }}</div>{% endif %}
<form hx-post="/cont/roteste-cheie" hx-target="#card-cont" hx-swap="outerHTML"
style="margin-top:{% if api_key %}14px{% else %}0{% endif %};">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn-warn">{{ 'Roteste cheia API' if are_cheie else 'Genereaza cheia API' }}</button>
<span style="font-size:12px; color:var(--muted); margin-left:8px;">Cheia veche se revoca imediat.</span>
</form>
</div>
{# ========================= Sub-tab: Cont (plan + parola) ==================== #}
<div class="setari-pane" data-subpane="cont" role="tabpanel"
{% if activ_subtab|default('firma') != 'cont' %}hidden{% endif %}>
{# --- Plan curent --- #}
{% if plan_linie is defined %}
<div class="setari-zone-title">
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3 3 8l9 5 9-5z"/><path d="M3 13l9 5 9-5"/></svg>
Plan curent
<span class="setari-badge sb-info">{{ tier_label|default('Gratuit') }}</span>
</div>
<div style="font-size:var(--fs-md); font-weight:600; margin-bottom:6px;
color:{% if plan_limita_atinsa|default(false) %}var(--err){% elif plan_warn|default(false) %}var(--warn){% else %}var(--ink){% endif %};">
{{ plan_linie }}
</div>
{% if plan_limita_atinsa|default(false) %}
<p class="setari-zone-desc" style="color:var(--err);">Limita lunara a fost atinsa — trimiterile noi sunt blocate pana la inceputul lunii urmatoare.</p>
{% elif plan_warn|default(false) %}
<p class="setari-zone-desc" style="color:var(--warn);">Te apropii de limita lunara.</p>
{% endif %}
<p class="setari-zone-desc">
Pentru mai mult volum sau import prin API, <a href="mailto:contact@romfast.ro">contacteaza-ne</a> — alocarea planurilor se face manual.
</p>
<hr class="setari-subzone-sep">
{% endif %}
{# --- Parola de acces --- #}
<div class="setari-zone-title">
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/><circle cx="12" cy="15.5" r="1.2"/></svg>
Parola de acces
</div>
<p class="setari-zone-desc">Parola cu care te autentifici in dashboard (separata de credentialele RAR). Minim 10 caractere.</p>
{% if parola_mesaj %}<div class="flash" style="margin-bottom:12px;">{{ parola_mesaj }}</div>{% endif %}
{% if parola_eroare %}<div class="banner" style="margin-bottom:12px; padding:8px 12px;">{{ parola_eroare }}</div>{% endif %}
<form hx-post="/cont/schimba-parola" hx-target="#card-cont" hx-swap="outerHTML">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<p style="margin:0 0 8px;">
<label style="font-size:13px; color:var(--muted);">Parola actuala</label><br>
<input type="password" name="parola_actuala" autocomplete="current-password" style="width:100%; max-width:340px;">
</p>
<p style="margin:0 0 8px;">
<label style="font-size:13px; color:var(--muted);">Parola noua</label><br>
<input type="password" name="parola_noua" autocomplete="new-password" style="width:100%; max-width:340px;">
</p>
<p style="margin:0 0 12px;">
<label style="font-size:13px; color:var(--muted);">Confirma parola noua</label><br>
<input type="password" name="parola_confirma" autocomplete="new-password" style="width:100%; max-width:340px;">
</p>
<button type="submit">Schimba parola</button>
</form>
</div>
</div>

View File

@@ -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 @@
</form>
{% 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. #}
<form hx-post="/_import/{{ import_id }}/rand/{{ row_index }}/confirma-review"

View File

@@ -1,6 +1,6 @@
{# _form_editare.html — partial partajat slim: campurile vehicul/data/odo + obs + chips prestatii.
US-007 (PRD 5.15): redesign slim cu VIN unic, Observatii textarea, chips prestatii (E4),
si reveal dinamic odometru initial cand chips contin R-ODO/I-ODO (D10c, E6 server-driven).
Redesign slim cu VIN unic, Observatii textarea, chips prestatii, si reveal dinamic
odometru initial cand chips contin R-ODO/I-ODO (server-driven).
Inclus cu {% include "_form_editare.html" %} INSIDE un <form> 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) }}
</div>
{# === 3. Observatii (obs) — textarea liber, US-005 === #}
{# === 3. Observatii (obs) — textarea liber === #}
<div class="camp-slim">
<label for="c-obs">Observatii (operatiile efectuate)</label>
<textarea id="c-obs" name="obs" rows="2"
@@ -44,7 +44,7 @@
placeholder="ex: Revizie; schimbare placute frana">{{ obs_val or '' }}</textarea>
</div>
{# === 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" %}

View File

@@ -14,7 +14,7 @@
{% else %}
Lipseste o cheie API activa.
{% endif %}
Mergi la <a href="/?tab=cont">Meniu cont &rsaquo; Cont</a> pentru a le configura.
Mergi la <a href="/?tab=cont">Meniu cont &rsaquo; Setari</a> pentru a le configura.
</div>
{% endif %}
@@ -28,7 +28,7 @@
</div>
<p class="muted" style="font-size:12px; margin:10px 0 0;">
Cererile trimit doar cheia API + datele prezentarii. Credentialele RAR se configureaza
o data in <a href="/?tab=cont">Cont</a> si sunt folosite automat la trimitere. Optional,
o data in <a href="/?tab=cont">Setari</a> si sunt folosite automat la trimitere. Optional,
poti include <code>rar_credentials</code> in payload ca sa le suprascrii pe acea cerere.
</p>
</div>
@@ -216,7 +216,7 @@
</div>
</div>
{# Card: De ce nu ajunge la RAR? (PRD 5.19 US-010 — onestitate canal API) #}
{# Card: De ce nu ajunge la RAR? (onestitate canal API) #}
{# Un POST /v1/prezentari poate intoarce 200 dar randul sa NU plece la RAR. #}
<div class="card" style="margin-bottom:16px;">
<h3 style="margin:0 0 6px; font-size:15px;">De ce nu ajunge la RAR?</h3>
@@ -292,7 +292,7 @@
<div id="integrare-test-rezultat" style="margin-top:8px;"></div>
</div>
{# Formular test credentiale RAR (US-007, PRD 5.20) #}
{# Formular test credentiale RAR #}
{# Login de proba pe mediul ales — fara efecte secundare, nu salveaza nimic. #}
{# Banner-ul de rezultat include eticheta mediului ("pe TESTARE" / "pe PRODUCTIE"). #}
<div class="card" style="margin-bottom:16px;">

View File

@@ -10,7 +10,7 @@
{% else %}
<span class="muted" style="font-size:12px;">doar evenimentele contului tau</span>
{% 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) %}
<span class="{{ _eb[1] }}" style="margin-left:auto;" title="Mediu RAR activ">{{ _eb[0] }}</span>

View File

@@ -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). #}
<div {% if slim %}class="camp-slim"{% else %}style="margin-bottom:10px;"{% endif %}>
<label for="{{ id_prefix }}-{{ nome }}"{% if not slim %} class="muted" style="font-size:12px; display:block;"{% endif %}>{{ eticheta }}</label>
{% 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 @@
</div>
{% 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). #}

View File

@@ -54,7 +54,7 @@
<div class="muted">{{ e.denumire or '(fara denumire)' }}</div>
</td>
<td class="muted" style="font-size:12px;" data-eticheta="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' %}

View File

@@ -82,7 +82,7 @@
</span>
</div>
{# Tabel mapare: coloana din fisier | exemplu | camp RAR (mockup 5.16 / US-013) #}
{# Tabel mapare: coloana din fisier | exemplu | camp RAR #}
<div class="tablewrap" style="margin-bottom:16px;">
<table style="border-collapse:collapse; width:100%;">
<thead>

View File

@@ -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 /

View File

@@ -14,7 +14,7 @@
Preview —
<span class="muted" style="font-weight:400;">{{ filename or ("import #" ~ import_id) }}</span>
</h2>
{# Badge mediu RAR (US-009): vizibil intotdeauna in preview (claritate tinta) #}
{# Badge mediu RAR: vizibil intotdeauna in preview (claritate tinta) #}
{% if rar_env %}
<span class="pill" style="font-size:var(--fs-xs);
{% if rar_env == 'prod' %}background:color-mix(in srgb,#B4452F 15%,var(--card)); border-color:#B4452F; color:#B4452F; font-weight:700;
@@ -71,7 +71,7 @@
</div>
<!-- Panou inline: operatii fara cod RAR, mapabile in flux (fara re-upload).
US-004: un singur <form> cu un select per operatie + un singur buton Salveaza. -->
Un singur <form> cu un select per operatie + un singur buton Salveaza. -->
{% if unmapped_ops %}
<div class="card" style="border-color:var(--err); background:color-mix(in srgb, var(--err) 12%, var(--card)); margin-bottom:14px;">
<h3 style="font-size:14px; margin:0 0 6px;">Operatii de mapat la cod RAR</h3>
@@ -98,7 +98,7 @@
{% if e.suggestions or e.sugestie_principala or (e.surse_sugestie and e.surse_sugestie.nul) %}
<div class="muted" style="font-size:12px; margin-top:4px;">
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 @@
</div>
{% endif %}
<!-- Banner discoverability (T1, US-007): vizibil cand exista randuri needs_review.
<!-- Banner discoverability: vizibil cand exista randuri needs_review.
Explica operatorului ca randurile cu 'Verifica valori' nu pleaca la RAR
pana le deschide in modal si apasa 'Confirma valorile'. Dispare via OOB
cand summary.needs_review == 0. -->
@@ -213,7 +213,7 @@
hx-target="#import-section"
hx-swap="outerHTML">
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
{# Mediu RAR ales la upload — propagar la commit (US-009) #}
{# Mediu RAR ales la upload — propagar la commit #}
<input type="hidden" name="rar_env" value="{{ rar_env or '' }}">
<div class="sticky-bar">
<div style="flex:1; min-width:280px;">
@@ -285,7 +285,7 @@ din <span id="n-hint-ok">{{ summary.get('ok', 0) }}</span> gata de trimis
}
/* Actualizeaza N dupa editare/confirmare rand (OOB).
US-007: reviewed_rows (checkboxe) eliminate; N = randurile ok din DB,
reviewed_rows (checkboxe) eliminate; N = randurile ok din DB,
actualizate via OOB (#preview-ok-count[data-ok]) dupa /confirma-review sau /editeaza. */
function updateN() {
var total = getOk();

View File

@@ -1,7 +1,7 @@
{#
_preview_rand.html — un singur rand de preview import.
US-006 (PRD 5.12): editarea inline (tr.preview-edit + mutual-exclusion script)
a fost eliminata. Butonul Editeaza deschide MODALUL global (#detaliu-modal-body).
Editarea inline (tr.preview-edit + mutual-exclusion script) a fost eliminata.
Butonul Editeaza deschide MODALUL global (#detaliu-modal-body).
Parametri:
editing — ELIMINAT (ignorat, pastrat pentru compatibilitate apeluri vechi)
@@ -39,7 +39,7 @@
<span class="pill {{ row.stare_css }}" style="display:inline-flex; align-items:center; gap:5px;"
{% if _nota %}title="{{ _nota }}"{% endif %}>
<span aria-hidden="true" style="display:inline-block; width:7px; height:7px; border-radius:99px; background:currentColor; flex-shrink:0;"></span>{{ row.stare_eticheta }}</span>
{# Badge mediu RAR (US-010 PRD 5.20) — rar_env disponibil din contextul batch-ului. #}
{# Badge mediu RAR — rar_env disponibil din contextul batch-ului. #}
{% if rar_env | default('') %}
{% set _eb = eticheta_env(rar_env) %}
<div style="margin-top:3px;"><span class="{{ _eb[1] }}">{{ _eb[0] }}</span></div>

View File

@@ -14,10 +14,10 @@
</div>
{% endif %}
{# === US-007 (PRD 5.19): banner coada tinuta imbatranita — mitigare OBLIGATORIE pt. default OFF.
{# Banner coada tinuta imbatranita — mitigare pt. default OFF.
Cand exista randuri `queued AND held=1` mai vechi de held_aged_days, semnalul de conformitate
L.142 (declarare obligatorie). Derivat (zero stare noua), varsta pe created_at.
Prioritate bannere (D7 — un-singur-banner "notice", ca sa nu impinga contoarele sub fold pe
Prioritate bannere (un-singur-banner "notice", ca sa nu impinga contoarele sub fold pe
mobil): cont inactiv (sus) + strip RAR jos raman separate; intre notice-uri, held-aged >
trial (risc legal > notita trial) -> trial e suprimat cand held-aged e activ. #}
{% set _held_aged_activ = (held_aged_count|default(0) > 0) and account_active %}
@@ -40,7 +40,7 @@
</div>
{% 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 @@
<script>(function(){ if(sessionStorage.getItem('tfx')){ var el=document.getElementById('banner-trial-expirat'); if(el) el.style.display='none'; } })();</script>
{% 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 @@
</div>
{% 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 @@
<div class="contor-label">Azi</div>
</div>
{# 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). #}
<div class="contor-card" style="flex:1; min-width:80px;">
<div class="contor-cifra s-queued">{{ counts_queued }}</div>
@@ -156,11 +156,11 @@
</a>
</div>
{# === 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') %}
<nav class="status-nav" aria-label="Navigatie rapida"
@@ -207,7 +207,7 @@
class="status-nav-link{% if _tab == 'mapari' %} status-nav-activ{% endif %}">Mapari{% if mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:16px; height:16px; margin-left:4px; padding:0 4px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ mapari_badge }}</span>{% endif %}</a>
</nav>
{# 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). #}

View File

@@ -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).

View File

@@ -6,7 +6,7 @@
<input type="hidden" id="f-page" name="page" value="{{ page | default(1) }}" hx-swap-oob="true">
{# 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. #}
<input type="hidden" id="f-held" name="held" value="{{ f_held | default('') }}" hx-swap-oob="true">
{# OOB: banner bulk "Trimite toate (N)" re-randat la fiecare reincarcare a listei (fix staleness). #}
@@ -20,7 +20,7 @@
<span id="trimiteri-versiune" data-v="{{ versiune_trimiteri | default('') }}" hidden></span>
{% 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. #}
<div class="bulk-message" role="status" aria-live="polite"
style="font-size:13px; color:var(--ink); background:var(--card2);
border:1px solid var(--line); border-radius:6px;
@@ -29,8 +29,8 @@
</div>
{% endif %}
{# US-007 (5.19): chip filtru activ "doar tinute" cu buton de golire (fix /code-review #2 —
filtrul held e sticky prin OOB, deci ii dam un off-switch explicit). #}
{# Chip filtru activ "doar tinute" cu buton de golire — filtrul held e sticky
prin OOB, deci ii dam un off-switch explicit. #}
{% if f_held %}
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px; font-size:var(--fs-sm);">
<span style="display:inline-flex; align-items:center; gap:6px; padding:2px 10px; border-radius:99px;
@@ -47,7 +47,7 @@
{% endif %}
{% if rows %}
{# Form bulk cu DOUA actiuni: (1) aplica cod RAR la selectate (bulk-fix, US-010),
{# Form bulk cu DOUA actiuni: (1) aplica cod RAR la selectate (bulk-fix),
(2) sterge selectate (sterge-bulk). Selectia opereaza DOAR pe randuri blocate
(gestionabil); sent/sending/queued nu au checkbox (read-only).
Butonul "Aplica cod" foloseste hx-post propriu (override form action).
@@ -62,7 +62,7 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div style="display:flex; justify-content:flex-end; align-items:center;
gap:6px; margin-bottom:8px; flex-wrap:wrap;">
{# Bulk-fix: input cod + buton aplica (US-010 PRD 5.15) #}
{# Bulk-fix: input cod + buton aplica #}
<input type="text" name="cod_prestatie" id="bulk-fix-cod"
placeholder="Cod RAR (ex: OE-1)"
autocomplete="off" autocapitalize="characters"
@@ -89,7 +89,7 @@
</button>
</div>
{# 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 %}
</div>
{# 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 #}
<div style="flex:0 0 auto; display:flex; flex-direction:column; align-items:flex-end; gap:3px; white-space:nowrap;">
<span class="pill {{ r.stare_css }}" title="{{ r.stare_text }}">{{ r.stare_scurt }}</span>
{% if r.rar_env %}
{% set _eb = eticheta_env(r.rar_env) %}
<span class="{{ _eb[1] }}">{{ _eb[0] }}</span>
{% 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 %}

View File

@@ -7,7 +7,7 @@
{% set cod_afis = prez.cod_rar if (prez.cod_rar and prez.cod_rar != '—') else 'nemapat' %}
<div class="card" id="detaliu-card-{{ id }}" style="border:none; padding:0; margin:0;">
{# === Header — #id + pill stare + badge mediu RAR (US-010 PRD 5.20) + motiv === #}
{# === Header — #id + pill stare + badge mediu RAR + motiv === #}
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:0 0 8px;">
<h2 id="detaliu-modal-titlu" style="font-size:15px; margin:0;">Detaliu trimitere #{{ id }}</h2>
<span class="pill {{ stare_css }}">{{ stare_text }}</span>
@@ -110,14 +110,13 @@
hx-disabled-elt="find button">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
{# Cleanup B (US-009 PRD 5.15): vechiul <select name="cod_prestatie"> eliminat.
Chips din _form_editare.html (via _chips_prestatii.html) il inlocuiesc complet:
emit hidden inputs name="cod_prestatie" + picker per-operatie (E4, US-007).
{# Vechiul <select name="cod_prestatie"> 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 %}
<div style="margin:0 0 12px;">
<div class="muted" style="font-size:12px;">Operatie service</div>
@@ -125,7 +124,7 @@
</div>
{% 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" %}
</form>
{% else %}
@@ -150,7 +149,7 @@
</div>
{% 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 %}

View File

@@ -32,7 +32,7 @@
hx-indicator="#upload-spinner">
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
{# Indicator mediu RAR (US-009, PRD 5.20): vizibil inainte de drop-zone #}
{# Indicator mediu RAR: vizibil inainte de drop-zone #}
{% set medii_rar = medii | default([]) %}
{% if medii_rar | length == 0 %}
{# Banner avertisment — non-blocant (upload continua; commit foloseste ancora globala) #}
@@ -41,7 +41,7 @@
border:1px solid var(--warn, #e6b34a); font-size:13px;" role="note">
<strong>Niciun mediu RAR activ.</strong>
Activeaza mediul de Productie sau Testare in
<a href="?tab=cont" style="color:var(--accent);">Meniu cont &rsaquo; Cont</a>.
<a href="?tab=cont" style="color:var(--accent);">Meniu cont &rsaquo; Setari</a>.
</div>
{% elif medii_rar | length == 1 %}
{# Eticheta statica (un singur mediu disponibil) #}

View File

@@ -36,8 +36,8 @@
})();
</script>
<style>
/* US-001 PRD 5.16: stive de font standard web. Toate regulile font-face IBM Plex sterse.
Motiv: decizie user (risc AI-Slop #11 acceptat constient), uniformitate cross-page.
/* Stive de font standard web. Toate regulile font-face IBM Plex sterse.
Motiv: decizie user, uniformitate cross-page.
Fisierele woff2 raman pe disc (curatare = follow-up optional, non-blocant).
Referinte catre directorul de fonturi statice eliminate — font-ui si font-mono sunt stive sistem. */
/* Paleta dark (default) — accent azur ROMFAST.
@@ -45,10 +45,10 @@
--line2: separator subtire (intre --bg si --line). */
:root { --bg:#0f1218; --card:#181c24; --card2:#0f1218; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36; --line2:#1f2530;
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#2E74D6;
/* US-001 (PRD 5.16): stive font standard web — sursa unica de adevar */
/* Stive font standard web — sursa unica de adevar */
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Segoe UI Mono", "Roboto Mono", Menlo, Consolas, monospace;
/* US-002 (PRD 5.16): scala tipografica uniforma — sursa unica de adevar */
/* Scala tipografica uniforma — sursa unica de adevar */
--fs-xs:12px; --fs-sm:13.5px; --fs-base:15px; --fs-md:16px;
--fs-lg:18px; --fs-xl:20px; --fs-2xl:28px; --fs-3xl:34px;
--lh-tight:1.25; --lh-body:1.55; }
@@ -60,7 +60,7 @@
[data-theme="petrol"] { --bg:#0e1416; --card:#161e20; --card2:#0e1416; --ink:#e6e9ef; --muted:#8b93a7; --line:#232c2e; --line2:#1c2426;
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#0E7C7B; }
/* Paleta Grafit — similara cu dark, accent azur deschis (#6ea2ec = landing --infot).
Distinta de dark la cererea userului (D2 PRD 5.15). */
Distinta de dark la cererea userului. */
[data-theme="grafit"] { --bg:#0f1218; --card:#181c24; --card2:#0f1218; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36; --line2:#1f2530;
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#6ea2ec; }
/* Paleta Cobalt — fundal bleumarin adanc, accent albastru viu (#8aa0ff = landing --infot). */
@@ -70,7 +70,7 @@
[data-theme="cupru"] { --bg:#15110b; --card:#211a12; --card2:#15110b; --ink:#efe6d6; --muted:#a89a85; --line:#36291c; --line2:#281e14;
--ok:#67b98c; --warn:#c97d2e; --err:#e2685a; --accent:#dfa45c; }
/* Paleta Hartie — fundal crem cald, accent albastru clasic (#1F5FBF = landing --infot = --accent).
Similara cu light, distinta la cererea userului (D2 PRD 5.15). */
Similara cu light, distinta la cererea userului. */
[data-theme="hartie"] { --bg:#f3efe6; --card:#fffdf7; --card2:#f3efe6; --ink:#1e1a13; --muted:#6a6052; --line:#e2dccc; --line2:#ece6d9;
--ok:#1c7d5d; --warn:#b45309; --err:#bd463c; --accent:#1F5FBF; }
* { box-sizing:border-box; }
@@ -104,7 +104,7 @@
th { color:var(--muted); font-weight:500; font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
.empty { color:var(--muted); padding:24px; text-align:center; }
.pill { font-size:12px; padding:2px 8px; border-radius:99px; border:1px solid var(--line); }
/* Badge sursa sugestie (5.18 US-007): de unde vine sugestia de cod in editorul de mapare.
/* Badge sursa sugestie: de unde vine sugestia de cod in editorul de mapare.
confirmat = GOLD validat de om (verde); similar = SILVER/embedding k-NN (azur);
non-operatie = pre-filtru NUL / vecin NUL (gri-cald). Suggestion-only, doar indiciu vizual. */
.sugg-sursa { display:inline-block; font-size:10px; font-weight:700; line-height:1; padding:2px 6px;
@@ -142,7 +142,7 @@
.s-ok{color:var(--ok);}
.s-needs_review{color:var(--warn);}
.s-already_sent,.s-duplicate_in_file{color:var(--muted);}
/* Badge mediu RAR (US-010 PRD 5.20) — semantica risc L.142.
/* Badge mediu RAR — semantica risc L.142.
Productie: fill atentie (ton --err), text alb — declaratie reala, ireversibila.
Testare: outline discret, ton --muted — mediu de proba, low-stakes.
NU se foloseste verde pentru Productie (ar semnala "sigur"). */
@@ -167,7 +167,7 @@
flex-wrap:wrap; z-index:10; }
/* Indicator HTMX — ascuns pana la request */
.htmx-indicator { display:none; }
/* US-011 (PRD 5.16): selector tema stil pill — icon + eticheta temei curente.
/* Selector tema stil pill — icon + eticheta temei curente.
Eticheta se ascunde pe <=560px (spatiu ingust), ramane iconita. */
.tema-btn { display:inline-flex; align-items:center; gap:6px; height:36px; padding:0 12px;
border-radius:8px; background:transparent; border:1px solid var(--line);
@@ -176,7 +176,7 @@
.tema-btn:hover { border-color:var(--accent); color:var(--ink); }
.tema-btn:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
@media (max-width:560px) { #tema-label { display:none; } }
/* US-003 (PRD 5.16): dot RAR compact in antet.
/* Dot RAR compact in antet.
Stare OK: dot verde pulsant + "RAR online". Stare BLOCAT: dot rosu.
Stilat ca pill; sensul NU depinde de culoare (aria-label + title). */
.rar-chip { display:inline-flex; align-items:center; gap:7px; height:36px; padding:0 12px;
@@ -190,7 +190,7 @@
.rar-dot.live { animation:rar-pulse 2s ease-in-out infinite; }
@keyframes rar-pulse { 0%,100%{opacity:1;} 50%{opacity:.5;} }
@media (max-width:560px) { .rar-chip .rar-tx { display:none; } }
/* US-010 (PRD 5.16): sub-titlu cu numele service-ului (cand autentificat). */
/* Sub-titlu cu numele service-ului (cand autentificat). */
.h-sub { font-size:var(--fs-xs); color:var(--muted); margin-top:2px; line-height:1.2; }
.h-sub .svc { color:var(--ink); font-weight:600; }
/* Badge env (test/prod) si badge tier (plan cont) langa titlu. */
@@ -367,7 +367,7 @@
border-radius:6px; cursor:pointer; min-height:36px; white-space:nowrap; }
.kebab-menu button:hover, .kebab-menu a:hover { background:var(--line); }
.kebab-menu button.danger { color:var(--err); }
/* === Accordion import compact (US-006 — regiune CSS disjuncta) ===
/* === Accordion import compact (regiune CSS disjuncta) ===
<details id="import-details"> invelete stepper + upload pe Acasa.
Atribut `open` setat de server din `are_trimiteri`:
False (first-run) → open; True (returning) → colapsat.
@@ -394,7 +394,7 @@
border-bottom:none; border-radius:0;
margin-bottom:0; }
/* === Sfarsit regiune accordion import === */
/* === Inceput regiune nav links status-bar (US-005) ===
/* === Inceput regiune nav links status-bar ===
Linkuri Trimiteri + Mapari sub contoare; marcaj activ cu aria-current/status-nav-activ. */
.status-nav-link { color:var(--accent); text-decoration:none; padding:2px 6px; border-radius:4px;
transition:background .1s; }
@@ -504,7 +504,7 @@
/* === Preview import: coloane extra fata de tabelul Trimiteri.
SCOPAT prin .tabel-trimiteri (clasa partajata). Regiune separata —
nu atinge coloanele existente (col-chk/id/stare/data/rar/actualizat).
US-007: 8 coloane (coloana de verificare manuala eliminata).
8 coloane (coloana de verificare manuala eliminata).
Suma latimi fixe: col-id(48) + col-stare(104) + col-data(104) +
col-km(76) + col-note(176) + col-actiuni(92) = 600px.
Restul (~680px la 1280px) revine lui col-vehicul + col-operatie (fluid). === */
@@ -561,7 +561,7 @@
font-size:12px; margin-bottom:2px; }
.tabel-trimiteri td.col-chk, .tabel-trimiteri td.col-id { display:none; }
/* === Card COMPACT (PRD 5.13, corectie dogfood) ===
/* === Card COMPACT ===
Inlocuieste stiva generica eticheta+valoare (prea inalta) cu un card
scanabil la prima vedere: vehicul = titlu, stare = pill dreapta-sus,
operatie+cod pe rand, meta (data/km/rar) muted mic, nota mica. Fara
@@ -701,10 +701,10 @@
.sticky-bar { padding:10px 12px; gap:10px; }
.sticky-bar button { width:100%; min-height:44px; }
}
/* === SENTINEL-COMPONENTE-SLIM: inceput componente slim US-002 (PRD 5.15).
/* === SENTINEL-COMPONENTE-SLIM: inceput componente slim.
Testele ancoreaza pe acest marker. Nu muta/sterge. === */
/* .contor-card — card cifra contor: fundal --card2, bordura --line, radius 8px.
US-002 PRD 5.16: padding marit (18px), cifra pe --fs-2xl, label pe --fs-sm, sub pe --fs-xs.
Padding marit (18px), cifra pe --fs-2xl, label pe --fs-sm, sub pe --fs-xs.
Variante de culoare a cifrei prin clasele .s-* existente (verde/accent/rosu). */
.contor-card { background:var(--card2); border:1px solid var(--line); border-radius:8px; padding:18px 18px; }
.contor-cifra { font-size:var(--fs-2xl); font-weight:700; line-height:1; }
@@ -796,8 +796,8 @@
@media (max-width:767px) {
.trimitere-slim { padding:12px 14px; }
}
/* === SENTINEL-COMPONENTE-SLIM: sfarsit componente slim US-002 === */
/* === Fix mobil Mapari (bug live 2026-06-29) ===
/* === SENTINEL-COMPONENTE-SLIM: sfarsit componente slim === */
/* === Fix mobil Mapari ===
Doua probleme raportate la 390px pe pagina Mapari:
(1) butoanele Salveaza/Sterge taiate: regula `.tabel-card td button {width:100%}`
(specificitate 0,1,2) batea `.act {width:44px}` (0,1,0) -> cele doua butoane act
@@ -823,10 +823,88 @@
.tabel-card td .act .act-tx { display:inline; }
.tabel-card td .act .act-ic { display:inline-block; }
}
/* === Meniu hamburger grupat === */
/* Iconite SVG stroke unic, currentColor — mostenesc culoarea intrarii. */
.ic { width:18px; height:18px; stroke:currentColor; fill:none; stroke-width:1.7;
stroke-linecap:round; stroke-linejoin:round; flex-shrink:0; }
.cont-menu a, .cont-menu form button { display:flex; align-items:center; gap:10px; }
.cont-menu a .mi, .cont-menu form button .mi { display:inline-flex; opacity:.6; }
.cont-menu a:hover .mi, .cont-menu form button:hover .mi { opacity:.9; }
.cont-menu-group { font-size:10px; text-transform:uppercase; letter-spacing:.08em;
color:var(--muted); font-weight:700; padding:8px 12px 4px; }
/* Badge-ul de mapari se ancoreaza la dreapta chiar cand intrarea e flex. */
.cont-menu a .tab-badge { margin-left:auto; }
/* === Pagina Setari cu sub-taburi === */
.setari-head { margin-bottom:4px; }
.setari-title { font-size:var(--fs-lg); margin:0 0 2px; letter-spacing:-.01em;
display:flex; align-items:center; gap:9px; }
.setari-title .ic { width:20px; height:20px; opacity:.7; }
.setari-sub { color:var(--muted); font-size:var(--fs-sm); margin:0 0 14px; }
.setari-subtabs { display:flex; gap:2px; border-bottom:1px solid var(--line); margin-bottom:20px;
overflow-x:auto; -webkit-overflow-scrolling:touch; scrollbar-width:none; }
.setari-subtabs::-webkit-scrollbar { display:none; }
.setari-subtab { appearance:none; background:transparent; border:none; border-bottom:2px solid transparent;
color:var(--muted); font:inherit; font-size:var(--fs-sm); font-weight:500; cursor:pointer;
padding:10px 2px; margin-right:22px; white-space:nowrap; display:inline-flex;
align-items:center; gap:8px; margin-bottom:-1px; transition:color .12s, border-color .12s; }
.setari-subtab:hover { color:var(--ink); }
/* Stare activa prin clasa (nu selector pe atribut): aria-selected ramane pe buton
pentru accesibilitate, dar stilul nu depinde de el. */
.setari-subtab.is-active { color:var(--ink); border-bottom-color:var(--accent); font-weight:600; }
.setari-subtab:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.st-dot { width:7px; height:7px; border-radius:99px; flex-shrink:0; }
.st-dot.ok { background:var(--ok); }
.st-dot.warn { background:var(--warn); }
.st-dot.info { background:var(--muted); }
.setari-pane[hidden] { display:none; }
.setari-zone-title { font-size:var(--fs-md); font-weight:600; margin:0 0 4px; display:flex;
align-items:center; gap:10px; flex-wrap:wrap; }
.setari-zone-title .ic { opacity:.7; }
.setari-zone-desc { color:var(--muted); font-size:var(--fs-sm); margin:0 0 16px; max-width:64ch; line-height:1.5; }
.setari-badge { font-size:11px; font-weight:700; padding:2px 9px; border-radius:99px;
text-transform:uppercase; letter-spacing:.03em; border:1px solid transparent; }
.setari-badge.sb-ok { color:var(--ok); background:color-mix(in srgb,var(--ok) 12%,transparent);
border-color:color-mix(in srgb,var(--ok) 40%,transparent); }
.setari-badge.sb-warn { color:var(--warn); background:color-mix(in srgb,var(--warn) 12%,transparent);
border-color:color-mix(in srgb,var(--warn) 40%,transparent); }
.setari-badge.sb-info { color:var(--muted); background:color-mix(in srgb,var(--muted) 12%,transparent);
border-color:color-mix(in srgb,var(--muted) 35%,transparent); }
.setari-subzone-sep { border:none; border-top:1px solid var(--line); margin:26px 0 22px; }
/* Cheie API afisata o singura data. */
.keybox { font-family:var(--font-mono); font-size:14px; background:var(--card2);
border:1px solid var(--line); border-radius:8px; padding:12px 14px; word-break:break-all;
max-width:480px; }
.btn-warn { background:transparent; color:var(--warn); border:1px solid var(--warn); border-radius:6px;
padding:8px 14px; font:inherit; font-weight:500; cursor:pointer; min-height:36px;
display:inline-flex; align-items:center; justify-content:center; gap:6px; }
.btn-warn:hover, .btn-warn:focus-visible { background:var(--warn); color:#fff; filter:none; }
/* Credentiale RAR: doua carduri de mediu paralele. */
.env-cards { display:grid; grid-template-columns:1fr 1fr; gap:14px; margin-bottom:18px; }
.env-card { border:1px solid var(--line); border-radius:10px; padding:16px; background:var(--card2); }
.env-card.active-test { border-color:color-mix(in srgb,var(--ok) 45%,var(--line)); }
.env-card h4 { margin:0 0 12px; font-size:var(--fs-sm); display:flex; align-items:center;
justify-content:space-between; gap:8px; }
.env-card .env-sw { display:flex; align-items:center; gap:8px; font-size:var(--fs-sm);
color:var(--muted); margin-bottom:12px; }
.env-card .env-lbl { display:block; font-size:var(--fs-xs); color:var(--muted); margin-bottom:5px; }
.env-card input[type=email], .env-card input[type=password] {
width:100%; font:inherit; background:var(--bg); color:var(--ink); border:1px solid var(--line);
border-radius:6px; padding:8px 10px; margin-bottom:10px; font-size:var(--fs-sm); }
.env-card .env-confirm { display:flex; align-items:flex-start; gap:8px; font-size:var(--fs-xs);
color:var(--muted); line-height:1.4;
background:color-mix(in srgb,var(--err) 7%,transparent);
border:1px solid color-mix(in srgb,var(--err) 22%,transparent);
border-radius:7px; padding:9px 11px; margin-top:2px; }
.env-card .env-confirm input { margin-top:2px; flex-shrink:0; }
.default-env { display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin:0 0 18px;
padding:14px; border:1px solid var(--line); border-radius:9px; background:var(--card2); }
.default-env select { font:inherit; background:var(--bg); color:var(--ink); border:1px solid var(--line);
border-radius:6px; padding:7px 10px; max-width:340px; }
@media (max-width:640px) { .env-cards { grid-template-columns:1fr; } }
</style>
</head>
<body>
{# US-010 (PRD 5.16): antet branduit ROA AUTOPASS.
{# Antet branduit ROA AUTOPASS.
Grila 3 coloane — stanga (logo) | centru (titlu+env+tier+account_name) | dreapta (RAR dot + tema + burger).
Antet MINIMAL pe /login: nu afiseaza RAR dot, meniu burger sau account_name (nelogat). #}
<header>
@@ -850,7 +928,7 @@
</div>
{# Celula dreapta: dot RAR (numai cand logat) + selector tema + versiune + meniu burger #}
<div class="header-right">
{# US-003 (PRD 5.16): dot RAR in antet — OK = chip verde pulsant, BLOCAT = chip rosu.
{# Dot RAR in antet — OK = chip verde pulsant, BLOCAT = chip rosu.
Banda plina apare DOAR in _status.html cand BLOCAT (nu mai e mereu vizibila). #}
{% if is_authenticated|default(false) %}
{% if sanatate_ok|default(true) %}
@@ -868,11 +946,11 @@
<span class="rar-tx">RAR blocat</span>
</div>
{% endif %}
{# US-004 (PRD 5.19 / D1): comutatorul "Trimite automat la RAR" langa semnalul RAR.
In afara swap-ului periodic al #status-bar (D3). #}
{# Comutatorul "Trimite automat la RAR" langa semnalul RAR.
In afara swap-ului periodic al #status-bar. #}
{% include '_auto_send_toggle.html' %}
{% endif %}
{# US-011 (PRD 5.16): selector tema = pill cu icon FIX (acelasi SVG ca landing) +
{# Selector tema = pill cu icon FIX (acelasi SVG ca landing) +
eticheta temei curente. Eticheta ascunsa pe <=560px via CSS. JS actualizeaza
doar #tema-label (iconita nu se mai schimba per tema — consecventa cu landing). #}
<button id="tema-toggle" class="tema-btn"
@@ -885,14 +963,14 @@
<span class="muted" style="font-size:var(--fs-xs);">v{{ version }}</span>
{% if is_authenticated|default(false) %}
{# Meniu cont: Cont/Integrare/Nomenclator + (admin) + logout.
US-010: structura cu <hr> separatori + RAR status (prima intrare) + Plan tier.
Structura cu <hr> separatori + RAR status (prima intrare) + Plan tier.
Pe paginile neautentificate (login/signup) nu se randeaza deloc. #}
<div class="cont-menu-wrap">
<button id="cont-menu-toggle" class="icon-btn"
aria-haspopup="true" aria-expanded="false" aria-controls="cont-menu"
aria-label="Meniu cont" title="Meniu cont">&#9776;</button>
<div id="cont-menu" class="cont-menu" role="menu" aria-labelledby="cont-menu-toggle" hidden>
{# Prima intrare: starea RAR (US-003) #}
{# Prima intrare: starea RAR #}
{% if sanatate_ok|default(true) %}
<div class="menu-rar-line ok" role="menuitem" aria-disabled="true">
<span style="width:8px;height:8px;border-radius:99px;background:currentColor;display:inline-block;"></span>
@@ -904,31 +982,31 @@
RAR indisponibil
</div>
{% endif %}
{# Plan cont curent (US-006 PRD 5.17): linie detaliata cu trial/consum/warn.
Warn = culoare + text (accesibilitate, decizie #14). #}
{# Plan cont curent: linie detaliata cu trial/consum/warn.
Warn = culoare + text (accesibilitate). #}
<div class="menu-rar-line" role="menuitem" aria-disabled="true"
style="color:{% if plan_limita_atinsa|default(false) %}var(--err){% elif plan_warn|default(false) %}var(--warn){% else %}var(--muted){% endif %};
{% if plan_warn|default(false) %}font-weight:600;{% endif %}">
{{ plan_linie|default('Plan: ' + (tier_label|default('Gratuit'))) }}
</div>
<hr>
{# Navigare principala: Trimiteri + Mapari #}
<a role="menuitem" href="/">Trimiteri</a>
{# Grup LUCRU: fluxul zilnic — trimiteri, mapari, nomenclator. #}
<div class="cont-menu-group">Lucru</div>
<a role="menuitem" href="/"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M22 2 11 13"/><path d="M22 2 15 22l-4-9-9-4z"/></svg></span>Trimiteri</a>
{% set _mapari_badge = (badges.mapari if (badges is defined and badges and badges.mapari) else 0) %}
<a role="menuitem" href="/?tab=mapari">Mapari{% if _mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; margin-left:6px; padding:0 5px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ _mapari_badge }}</span>{% endif %}</a>
<a role="menuitem" href="/?tab=mapari"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14 14 10"/><path d="M11.5 6.5 13 5a4 4 0 0 1 6 6l-1.5 1.5"/><path d="M12.5 17.5 11 19a4 4 0 0 1-6-6l1.5-1.5"/></svg></span>Mapari{% if _mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; padding:0 5px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ _mapari_badge }}</span>{% endif %}</a>
<a role="menuitem" href="/?tab=nomenclator"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4a1 1 0 0 1 1-1h13v18H6a1 1 0 0 0-1 1z"/><path d="M5 18h14"/></svg></span>Nomenclator</a>
<hr>
{# Nomenclator: coduri RAR — public, dar in meniu arata mai logic la logat #}
<a role="menuitem" href="/?tab=nomenclator">Nomenclator</a>
<hr>
{# Setari cont #}
<a role="menuitem" href="/?tab=cont">Cont</a>
<a role="menuitem" href="/?tab=integrare">Integrare</a>
<a role="menuitem" href="/?tab=jurnal">Jurnal</a>
{% if is_admin|default(false) %}<a role="menuitem" href="/admin">Conturi clienti</a>{% endif %}
{# Grup CONT: configurare + audit + admin. "Cont" redenumit "Setari" (URL ramane ?tab=cont). #}
<div class="cont-menu-group">Cont</div>
<a role="menuitem" href="/?tab=cont"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.6 4.6l2.1 2.1M17.3 17.3l2.1 2.1M4.6 19.4l2.1-2.1M17.3 6.7l2.1-2.1"/></svg></span>Setari</a>
<a role="menuitem" href="/?tab=integrare"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 3v6M15 3v6"/><path d="M7 9h10v3a5 5 0 0 1-10 0z"/><path d="M12 17v4"/></svg></span>Integrare</a>
<a role="menuitem" href="/?tab=jurnal"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 3h8l4 4v14H7z"/><path d="M15 3v4h4"/><path d="M10 13h6M10 17h6"/></svg></span>Jurnal</a>
{% if is_admin|default(false) %}<a role="menuitem" href="/admin"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="9" cy="8" r="3"/><path d="M3.5 20a5.5 5.5 0 0 1 11 0"/><path d="M16 5.5a3 3 0 0 1 0 5"/><path d="M17.5 14.2A5.5 5.5 0 0 1 21 20"/></svg></span>Conturi clienti</a>{% endif %}
<hr>
<form method="post" action="/logout">
<input type="hidden" name="csrf_token" value="{{ csrf_token|default('') }}">
<button role="menuitem" type="submit">Iesi din cont</button>
<button role="menuitem" type="submit"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2v-3"/><path d="M9 12h12"/><path d="m18 9 3 3-3 3"/></svg></span>Iesi din cont</button>
</form>
</div>
</div>
@@ -954,12 +1032,12 @@
<div id="detaliu-modal-body"></div>
</div>
</div>
{# Slot pentru modalul de confirmare tipata (PRD 5.19 D4): auto-release OFF->ON injecteaza
{# Slot pentru modalul de confirmare tipata: auto-release OFF->ON injecteaza
aici modalul via OOB (#modal-confirma-slot). Bulk "Trimite toate" isi randeaza propriul
modal inline in _coada.html. #}
<div id="modal-confirma-slot"></div>
<script>
// Comutator tema ciclic (DRY E2 — PRD 5.15): config traieste intr-o singura structura
// Comutator tema ciclic: config traieste intr-o singura structura
// sursa-de-adevar THEMES din care se DERIVA CYCLE/VALID/LABELS/NEXT.
// Adaugarea unei teme noi = O singura intrare in THEMES.
// Ciclu: Light->Dark->Petrol->Grafit->Cobalt->Cupru->Hartie->Auto->(inapoi la Light).
@@ -997,7 +1075,7 @@
}
function _syncButton(stored) {
var s = VALID[stored] ? stored : 'auto';
// US-011: iconita e fixa (nu se schimba per tema); actualizam doar eticheta.
// Iconita e fixa (nu se schimba per tema); actualizam doar eticheta.
var label = document.getElementById('tema-label');
if (label) label.textContent = LABELS[s];
btn.setAttribute('aria-label', 'Tema: ' + LABELS[s] + ', apasa pentru ' + NEXT[s]);
@@ -1109,6 +1187,28 @@
if (saveBtn) saveBtn.classList.add('dirty');
});
</script>
<script>
// Sub-taburi pagina Setari: comutare pur client-side, fara server.
// Delegare pe document -> supravietuieste swap-urilor HTMX ale #card-cont (dupa
// fiecare salvare serverul re-randeaza cu sub-tabul corect via `aria-selected`/`hidden`,
// iar acest handler preia doar navigarea manuala intre taburi).
document.addEventListener('click', function(e) {
var tab = e.target.closest('.setari-subtab');
if (!tab) return;
var card = tab.closest('#card-cont');
if (!card) return;
var name = tab.getAttribute('data-subtab');
card.querySelectorAll('.setari-subtab').forEach(function(t) {
var on = t === tab;
t.setAttribute('aria-selected', on ? 'true' : 'false');
t.classList.toggle('is-active', on);
});
card.querySelectorAll('.setari-pane').forEach(function(p) {
if (p.getAttribute('data-subpane') === name) { p.removeAttribute('hidden'); }
else { p.setAttribute('hidden', ''); }
});
});
</script>
<script>
// Toast global: feedback tranzitoriu vizibil + accesibil (aria-live).
// window.arataToast(text, stareCss) — stareCss (ex. "s-error"/"s-needs_review")
@@ -1274,7 +1374,7 @@
// Deschidere la click pe rand (htmx:beforeRequest): arata modalul cu placeholder
// inainte ca raspunsul fragmentului sa fie swap-uit in corp.
// Trateaza atat .trimitere-row (Trimiteri) cat si .btn-editeaza (preview import)
// → open() instaleaza inert pe <main>, focus-trap si readuce focusul la inchidere (US-006).
// → open() instaleaza inert pe <main>, focus-trap si readuce focusul la inchidere.
document.body.addEventListener('htmx:beforeRequest', function(evt) {
var elt = evt.detail && evt.detail.elt;
if (!elt || !elt.classList) return;
@@ -1387,7 +1487,7 @@
// Daca difera (schimbari externe — ex. worker a procesat trimiteri), reincarca automat
// pastrand filtrul curent. Fara nudge "Date noi" — auto-refresh e mai consistent.
// Decizie: nudge eliminat; distinctia propriu vs extern e imposibila pe client
// fara sesiune dedicata — auto-refresh acoper ambele cazuri (US-008).
// fara sesiune dedicata — auto-refresh acoper ambele cazuri.
var INTERVAL = 20000;
function versiuneCurenta() {
var e = document.getElementById('trimiteri-versiune');

View File

@@ -7,7 +7,7 @@
<meta name="description" content="Încarci fișierul tău cu operațiile service-ului, completezi o dată codurile RAR și le salvezi. ROMFAST trimite prestațiile la RAR AUTOPASS în locul tău, fără tastat manual. Conform Legii 142/2023.">
<style>
/* US-001/US-008 (PRD 5.16): IBM Plex eliminat complet — stive font sistem standard web.
/* IBM Plex eliminat complet — stive font sistem standard web.
Tokenurile --font-ui / --font-mono definite in :root (sursa unica de adevar). */
:root{--font-ui:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;--font-mono:ui-monospace,"SF Mono","Cascadia Code","Segoe UI Mono","Roboto Mono",Menlo,Consolas,monospace;}
*{box-sizing:border-box;}

View File

@@ -2,7 +2,7 @@
{% block title %}Autentificare — ROA AUTOPASS{% endblock %}
{% block content %}
{# /login — card de autentificare branduit, o singura coloana (simplificat din layout-ul
2-coloane US-010 PRD 5.16). Formular: CSRF, POST /login, link signup. #}
2-coloane). Formular: CSRF, POST /login, link signup. #}
<div class="login-2col" style="max-width:860px; margin:32px auto;">
{# Antet minimal deja randat in base.html (fara RAR dot, fara burger, fara account_name) #}
<div class="login-shell">

View File

@@ -16,7 +16,7 @@ Worker-ul face login per CONT cu acele creds, cache-uieste JWT (30h) in memorie
STERGE creds-urile efemere dupa primul login reusit. Token-ul in memorie acopera
restul trimiterilor; la restart token-ul se pierde si contul re-logheaza la urmatorul
submission care aduce creds proaspete (degradare acceptata). Fallback durabil: slotul
per-env al contului (accounts.rar_creds_{env}_enc, US-013; coloana legacy dropata).
per-env al contului (accounts.rar_creds_{env}_enc; coloana legacy dropata).
Dev: `worker_use_test_creds` foloseste creds <test> cand submission-ul nu are enc.
Pornire: python -m app.worker
@@ -145,7 +145,7 @@ def purge_expired(conn) -> dict[str, int]:
def expire_held(conn, settings: Settings) -> int:
"""Expira randurile tinute manual (`queued AND held=1`) mai vechi de `held_retention_days`.
PRD 5.19 US-008 (inchide gaura GDPR/L.142): un `queued held=1` nu e nici `sent` nici
Inchide gaura GDPR/L.142: un `queued held=1` nu e nici `sent` nici
blocat -> altfel NU primeste `purge_after` -> PII criptat ar sta la nesfarsit.
Le trece la `error`/`TINUT_EXPIRAT` (terminal) si seteaza `purge_after` DIRECT la
momentul expirarii (NU lasa retentia de 30z a randurilor blocate sa se adauge; altfel
@@ -154,7 +154,7 @@ def expire_held(conn, settings: Settings) -> int:
(`purge_after < datetime('now')`), iar `datetime('now')` are rezolutie de o secunda —
cu `purge_after=now` cele doua ar fi egale in acelasi ciclu si randul NU s-ar purja
decat la ciclul urmator (~1h de PII in plus). Cu `-1 second` randul e purjabil IMEDIAT
in acelasi ciclu de purjare (US-008 inchide gaura strict).
in acelasi ciclu de purjare.
Intoarce numarul de randuri expirate.
"""
@@ -199,7 +199,7 @@ def claim_one(conn) -> dict | None:
"SELECT s.id, s.account_id, s.payload_json, s.rar_creds_enc, s.rar_env "
"FROM submissions s LEFT JOIN accounts a ON a.id = s.account_id "
"WHERE s.status='queued' "
# PRD 5.19 US-003: randurile tinute manual (held=1) NU se revendica —
# Randurile tinute manual (held=1) NU se revendica —
# asteapta eliberarea umana (manual/auto). Se aplica DOAR la claim din
# 'queued'; randurile deja 'sending' (orfani/reconciliere) sunt neafectate.
"AND s.held = 0 "
@@ -464,7 +464,7 @@ def _creds_for(claimed: dict, settings: Settings) -> dict | None:
def _creds_from_account(conn, account_id: int, rar_env: str = "test") -> dict | None:
"""Creds RAR durabile per-cont din slotul per-env (US-013 — coloana legacy dropata).
"""Creds RAR durabile per-cont din slotul per-env (coloana legacy dropata).
Canal web: creds in accounts.rar_creds_{rar_env}_enc (per-env, singurul slot valid).
"""
@@ -482,7 +482,7 @@ def _keepalive_target(conn, settings: Settings) -> tuple[int | None, dict | None
"""Un cont cu creds durabile pentru login-ul de proba (sau creds <test> in dev).
Ancora M2: cauta in slotul per-env al mediului `settings.rar_env` (ancora globala).
US-013: coloana legacy accounts.rar_creds_enc a fost dropata — se foloseste EXCLUSIV
Coloana legacy accounts.rar_creds_enc a fost dropata — se foloseste EXCLUSIV
slotul per-env. Sare conturile ale caror creds NU se decripteaza sub cheia curenta
(in dev `start.sh both` genereaza o cheie efemera noua la fiecare pornire).
"""
@@ -565,7 +565,7 @@ def run() -> int:
# Purjare periodica (odata pe ora) — NU mai frecvent.
now_ts = time.time()
if now_ts - _last_purge_time >= _PURGE_INTERVAL_S:
# Expira intai randurile tinute imbatranite (US-008), apoi purjeaza:
# Expira intai randurile tinute imbatranite, apoi purjeaza:
# expire_held seteaza purge_after=now-1s -> randul e purjabil IMEDIAT in
# acelasi ciclu (purge_expired foloseste comparatie stricta purge_after<now).
expired = expire_held(conn, settings)

View File

@@ -1,4 +1,4 @@
"""Liveness probe pentru worker (T6) — folosit de healthcheck-ul Docker.
"""Liveness probe pentru worker — folosit de healthcheck-ul Docker.
Worker-ul nu e server HTTP, deci `restart: always` prinde doar procesul MORT,
nu si worker-ul AGATAT (proces viu care nu mai bate heartbeat). Acest probe

View File

@@ -13,7 +13,7 @@ services:
- autopass-data:/data
environment:
AUTOPASS_DB_PATH: /data/autopass.db
# Ancora globala, NU tinta trimiterilor: dupa PRD 5.20 mediul RAR e per
# Ancora globala, NU tinta trimiterilor: mediul RAR e per
# submission/cont (worker-ul trimite dupa submissions.rar_env). Variabila
# asta serveste doar ca fallback API pentru conturi fara medii disponibile,
# backfill la migrare (db.py) si afisaj dashboard (routes.py). Obligatorie

View File

@@ -22,9 +22,9 @@ os.environ.setdefault("AUTOPASS_WORKER_USE_TEST_CREDS", "false")
# lazy-load-eze modelul de ~230MB la fiecare test care atinge editorul de mapari
# (suita rapida, fara download in CI). Testele de embeddings il pornesc punctual.
os.environ.setdefault("AUTOPASS_EMBEDDINGS_ENABLED", "false")
# Seed-ul de operatii etichetate (SILVER, PRD 5.18) e ON in app, dar OFF in teste:
# multe teste presupun mapping_suggestions GOL la init_db. Testele US-004/005/006 il
# pornesc punctual (object.__setattr__ pe settings sau apel direct la seeder).
# Seed-ul de operatii etichetate e ON in app, dar OFF in teste: multe teste presupun
# mapping_suggestions GOL la init_db. Testele care au nevoie de el il pornesc punctual
# (object.__setattr__ pe settings sau apel direct la seeder).
os.environ.setdefault("AUTOPASS_SEED_OPERATII_ENABLED", "false")
@@ -35,15 +35,15 @@ _REAL_INIT_DB = None
@pytest.fixture(autouse=True)
def _auto_send_on_default_account(request, monkeypatch):
"""PRD 5.19 US-009: contul default id=1 (seed schema.sql, INSERT OR IGNORE) porneste pe
"""Contul default id=1 (seed schema.sql, INSERT OR IGNORE) porneste pe
`auto_send_enabled=0` (Auto OFF) — corect in productie (nimic nu pleaca fara confirmare).
Dar suita exercita lantul `POST -> claim_one -> sent` pe contul id=1
(test_import_e2e, test_creds_delivery, test_live_rar): cu Auto OFF, ingestia ar marca
`held=1` (US-002) si worker-ul (`claim_one ... AND held=0`, US-003) nu ar mai lua randul
`held=1` si worker-ul (`claim_one ... AND held=0`) nu ar mai lua randul
-> testele ar stagna tacit. E un fix de STARE DB, nu env var (coloana e per-rand in
`accounts`), deci il aplicam wraparind `init_db` (contul id=1 NU e creat de `create_account`
-> un patch pe factory NU l-ar acoperi — Eng HIGH).
-> un patch pe factory NU l-ar acoperi).
Wrapam `init_db` astfel incat orice DB de test creat sa aiba id=1 pe Auto ON (held=0 la
ingestie), pastrand suita verde ca inainte de 5.19. Conturile create explicit de teste
@@ -114,7 +114,7 @@ def _reset_embeddings_singleton():
def make_test_cui(seed: str = "") -> str:
"""Factory centralizat (D#14, PRD 5.12 US-001): genereaza un CUI de test unic din seed.
"""Factory centralizat: genereaza un CUI de test unic din seed.
Folosit de fixture-urile de test care creeaza conturi via /signup sau create_account
si au nevoie de un CUI unic per test (altfel unicitatea CUI-ului bloca al doilea signup

View File

@@ -1,8 +1,8 @@
"""Teste US-003 (PRD 3.6): Acasa unificata — Trimiteri ca sectiune permanenta.
"""Teste Acasa unificata — Trimiteri ca sectiune permanenta.
Tab-ul "Trimiteri" (coada) eliminat din tab-bar; Trimiterile devin o sectiune sub
upload pe Acasa, cu heading "Trimiterile tale". ?tab=coada si /_fragments/coada
servesc continutul Acasa (nu 404, nu fragment orfan). Poll aliniat la 15s (M5).
servesc continutul Acasa (nu 404, nu fragment orfan).
"""
from __future__ import annotations
@@ -50,14 +50,14 @@ def _seed_submission(status: str = "sent", n: int = 1) -> None:
def test_tab_bar_fara_trimiteri(client):
"""US-009: tab-bar eliminat; 'Coada' nu exista; Mapari/Cont/Nomenclator raman in meniu."""
"""Tab-bar eliminat; 'Coada' nu exista; Mapari/Cont/Nomenclator raman in meniu."""
r = client.get("/")
assert r.status_code == 200
html = r.text
# "Coada" nu trebuie sa existe nici ca tab, nici ca link in meniu
assert 'id="tab-coada"' not in html
assert 'href="/?tab=coada"' not in html
# US-009: tab-bar eliminat; Mapari/Cont/Nomenclator sunt in meniul hamburger
# Mapari/Cont/Nomenclator sunt in meniul hamburger
for label in ("Mapari", "Cont", "Nomenclator"):
assert f">{label}" in html or f"{label}<" in html, f"lipseste intrarea {label} in meniu"
@@ -70,8 +70,7 @@ def test_acasa_contine_sectiunea_trimiteri(client):
html = r.text
assert 'id="filtre-trimiteri"' in html
assert "/_fragments/submissions" in html
# PRD 5.9 US-003: detaliul s-a mutat in modalul global (#modal-detaliu); vechiul
# panou inert #trimitere-detaliu a fost eliminat.
# Detaliul e in modalul global (#modal-detaliu); panoul inert vechi eliminat.
assert 'id="modal-detaliu"' in html
assert 'id="trimitere-detaliu"' not in html
@@ -108,7 +107,7 @@ def test_acasa_fara_linkuri_ajutor(client):
def test_acasa_fara_wayfinding_ajutor(client):
"""US-001 (5.5): randul 'Ajutor' (wayfinding Mapari/Coduri RAR) eliminat din Acasa —
"""Randul 'Ajutor' (wayfinding Mapari/Coduri RAR) eliminat din Acasa —
navigarea traieste in tab-bar si in meniul de cont."""
r = client.get("/?tab=acasa")
html = r.text
@@ -132,7 +131,7 @@ def test_trimiteri_fara_poll_periodic_pe_tabel(client):
"""Tabelul de trimiteri NU se reimprospateaza periodic: #submissions-wrap se
incarca la load / actiunile utilizatorului (trimiteriChanged) / reincarcaTrimiteri.
Reimprospatarea automata la date noi externe se face prin pollerul de versiune
care cheama reincarcaTrimiteri() — fara nudge manual (US-008)."""
care cheama reincarcaTrimiteri()."""
_seed_submission("sent")
r = client.get("/?tab=acasa")
html = r.text
@@ -142,6 +141,6 @@ def test_trimiteri_fara_poll_periodic_pe_tabel(client):
assert "every" not in wrap, f"tabelul nu trebuie sa aiba poll periodic: {wrap}"
assert "reincarcaTrimiteri" in wrap
assert "trimiteriChanged" in wrap
# Pollerul de versiune exista (auto-refresh la date noi externe). Nudge eliminat (US-008).
# Pollerul de versiune exista (auto-refresh la date noi externe); fara nudge manual.
assert "/_fragments/trimiteri-versiune" in html
assert 'id="nudge-trimiteri"' not in html

View File

@@ -1,4 +1,4 @@
"""Teste US-004 (PRD 5.5): model de stare a contului `accounts.status` + helperi.
"""Teste model de stare a contului `accounts.status` + helperi.
Acopera: derivarea status din active la migrare, invariantul active=1 <=> status='active',
helperii set_status/delete_account, protectia contului de sistem id=1, excluderea 'deleted'
@@ -94,7 +94,7 @@ def test_delete_purjeaza_pii_si_elibereaza_cui(conn):
"""Stergerea soft purjeaza creds RAR + revoca cheile API + elibereaza CUI (re-inregistrabil)."""
from app.accounts import create_account, delete_account, list_accounts
acct_id = create_account(conn, "Service GDPR", cui="RO12345", active=True)
# US-013: creds in sloturi per-env (rar_creds_enc legacy dropata)
# creds in sloturi per-env
conn.execute(
"UPDATE accounts SET rar_creds_test_enc='secret_enc_test', rar_creds_prod_enc='secret_enc_prod' WHERE id=?",
(acct_id,),
@@ -138,7 +138,6 @@ def test_migrare_deriva_status_din_active(conn):
from app.db import _migrate
# Reconstruim accounts fara `status` (rebuild de tabela — singura cale in SQLite vechi).
# US-013: rar_creds_enc nu mai exista in accounts (dropata); nu e in legacy DDL.
conn.executescript(
"""
PRAGMA foreign_keys=OFF;

View File

@@ -1,4 +1,4 @@
"""Teste US-001 (PRD 3.1): coloana accounts.active + helper-e cont in app/accounts.py."""
"""Teste coloana accounts.active + helper-e cont in app/accounts.py."""
from __future__ import annotations
@@ -119,7 +119,7 @@ def test_list_accounts_ordonat_fara_creds(conn):
# ---------------------------------------------------------------------------
# US-001 (PRD 5.12): accounts.email + validari companie/email/CUI
# accounts.email + validari companie/email/CUI
# ---------------------------------------------------------------------------
def test_create_account_fara_email_ridica(conn):
@@ -176,7 +176,7 @@ def test_account_is_complete_false_pe_legacy_incomplet(conn):
# ---------------------------------------------------------------------------
# 5.17 US-001/US-008: schema tier + trial_until + set_tier + CLI set-tier
# schema tier + trial_until + set_tier + CLI set-tier
# ---------------------------------------------------------------------------
def test_migrare_tier_trial_defensiva(conn):

View File

@@ -1,4 +1,4 @@
"""Teste US-008 (PRD 5.5): rute admin pentru ciclul de viata al conturilor —
"""Rute admin pentru ciclul de viata al conturilor —
block/archive/delete + bulk pe lista account_id, require_admin + CSRF + PRG, dev id=1 protejat.
"""

View File

@@ -1,7 +1,4 @@
"""Teste US-011 (PRD 3.3b): panou admin web /admin — conturi in asteptare + activare.
TDD strict: testele se scriu INAINTE de implementare; la inceput pica (RED),
dupa implementare trec (GREEN).
"""Teste panou admin web /admin — conturi in asteptare + activare.
Fisiere testate: app/web/admin_routes.py, app/web/templates/admin.html.
"""
@@ -175,7 +172,7 @@ def test_non_admin_403(client):
Creeaza intai un cont admin (bootstrap: primul user devine admin),
apoi un al doilea cont (non-admin) si verifica ca al doilea primeste 403.
"""
# Primul signup devine automat admin (bootstrap US-010)
# Primul signup devine automat admin (bootstrap)
_signup(client, "Admin Bootstrap SA", "bootstrap@test.ro")
# Al doilea user NU e admin
@@ -286,7 +283,7 @@ def test_set_tier_din_admin_incheie_trial(client):
"""POST /admin/set-tier -> tier actualizat, trial_until=NULL (trial incheiat), 303.
Contul nou are trial Pro 30z; alocarea manuala trebuie sa-l incheie ca alegerea
sa aiba efect imediat (decizie user 2026-06-29)."""
sa aiba efect imediat."""
target_id = _signup(client, "Firma Upgrade SRL", "upgrade@test.ro")
tier0, trial0 = _get_tier_trial(target_id)
assert tier0 == "free", "cont nou trebuie sa porneasca pe free"

View File

@@ -1,4 +1,4 @@
"""Teste US-010 (PRD 3.3b): rol admin + bootstrap + guard require_admin.
"""Teste rol admin + bootstrap + guard require_admin.
Fisiere testate: app/users.py (helper-e admin), app/web/session.py (require_admin),
tools/account.py (subcomanda set-admin).

View File

@@ -1,4 +1,4 @@
"""Teste API /v1/prezentari — rutare validare (T3) + idempotenta."""
"""Teste API /v1/prezentari — rutare validare + idempotenta."""
from __future__ import annotations
@@ -63,7 +63,7 @@ def test_idempotenta_dedup(client):
# --------------------------------------------------------------------------- #
# PRD 5.7 — raspuns onest: erori/nemapate/motiv pe randuri blocate #
# Raspuns onest: erori/nemapate/motiv pe randuri blocate #
# --------------------------------------------------------------------------- #
def test_needs_data_intoarce_erori(client):

View File

@@ -1,4 +1,4 @@
"""Teste US-010 (PRD 5.6): API DELETE + re-pune in coada, scoped + oracol scope/stare."""
"""Teste API DELETE + re-pune in coada, scoped + oracol scope/stare."""
from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""Teste US-005 — camp rar_env pe POST /v1/prezentari si /valideaza.
"""Teste camp rar_env pe POST /v1/prezentari si /valideaza.
Acopera: default din cont, tinta explicita, respingere tinta indisponibila,
echo GET, valoare invalida (422 Pydantic), echo dry-run valideaza.

View File

@@ -1,4 +1,4 @@
"""US-011 (PRD 5.15): account-scope pe GET-urile de listare API (securitate).
"""Teste account-scope pe GET-urile de listare API (securitate).
Verifica:
- GET /v1/prezentari: un cont nu vede submissions ale altui cont
@@ -6,9 +6,8 @@ Verifica:
- Unauthenticated access cu require_api_key=True -> 401
- Filtre si paginare nu sparg scope-ul
Legatura cu implementare: resolve_account_id + account_scope_clause (mecanisme
existente, reutilizate). Testele LOCK DOWN comportamentul deja implementat
si verifica alinierea cu AUTOPASS_REQUIRE_API_KEY (dev vs prod).
Foloseste resolve_account_id + account_scope_clause; verifica alinierea
cu AUTOPASS_REQUIRE_API_KEY (dev vs prod).
"""
from __future__ import annotations
@@ -227,7 +226,7 @@ def test_get_listare_filtru_status_nu_sparge_scope(client):
# ============================================================================
# PRD 5.17 — enforcement planuri: gate API (T4) + volum (T3) + kill-switch (T5)
# Enforcement planuri: gate API + volum + kill-switch
# ============================================================================
_PREZ_PLAN = {
@@ -279,7 +278,7 @@ def _insert_n_submissions(account_id: int, n: int) -> None:
def test_free_fara_api_403(client):
"""Cont free (non-default, cu cheie API) → 403 PLAN_FARA_API pe POST /v1/prezentari.
T4 PRD 5.17: gate API refuza conturi cu api_access=False in PLANS.
Gate-ul API refuza conturi cu api_access=False in PLANS.
"""
acct_id, key = _create_account_with_key("FreePlanGate")
_set_tier_acct(acct_id, "free", trial_until=None)
@@ -349,8 +348,8 @@ def test_trial_pro_api_ok(client):
def test_dry_run_valideaza_ramane_permis(client):
"""POST /v1/prezentari/valideaza (dry-run) e permis pe orice plan, inclusiv free.
Decizie design (PRD 5.17): valideaza nu face enqueue si nu consuma cota,
deci NU e protejat de gate-ul API — integratorii pot testa fara upgrade.
Valideaza nu face enqueue si nu consuma cota, deci NU e protejat de
gate-ul API — integratorii pot testa fara upgrade.
"""
acct_id, key = _create_account_with_key("FreeValideazaTest")
_set_tier_acct(acct_id, "free", trial_until=None)
@@ -374,8 +373,8 @@ def test_dry_run_valideaza_ramane_permis(client):
def test_free_peste_60_respins_api(client):
"""Dev account (id=1) la 60/60 pe free → a 61-a prezentare respinsa 422 PLAN_LIMITA_LUNARA.
Dev account (id=1) in dev mode (require_api_key=False) are bypass la gate-ul API (T4),
dar NU la verificarea de volum (T3). La 60/60, urmatoarea cerere trebuie respinsa 422.
Dev account (id=1) in dev mode (require_api_key=False) are bypass la gate-ul API,
dar NU la verificarea de volum. La 60/60, urmatoarea cerere trebuie respinsa 422.
"""
_set_tier_acct(1, "free", trial_until=None)
_insert_n_submissions(1, 60)
@@ -392,7 +391,7 @@ def test_free_peste_60_respins_api(client):
def test_eroare_3_niveluri_plan_limita(client):
"""Eroarea PLAN_LIMITA_LUNARA contine toate 3 nivelurile: cod, problema, cauza, fix.
Pattern standard (PRD 5.17): eroare structurata pe 3 niveluri — nu 500, nu catch-all.
Pattern standard de eroare structurata pe 3 niveluri — nu 500, nu catch-all.
"""
_set_tier_acct(1, "free", trial_until=None)
_insert_n_submissions(1, 60)
@@ -512,7 +511,7 @@ def test_dev_id1_neblocat(client):
def test_flag_enforce_plans_false_sare_enforcement(client, monkeypatch):
"""Kill-switch AUTOPASS_ENFORCE_PLANS=false sare toate gate-urile de plan.
T5 PRD 5.17: flag de operare pentru debugging sau rollback rapid fara revert de cod.
Flag de operare pentru debugging sau rollback rapid fara revert de cod.
Chiar si cu free la 60/60, nu trebuie 422 cand flag-ul e oprit.
"""
from app.config import get_settings

View File

@@ -1,4 +1,4 @@
"""Teste US-004 (PRD 5.6): audit cerere API per cont in jurnal."""
"""Teste audit cerere API per cont in jurnal."""
from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""US-001 (PRD 5.19): schema comutator Auto per cont + flag `held` pe submission.
"""Schema comutator Auto per cont + flag `held` pe submission.
Acopera: migrarea aditiva idempotenta (coloane + index partial pe DB vechi), helperii
`get_auto_send`/`set_auto_send`/`held_for_account` din app/accounts.py si default-ul 0.

View File

@@ -1,6 +1,5 @@
"""Teste US-007 (PRD 3.6) actualizate dupa US-002 (PRD 5.11).
"""Teste comutator auto_send: macro autosend_toggle neutralizat (intoarce string gol).
US-002: macro autosend_toggle neutralizat (intoarce string gol).
Checkbox-ul name=auto_send a fost scos din UI. Coloanele DB raman.
Testele de UI verifica ABSENTA toggle-ului; testele de backend (stocare DB) raman.
"""
@@ -97,24 +96,24 @@ def _macro_html(checked: bool = True, form_id: str = "") -> str:
return str(mod.autosend_toggle(form_id=form_id, checked=checked))
# --- markup: macro neutralizat dupa US-002 ---
# --- markup: macro neutralizat ---
def test_comutator_coada_prezent():
"""US-002: macro autosend_toggle neutralizat -> output gol (fara checkbox)."""
"""macro autosend_toggle neutralizat -> output gol (fara checkbox)."""
html = _macro_html()
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"
assert html.strip() == "", f"macro neutralizat trebuie sa intoarca string gol, got: {html!r}"
def test_eticheta_scoped_pe_operatie():
"""US-002: macro neutralizat -> nicio eticheta scoped."""
"""macro neutralizat -> nicio eticheta scoped."""
html = _macro_html()
assert "aceasta operatie" not in html
assert html.strip() == ""
def test_default_pune_automat():
"""US-002: macro neutralizat intoarce gol indiferent de parametrul checked."""
"""macro neutralizat intoarce gol indiferent de parametrul checked."""
html_default = _macro_html(checked=True)
assert html_default.strip() == ""
html_off = _macro_html(checked=False)
@@ -154,7 +153,7 @@ def test_tine_pentru_verificare_mapeaza_auto_send_false(client):
# --- prezent in AMBELE locuri (mapari tab + panou preview) ---
def test_comutator_in_tab_mapari(client):
"""US-002: tabul Mapari nu mai contine checkbox auto_send (macro neutralizat)."""
"""Tabul Mapari nu mai contine checkbox auto_send (macro neutralizat)."""
from app.db import get_connection
import json
acct = _create_account_user("tm@test.com")
@@ -172,12 +171,12 @@ def test_comutator_in_tab_mapari(client):
_login(client, "tm@test.com")
resp = client.get("/?tab=mapari")
assert resp.status_code == 200
assert 'name="auto_send"' not in resp.text, "US-002: checkbox auto_send scos din UI"
assert "In coada" not in resp.text, "US-002: coloana 'In coada' scoasa"
assert 'name="auto_send"' not in resp.text, "checkbox auto_send scos din UI"
assert "In coada" not in resp.text, "coloana 'In coada' scoasa"
def test_comutator_in_panou_preview(client):
"""US-002: panoul de mapare din preview nu mai contine checkbox auto_send."""
"""Panoul de mapare din preview nu mai contine checkbox auto_send."""
_create_account_user("pp@test.com")
_seed_nomenclator("R-FRANE")
_login(client, "pp@test.com")
@@ -203,5 +202,5 @@ def test_comutator_in_panou_preview(client):
})
assert r.status_code == 200
assert "OP-NEMAPAT" in r.text, "operatia nemapata trebuie sa apara in panoul de mapare"
assert 'name="auto_send"' not in r.text, "US-002: checkbox auto_send scos din preview"
assert "In coada automat" not in r.text, "US-002: eticheta 'In coada automat' scoasa"
assert 'name="auto_send"' not in r.text, "checkbox auto_send scos din preview"
assert "In coada automat" not in r.text, "eticheta 'In coada automat' scoasa"

View File

@@ -1,4 +1,4 @@
"""Teste US-010 (PRD 5.20): badge mediu RAR in liste/detaliu/audit/ecou API.
"""Teste badge mediu RAR in liste/detaliu/audit/ecou API.
Teste:
test_badge_in_lista -- /_fragments/submissions afiseaza PRODUCȚIE / Testare per rand

View File

@@ -137,7 +137,7 @@ def test_legacy_dual_lookup_strategy():
assert found, "dual-lookup trebuie sa gaseasca randul vechi"
# --- Integrare: API route foloseste build_key (OV-2) ---
# --- Integrare: API route foloseste build_key ---
@pytest.fixture()
def client(monkeypatch):

View File

@@ -1,4 +1,4 @@
"""Teste PRD 5.19 R4 — tools/carantina_held.py (atenuare hazard de rollback).
"""Teste tools/carantina_held.py (atenuare hazard de rollback).
Carantineaza randurile `queued AND held=1` (-> `error`/`ROLLBACK_QUARANTINE`)
ca sa nu fie trimise de un worker fara filtrul `AND held=0` dupa un revert.
@@ -63,7 +63,7 @@ def test_carantineaza_rand_tinut(env, capsys):
r = _row(conn, sid)
assert r["status"] == "error"
assert r["rar_error"] == "ROLLBACK_QUARANTINE"
# /code-review #4: randul carantinat primeste purge_after (altfel PII ar sta la nesfarsit,
# randul carantinat primeste purge_after (altfel PII ar sta la nesfarsit,
# purge_expired sterge doar randuri cu purge_after setat).
assert r["purge_after"] is not None
assert conn.execute(

View File

@@ -1,4 +1,4 @@
"""Teste US-008 (PRD 5.20): configurare medii RAR per cont — Testare + Productie.
"""Teste configurare medii RAR per cont — Testare + Productie.
Ruta testata: POST /cont/rar-medii

View File

@@ -47,12 +47,12 @@ def _body(**over):
def test_dashboard_renders_with_rar_state(client):
r = client.get("/")
assert r.status_code == 200
# Dupa US-003 bara de status e incarcata via HTMX (hx-trigger=load, every 15s)
# Bara de status e incarcata via HTMX (hx-trigger=load, every 15s)
assert "/_fragments/status" in r.text, "Dashboard-ul trebuie sa referenceze fragmentul de status"
# Fragmentul de status contine starea de sanatate (text uman, nu brut tehnic)
rs = client.get("/_fragments/status")
assert rs.status_code == 200
# US-003 D6: strip sanatate unificat — "declaratiile" apare in orice stare (curg/blocat)
# strip sanatate unificat — "declaratiile" apare in orice stare (curg/blocat)
assert "declaratiile" in rs.text.lower(), (
f"Strip sanatate lipseste din fragment. HTML: {rs.text[:500]}"
)
@@ -73,7 +73,7 @@ def test_nomenclator_fragment_lists_seed(client):
def test_submissions_fragment_empty_state(client):
r = client.get("/_fragments/submissions")
assert r.status_code == 200
# US-005: empty state prietenos cu indemn la Import (nu mesajul tehnic vechi)
# empty state prietenos cu indemn la Import (nu mesajul tehnic vechi)
assert "Nicio trimitere" in r.text or "incepe cu Import" in r.text or "?tab=import" in r.text

View File

@@ -1,7 +1,6 @@
"""Test US-011 (discoverability): linkul 'Panou admin' apare pe dashboard doar pentru admini.
"""Linkul 'Panou admin' apare pe dashboard doar pentru admini.
Completeaza intentia US-011 — adminul trebuie sa poata descoperi /admin din UI, nu doar
prin URL direct.
Adminul trebuie sa poata descoperi /admin din UI, nu doar prin URL direct.
"""
from __future__ import annotations

View File

@@ -1,6 +1,6 @@
"""Teste US-005 (PRD 3.3): scoping dashboard pe sesiune (2 conturi, citiri).
"""Teste scoping dashboard pe sesiune (2 conturi, citiri).
Comportamental (C6): nu grep, ci verificare reala cu 2 conturi + date distincte.
Verificare comportamentala reala cu 2 conturi + date distincte, nu grep.
"""
from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""Teste US-012 (PRD 5.6): un rand `error` nu mai blocheaza retrimiterea (dedup).
"""Un rand `error` nu mai blocheaza retrimiterea (dedup).
La resubmit cu aceeasi cheie de continut peste un rand `error`: se RE-ACTIVEAZA
(reactivated:true + stare noua), creds-urile se actualizeaza. Peste sent/queued/
@@ -82,7 +82,7 @@ def test_resubmit_actualizeaza_creds_pe_reactivare(client):
enc_dupa = _creds_enc(sid)
assert enc_dupa is not None
assert enc_dupa != enc_initial, "creds-urile trebuie actualizate la reactivare"
# US-013: creds propagate in slotul per-env (rar_creds_test_enc, ancora globala=test)
# creds propagate in slotul per-env (rar_creds_test_enc, ancora globala=test)
from app.db import get_connection
from app.crypto import decrypt_creds
conn = get_connection()
@@ -118,7 +118,7 @@ def test_resubmit_peste_queued_ramane_deduped(client):
def test_reactivare_pe_needs_data_expune_erori(client):
"""PRD 5.7: reactivarea unui rand error care re-clasifica needs_data trebuie sa
"""Reactivarea unui rand error care re-clasifica needs_data trebuie sa
expuna erori/motiv (raspuns onest), nu doar status + reactivated."""
# rand initial valid -> queued; il fortam error
r1 = client.post("/v1/prezentari", json=_body())

View File

@@ -1,6 +1,4 @@
"""Teste US-012 (PRD 5.15): Analytics device-mix — validare premisa mobil, fara PII.
TDD: RED inainte de implementare.
"""Teste analytics device-mix — validare premisa mobil, fara PII.
Semnal: la acces dashboard -> eveniment 'device_mix' in app_events cu cod 'desktop'/'mobil'.
Zero PII: nu se stocheaza UA brut, IP sau VIN.

View File

@@ -1,6 +1,4 @@
"""Teste US-012 (PRD 5.20): audit evenimente medii RAR + e2e lant submission.
Fisier nou; nu modifica teste existente.
"""Teste audit evenimente medii RAR + e2e lant submission.
Teste:
test_lant_import_pana_la_queued -- submission ajunge la status=queued cu rar_env corect

View File

@@ -1,7 +1,7 @@
"""US-005 (PRD 5.18) — embeddings indexeaza corpusul etichetat (NU nomenclatorul).
"""Embeddings indexeaza corpusul etichetat (NU nomenclatorul).
k-NN peste exemple reale etichetate (denumire_normalizata -> cod, is_nul) e net mai
precis decat peste cele 18 categorii generice. Acopera si simetria corpus/query (F1):
precis decat peste cele 18 categorii generice. Acopera si simetria corpus/query:
corpusul e text NORMALIZAT, deci query-ul trebuie normalizat la fel inainte de embedding.
"""
@@ -34,7 +34,7 @@ def env(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "us005.db"))
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
monkeypatch.setenv("AUTOPASS_EMBEDDINGS_ENABLED", "true") # US-005 are nevoie de embeddings ON
monkeypatch.setenv("AUTOPASS_EMBEDDINGS_ENABLED", "true") # necesar pentru testele de embeddings
from app.config import get_settings
get_settings.cache_clear()
from app.db import init_db
@@ -115,7 +115,7 @@ def test_semnatura_corpus_pe_seed(conn):
def test_query_normalizat_ca_si_corpusul(conn, monkeypatch):
"""F1 (HIGH): enrich_suggestions interogheaza suggest_nearest cu textul NORMALIZAT."""
"""enrich_suggestions interogheaza suggest_nearest cu textul NORMALIZAT."""
import app.embeddings as emb
captura = {}
monkeypatch.setattr(emb, "has_corpus", lambda: True)

View File

@@ -1,8 +1,8 @@
"""US-006 (PRD 5.18) — enrich_suggestions = pre-filtru NUL + k-NN pe corpus etichetat.
"""enrich_suggestions = pre-filtru NUL + k-NN pe corpus etichetat.
Ordinea de precedenta: pre-filtru NUL -> (daca NUL: fara cod) altfel GOLD partajat >
exact (SILVER) > k-NN embeddings. k-NN sub prag -> abtinere. Vecin k-NN NUL -> supresie.
Invariant #13: nimic din asta nu intra in resolve_prestatii/load_mapping.
Nimic din enrich_suggestions nu intra in resolve_prestatii/load_mapping.
"""
from __future__ import annotations
@@ -123,7 +123,7 @@ def test_vecin_knn_nul_supreseaza(conn, monkeypatch):
def test_invariant_13_resolve_neatins(conn):
"""Regresie #13: SILVER populat NU produce auto-rezolvare in resolve_prestatii."""
"""SILVER populat NU produce auto-rezolvare in resolve_prestatii."""
from app.mapping import resolve_prestatii, normalize_for_match
_silver(conn, normalize_for_match("OPERATIE X"), "OE-1")
resolved, unmapped = resolve_prestatii(

View File

@@ -1,4 +1,4 @@
"""Teste US-001 (PRD 5.6): handler global de excepții -> 500 structurat (3 niveluri) + log."""
"""Teste handler global de exceptii -> 500 structurat (3 niveluri) + log."""
from __future__ import annotations
@@ -39,7 +39,7 @@ def test_exceptie_neasteptata_da_500_structurat(client, monkeypatch):
assert r.status_code == 500
body = r.json()
assert body["cod"] == "EROARE_INTERNA"
# 3 niveluri (PRD 5.4): problema + fix
# 3 niveluri: cod + problema + fix
assert body["problema"]
assert body["fix"]
assert "request_id" in body

View File

@@ -1,7 +1,4 @@
"""Teste pentru app/errors.py — catalog central de erori (US-001 / PRD 5.4).
Urmeaza TDD: testele sunt scrise INAINTE de implementare.
"""
"""Teste pentru app/errors.py — catalog central de erori."""
from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""US-002 (PRD 5.18) — etichetator offline multi-backend cu prompt procedural.
"""Etichetator offline multi-backend cu prompt procedural.
Toate testele ruleaza FARA retea reala (transport injectabil / inspectie body).
Acopera: prompt 3 pasi, envelope json_schema strict + enum, backend selectabil
@@ -70,7 +70,7 @@ def test_parseaza_raspuns_si_garda_truncare():
def test_backend_selectabil_env(monkeypatch):
# Default = lmstudio (backend aprobat v1, D4).
# Default = lmstudio.
monkeypatch.delenv("ETICHETARE_BACKEND", raising=False)
assert eticheteaza.get_backend().name == "lmstudio"
# Selectie prin env.

View File

@@ -1,4 +1,4 @@
"""Teste US-006 (PRD 3.6): "Formate de coloane salvate" randate ca TABEL.
"""Teste "Formate de coloane salvate" randate ca TABEL.
Doar marcaj de template — POST-urile /formate-coloane/editeaza si /formate-coloane/sterge
raman identice. .tablewrap pentru consistenta cu celelalte tabele.

View File

@@ -1,4 +1,4 @@
"""Teste FOUNDATION (Task #1): schema + migrari idempotente + openpyxl disponibil."""
"""Teste schema + migrari idempotente + openpyxl disponibil."""
from __future__ import annotations
@@ -35,11 +35,11 @@ def _tables(conn) -> set[str]:
# --- Coloane noi ---
def test_accounts_rar_creds_enc_dropata(db_conn):
"""US-013: accounts.rar_creds_enc a fost dropata; submissions.rar_creds_enc ramane."""
"""accounts.rar_creds_enc a fost dropata; submissions.rar_creds_enc ramane."""
acc_cols = _table_cols(db_conn, "accounts")
sub_cols = _table_cols(db_conn, "submissions")
assert "rar_creds_enc" not in acc_cols, \
"accounts.rar_creds_enc trebuie sa fie ABSENTA dupa US-013 DROP"
"accounts.rar_creds_enc trebuie sa fie ABSENTA"
assert "rar_creds_enc" in sub_cols, \
"submissions.rar_creds_enc trebuie sa RAMANA (creds efemere per-cerere)"
@@ -158,7 +158,7 @@ def test_migrate_on_existing_db(monkeypatch):
assert "batch_id" in sub_cols
assert "row_index" in sub_cols
assert "purge_after" in sub_cols
# US-013: accounts.rar_creds_enc a fost dropata.
# accounts.rar_creds_enc a fost dropata.
assert "rar_creds_enc" not in acc_cols
conn.close()
get_settings.cache_clear()

View File

@@ -1,10 +1,10 @@
"""US-003 (PRD 5.18) — generare seed etichetat in faze pe frecventa.
"""Generare seed etichetat in faze pe frecventa.
Pipeline dedup OBLIGATORIU inainte de orice apel LLM (D5):
Pipeline dedup OBLIGATORIU inainte de orice apel LLM:
brut -> normalize_for_match -> arunca chei vide -> dedup pe cheie (freq=suma NR)
-> reuse etichete existente (labels-groq + seed comis, conflict freq-max) -> de_etichetat.
Idempotenta cross-run (F2/F7): a doua rulare consuma seedul comis ca cache -> 0 apeluri LLM.
Idempotenta cross-run: a doua rulare consuma seedul comis ca cache -> 0 apeluri LLM.
Toate testele FARA retea: `clasifica` e injectat (mock care inregistreaza ce primeste).
"""
@@ -134,7 +134,7 @@ def test_zero_duplicate_trimis_la_llm(tmp_path):
def test_rerun_zero_apeluri_llm(tmp_path):
"""Criteriul real de idempotenta (F2/F7): a doua rulare = 0 apeluri LLM, seed identic."""
"""Criteriul real de idempotenta: a doua rulare = 0 apeluri LLM, seed identic."""
f = tmp_path / "a.csv"
_scrie_csv(f, [("OP UNU", 10), ("OP DOI", 5)])
seed = tmp_path / "seed.json"

View File

@@ -1,4 +1,4 @@
"""Teste scope cont pe GET /v1/audit/export (US-003, PRD 3.2)."""
"""Teste scope cont pe GET /v1/audit/export."""
from __future__ import annotations
@@ -51,7 +51,7 @@ def test_export_doar_contul_cheii(env):
from app.db import get_connection
conn = get_connection()
try:
# tier='pro' ca sa treaca gate-ul API (T4 PRD 5.17); testul masoara scoping, nu planuri.
# tier='pro' ca sa treaca gate-ul API; testul masoara scoping, nu planuri.
conn.execute("INSERT INTO accounts (id, name, tier) VALUES (2, 'al-doilea', 'pro')")
k1 = create_api_key(conn, 1)
k2 = create_api_key(conn, 2)

View File

@@ -1,6 +1,6 @@
"""Teste scope cont pe GET /v1/mapari + /pending (US-002, PRD 3.2).
"""Teste scope cont pe GET /v1/mapari + /pending.
TD-3.2 (decis la poarta): parametrul ?account_id= din query se pastreaza DAR:
Parametrul ?account_id= din query se pastreaza DAR:
- daca e prezent SI difera de contul cheii -> 400 explicit
- daca e prezent si egal -> ok
- daca lipseste -> contul cheii
@@ -69,7 +69,7 @@ def test_mapari_doar_contul_cheii(env):
def test_mapari_query_account_id_diferit_400(env):
"""Daca ?account_id difera de contul cheii -> 400 explicit (TD-3.2)."""
"""Daca ?account_id difera de contul cheii -> 400 explicit."""
with _client() as c:
from app.auth import create_api_key
from app.db import get_connection
@@ -85,7 +85,7 @@ def test_mapari_query_account_id_diferit_400(env):
def test_mapari_query_account_id_egal_ok(env):
"""Daca ?account_id egal cu contul cheii -> 200 (TD-3.2)."""
"""Daca ?account_id egal cu contul cheii -> 200."""
with _client() as c:
from app.auth import create_api_key
from app.db import get_connection
@@ -161,7 +161,7 @@ def test_pending_web_global_neschimbat(env):
def test_pending_filtreaza_in_sql_cu_regula_null(env):
"""B1: pending_unmapped(conn, account_id=1) include si randuri cu account_id=NULL (legacy).
"""pending_unmapped(conn, account_id=1) include si randuri cu account_id=NULL (legacy).
Filtrarea trebuie sa se faca IN SQL cu:
WHERE status='needs_mapping' AND (account_id=? OR (account_id IS NULL AND ?=1))

View File

@@ -1,8 +1,4 @@
"""Teste scope cont pe GET /v1/prezentari + /{id} (US-001, PRD 3.2).
Metoda TDD: testele se scriu inainte de implementare (RED) si trebuie sa ramana
verzi dupa implementare (GREEN).
"""
"""Teste scope cont pe GET /v1/prezentari + /{id}."""
from __future__ import annotations
@@ -47,7 +43,7 @@ def test_lista_doar_contul_cheii(env):
from app.db import get_connection
conn = get_connection()
try:
# tier='pro' pe ambele conturi — testul verifica scoping GET, nu planuri (T4 PRD 5.17).
# tier='pro' pe ambele conturi — testul verifica scoping GET, nu planuri.
conn.execute("UPDATE accounts SET tier='pro' WHERE id=1")
conn.execute("INSERT INTO accounts (id, name, tier) VALUES (2, 'al-doilea', 'pro')")
k1 = create_api_key(conn, 1)
@@ -77,7 +73,7 @@ def test_lista_doar_contul_cheii(env):
def test_detaliu_cross_account_404(env):
"""GET /{id} cu cheia contului B pentru submission-ul contului A -> 404.
B3: detail-ul 404 cross-account trebuie byte-identic cu cel al unui id inexistent
Raspunsul 404 cross-account trebuie byte-identic cu cel al unui id inexistent
(acelasi status + acelasi mesaj) — nu dam indicii ca randul exista.
"""
with _client() as c:
@@ -103,7 +99,7 @@ def test_detaliu_cross_account_404(env):
def test_legacy_null_vizibil_pentru_cont_1(env):
"""Randuri cu account_id=NULL apartin contului 1 (legacy OV-2); contul 2 nu le vede."""
"""Randuri cu account_id=NULL apartin contului 1 (legacy); contul 2 nu le vede."""
with _client() as c:
from app.auth import create_api_key
from app.db import get_connection
@@ -149,11 +145,11 @@ def test_fara_cheie_flag_off_vede_contul_1(env):
def test_detaliu_nu_expune_creds(env):
"""B4: GET /v1/prezentari/{id} nu expune campuri sensibile (rar_creds_enc, payload_json,
"""GET /v1/prezentari/{id} nu expune campuri sensibile (rar_creds_enc, payload_json,
idempotency_key).
NOTA T9 (PRD 5.6): `rar_error` e ACUM expus intentionat (recovery API observabil) —
contine doar coduri/mesaje de validare RAR, niciodata creds.
`rar_error` e expus intentionat (recovery API observabil) — contine doar
coduri/mesaje de validare RAR, niciodata creds.
"""
with _client() as c:
from app.auth import create_api_key

View File

@@ -1,4 +1,4 @@
"""Teste PRD 5.19 US-010 — onestitate + observabilitate `held` pe canalul API.
"""Onestitate + observabilitate `held` pe canalul API.
- `held` in raspunsul enqueue (SubmissionResult) + motiv non-null.
- `held` in proiectiile GET /v1/prezentari si /v1/prezentari/{id}.

View File

@@ -1,6 +1,6 @@
"""Teste PRD 5.19 US-002 — snapshot `held` la ingestie din comutatorul de cont.
"""Teste snapshot `held` la ingestie din comutatorul de cont.
Acopera TOATE situ-rile care scriu `status='queued'` prin ingestie (chokepoint
Acopera TOATE situatiile care scriu `status='queued'` prin ingestie (chokepoint
`held_for_account`): enqueue API, reactivare error->queued, dedup echo, commit import,
reresolve `needs_mapping`. `held` NU intra in idempotency_key.
@@ -107,7 +107,7 @@ def test_auto_on_ingestie_not_held(client):
# --------------------------------------------------------------------------- #
# Reactivare error->queued (bug de bypass Eng Finding A) #
# Reactivare error->queued #
# --------------------------------------------------------------------------- #
def test_reactivare_respecta_auto_off(client):

View File

@@ -1,9 +1,9 @@
"""Regresie PRD 5.19 US-002 — re-snapshot `held` pe caile de re-punere in coada.
"""Regresie — re-snapshot `held` pe caile de re-punere in coada.
Bug (gasit la /code-review high): create_prezentari (reactivare) si reresolve_account
re-calculau `held` din comutatorul contului la tranzitia -> queued, dar caile de
re-punere din dashboard/admin (requeue_submission, corectie, repune-cu-cod, bulk-fix)
lasau `held` pe valoarea VECHE. Consecinta: un rand ingerat pe Auto ON (held=0) care a
Bug: create_prezentari (reactivare) si reresolve_account re-calculau `held` din
comutatorul contului la tranzitia -> queued, dar caile de re-punere din
dashboard/admin (requeue_submission, corectie, repune-cu-cod, bulk-fix) lasau
`held` pe valoarea VECHE. Consecinta: un rand ingerat pe Auto ON (held=0) care a
esuat, apoi contul trecut pe Auto OFF, la re-punere pastra held=0 -> worker-ul il
auto-trimitea la RAR (FINALIZATA ireversibil) desi contul e Auto OFF.

View File

@@ -1,4 +1,4 @@
"""Teste TDD pentru tools/mapare-llm/heldout_eval.py (L14-S5).
"""Teste pentru tools/mapare-llm/heldout_eval.py.
Fixture sintetic cu predictii+ground_truth cunoscute. Verifica:
- precizie globala
@@ -398,8 +398,7 @@ class TestExportForLabeling:
for row in reader:
# Coloana cod_gold trebuie sa fie vida (nu etichetata de cod!)
assert row["cod_gold"] == "", (
"cod_gold nu trebuie pre-completat: ar fi 'antrenare pe test' "
"(Decision #19 PRD 5.14)"
"cod_gold nu trebuie pre-completat: ar fi 'antrenare pe test'"
)
def test_n_randuri_egal_cu_sample(self, tmp_path):
@@ -413,7 +412,7 @@ class TestExportForLabeling:
# ---------------------------------------------------------------------------
# Sectiunea 8: kill_criterion — pragul de acceptanta (F-E, PRD 5.14)
# Sectiunea 8: kill_criterion — pragul de acceptanta
# ---------------------------------------------------------------------------
class TestKillCriterion:
@@ -424,8 +423,8 @@ class TestKillCriterion:
Justificare threshold 0.5% (0.005):
Un service cu 200 operatii/zi auto-rezolvate = 1 FINALIZATA gresita/zi.
FINALIZATA e ireversibila (cf. PRD 5.14 Premisa 3 / invariant CLAUDE.md).
Pragul poate fi RELAXAT empiric; nu INASPRIT post-hoc (sesizare-in-timp).
FINALIZATA e ireversibila.
Pragul poate fi RELAXAT empiric; nu INASPRIT post-hoc.
"""
def test_trece_cand_sub_prag(self):

View File

@@ -1,4 +1,4 @@
"""US-003 (PRD 5.20): build_key incorporeaza rar_env."""
"""build_key incorporeaza rar_env."""
from __future__ import annotations

View File

@@ -1,10 +1,10 @@
"""Teste API import Treapta 2 — POST /v1/import, preview, commit, export-failed.
"""Teste API import — POST /v1/import, preview, commit, export-failed.
Acopera:
- #11 U1+T4: upload + staging + mapare coloane semnatura/drift/fuzzy
- #12 T2+T11: preview 6 stari + already_sent batch lookup + intra-batch collision
- #13 T5+T12: gate HARD confirmare + atestare valori + commit ON CONFLICT (TOCTOU)
- #14 T8: export randuri esuate CSV
- upload + staging + mapare coloane semnatura/drift/fuzzy
- preview 6 stari + already_sent batch lookup + intra-batch collision
- gate HARD confirmare + atestare valori + commit ON CONFLICT (TOCTOU)
- export randuri esuate CSV
"""
from __future__ import annotations
@@ -91,15 +91,11 @@ def _default_column_mapping() -> dict:
def _setup_nomenclator(client: TestClient) -> None:
"""Seed nomenclator cu un cod de prestatie pentru teste."""
# Folosim POST /v1/prezentari pentru a forta seed-ul nomenclatorului
# care are loc in init_db -> seed_nomenclator_if_empty
pass # seed-ul se face automat in init_db
def _seed_operation_mapping(client: TestClient, cod_op: str = "Revizie", cod_prest: str = "OE-1") -> None:
"""Salveaza o mapare de operatii pentru teste."""
# Adauga mai intai in nomenclator daca nu exista (prin POST prezentare care creeaza cod)
# De fapt, cod OE-1 e in nomenclatorul seed
client.post("/v1/mapari", json={
"cod_op_service": cod_op,
"cod_prestatie": cod_prest,
@@ -108,7 +104,7 @@ def _seed_operation_mapping(client: TestClient, cod_op: str = "Revizie", cod_pre
# =========================================================================== #
# #11 — Upload + staging (U1+T4) #
# Upload + staging #
# =========================================================================== #
class TestUploadStaging:
@@ -144,7 +140,7 @@ class TestUploadStaging:
assert r.status_code == 422
def test_issue5a_raw_json_criptat(self, client):
"""Issue 5a: raw_json din import_rows trebuie sa fie criptat (ciphertext la rest)."""
"""raw_json din import_rows trebuie sa fie criptat (ciphertext la rest)."""
data = _make_xlsx([_HEADER, _ROW_OK])
r = _upload_file(client, data, "test.xlsx")
assert r.status_code == 200
@@ -173,7 +169,7 @@ class TestUploadStaging:
conn.close()
def test_issue5b_fuzzy_coloane_refoloseste_normalize_for_match(self, client):
"""Issue 5b: fuzzy_suggestions din raspuns foloseste normalize_for_match (fara duplicat)."""
"""fuzzy_suggestions din raspuns foloseste normalize_for_match (fara duplicat)."""
data = _make_xlsx([_HEADER, _ROW_OK])
r = _upload_file(client, data, "test.xlsx")
assert r.status_code == 200
@@ -190,7 +186,7 @@ class TestUploadStaging:
assert "odometru_final" in camps, f"'odometru_final' trebuie in sugestii"
def test_drift_semnatura_coloane(self, client):
"""T4/D3: upload 2 cu coloane mutate -> mapping_status='new' (nu aplica orb)."""
"""Upload 2 cu coloane mutate -> mapping_status='new' (nu aplica orb)."""
# Upload 1 cu header standard
data1 = _make_xlsx([_HEADER, _ROW_OK])
r1 = _upload_file(client, data1, "test.xlsx")
@@ -275,7 +271,7 @@ class TestUploadStaging:
assert body["sample_rows"][0]["VIN"] == _ROW_OK2[0]
def test_purge_after_setat_la_insert(self, client):
"""T16: purge_after trebuie setat la insert import_batches."""
"""purge_after trebuie setat la insert import_batches."""
data = _make_xlsx([_HEADER, _ROW_OK])
r = _upload_file(client, data, "test.xlsx")
import_id = r.json()["import_id"]
@@ -294,7 +290,7 @@ class TestUploadStaging:
# =========================================================================== #
# #11 — Mapare coloane (T4) #
# Mapare coloane #
# =========================================================================== #
class TestColumnMapping:
@@ -349,7 +345,7 @@ class TestColumnMapping:
# =========================================================================== #
# #12 — Preview 6 stari (T2 + T11) #
# Preview 6 stari #
# =========================================================================== #
class TestPreview:
@@ -405,7 +401,7 @@ class TestPreview:
assert any(r["resolved_status"] in ("needs_data",) for r in body["rows"])
def test_preview_already_sent_dupa_submit(self, client):
"""Rand deja trimis prin API -> stare already_sent la preview (T2/D5)."""
"""Rand deja trimis prin API -> stare already_sent la preview."""
# Trimite prin API canalul standard
client.post("/v1/prezentari", json={
"rar_credentials": {"email": "x@y.ro", "password": "s"},
@@ -430,7 +426,7 @@ class TestPreview:
assert "already_sent" in statuses, f"Asteptat 'already_sent', primit: {statuses}"
def test_preview_duplicate_in_file(self, client):
"""T11/OV-3: 2 randuri identice in ACELASI fisier -> duplicate_in_file."""
"""2 randuri identice in ACELASI fisier -> duplicate_in_file."""
# Acelasi rand de doua ori
rows = [_HEADER, _ROW_OK, _ROW_OK] # duplicat exact
import_id = self._upload_and_map(client, rows=rows)
@@ -444,7 +440,7 @@ class TestPreview:
f"Asteptat 'duplicate_in_file', primit: {statuses}"
def test_preview_already_sent_batch_lookup_nu_n_plus_1(self, client):
"""Eng#5: already_sent lookup BATCH (nu N+1) — ≤7 interogari pentru 5 randuri."""
"""already_sent lookup BATCH (nu N+1) — ≤7 interogari pentru 5 randuri."""
# Cream 5 randuri distincte
rows_data = [_HEADER]
for i in range(5):
@@ -492,7 +488,7 @@ class TestPreview:
# =========================================================================== #
# #13 — Commit gate HARD + atestare + TOCTOU (T5 + T12) #
# Commit gate HARD + atestare + TOCTOU #
# =========================================================================== #
class TestCommit:
@@ -528,7 +524,7 @@ class TestCommit:
assert body["enqueued"] == n_ok
def test_commit_cu_n_gresit_reject(self, client):
"""T5/D3: commit cu N gresit -> 422, nu enqueue."""
"""Commit cu N gresit -> 422, nu enqueue."""
import_id, preview = self._upload_preview_ok(client)
n_ok = preview["summary"].get("ok", 0)
@@ -543,7 +539,7 @@ class TestCommit:
f"Eroare neasteptata: {detail}"
def test_commit_log_atestare(self, client):
"""T12/Voce#9: commit scrie import_attestations cu rows_hash + n_confirmed."""
"""Commit scrie import_attestations cu rows_hash + n_confirmed."""
import_id, preview = self._upload_preview_ok(client)
n_ok = preview["summary"].get("ok", 0)
@@ -574,7 +570,7 @@ class TestCommit:
conn.close()
def test_commit_batch_id_setat_pe_submission(self, client):
"""T7: submission creata la commit trebuie sa aiba batch_id + row_index setate."""
"""Submission creata la commit trebuie sa aiba batch_id + row_index setate."""
import_id, preview = self._upload_preview_ok(client)
n_ok = preview["summary"].get("ok", 0)
@@ -602,7 +598,7 @@ class TestCommit:
conn.close()
def test_commit_toctou_cheie_inserata_concurent(self, client):
"""Issue 1 (TOCTOU): cheie inserata de canal concurent -> reclasificata already_sent.
"""Cheie inserata de canal concurent (TOCTOU) -> reclasificata already_sent.
Simulam TOCTOU inserand cheia direct in submissions inainte de commit.
"""
@@ -635,7 +631,6 @@ class TestCommit:
"reviewed_rows": [],
})
# Poate fi 200 cu toctou_collisions sau 422 cu informatii clare
# Conform planului Issue 1: reclasificat already_sent, nu rollback
# Dar n_enqueued va fi 0 daca toate colideaza
if rc.status_code == 200:
body = rc.json()
@@ -644,7 +639,7 @@ class TestCommit:
# (dupa reclasificare, n_total_ok scade)
def test_commit_needs_review_nebifat_exclus_din_n(self, client):
"""Voce#1: rand needs_review nebifat explicit -> NU intra in N, NU se enqueued."""
"""Rand needs_review nebifat explicit -> NU intra in N, NU se enqueued."""
# Rand cu VIN numeric (coercion -> needs_review)
import datetime as dt
wb = openpyxl.Workbook()
@@ -704,7 +699,7 @@ class TestCommit:
assert r2.status_code == 409
def test_atestare_purge_after_setat_pe_submission(self, client):
"""T16: submissions create la commit trebuie sa aiba purge_after setat."""
"""Submissions create la commit trebuie sa aiba purge_after setat."""
import_id, preview = self._upload_preview_ok(client)
n_ok = preview["summary"].get("ok", 0)
@@ -732,7 +727,7 @@ class TestCommit:
# =========================================================================== #
# #14 — Export randuri esuate CSV (T8) #
# Export randuri esuate CSV #
# =========================================================================== #
class TestExportFailed:
@@ -806,12 +801,12 @@ class TestExportFailed:
# =========================================================================== #
# Regresie: reconcile.py ramane op-blind (OV-3) #
# Regresie: reconcile.py ramane op-blind #
# =========================================================================== #
class TestReconcileRegresie:
def test_match_finalizata_ramane_op_blind(self):
"""OV-3: reconcile.py trebuie sa ramana op-blind (nu editat de import).
"""reconcile.py trebuie sa ramana op-blind (nu editat de import).
Importam reconcile si verificam ca nu s-au adaugat parametri de operatie.
"""

View File

@@ -1,6 +1,5 @@
"""Teste US-007 (PRD 5.11) — Post-commit: lista Trimiteri apare + refresh auto.
"""Teste post-commit: lista Trimiteri apare + refresh auto.
TDD RED: testele sunt scrise inainte de implementare.
Verifica:
1. Raspunsul confirma emite header HX-Trigger: trimiteriChanged.
2. Raspunsul confirma include OOB swap al #trimiteri-section cu submissions-wrap.
@@ -130,7 +129,7 @@ _ROWS_OK = [
# --------------------------------------------------------------------------- #
# Teste RED → GREEN #
# Teste #
# --------------------------------------------------------------------------- #
def test_confirma_emite_hx_trigger(client):

View File

@@ -1,16 +1,16 @@
"""Teste E2E T15 — integrare import->commit->worker (RAR mock) + masina de stari + failure registry.
"""Teste E2E: integrare import->commit->worker (RAR mock) + masina de stari + failure registry.
Acopera (plan T15, sect.12):
Acopera:
- Scenariul 1: upload xlsx/csv -> column-mapping -> preview -> commit N
-> worker process_one (MockRar) -> submission FINALIZATA cu id_prezentare.
- Scenariul 2: re-upload acelasi continut -> preview marcheaza already_sent
(NU al doilea FINALIZATA dupa commit).
- Scenariul 3: coada MIXTA API(creds efemere)+web(creds durabile per-env, US-013):
- Scenariul 3: coada mixta API(creds efemere)+web(creds durabile per-env):
dupa login + purjare creds efemere, submission-urile web tot se trimit
prin fallback accounts.rar_creds_{env}_enc (T1/Voce#5).
- Masina de stari (sect. 6): tranzitii queued->sending->sent/requeued/error.
- Failure registry (sect. 8): 400/403/503+reconciliere.
- T16: purge_expired + purge_after setat la commit.
prin fallback accounts.rar_creds_{env}_enc.
- Masina de stari: tranzitii queued->sending->sent/requeued/error.
- Failure registry: 400/403/503+reconciliere.
- Purjare: purge_expired + purge_after setat la commit.
"""
from __future__ import annotations
@@ -451,10 +451,10 @@ class TestE2EAlreadySent:
class TestE2EMixedQueue:
def test_coada_mixta_ambele_trimise(self, env, monkeypatch):
"""Scenariul 3 (T1/Voce#5): API (creds efemere) + web (canal import, fara creds pe submission).
"""Scenariul 3: API (creds efemere) + web (canal import, fara creds pe submission).
Dupa purjarea creds efemere ale canalului API (la primul login), submission-urile
import (web) tot se trimit prin fallback accounts.rar_creds_{env}_enc (US-013, per-env).
import (web) tot se trimit prin fallback accounts.rar_creds_{env}_enc, per-env.
Verificam ca ambele submission-uri ajung la status='sent'.
"""
import app.worker.__main__ as w
@@ -486,11 +486,11 @@ class TestE2EMixedQueue:
assert r_api.status_code == 200, r_api.text
sub_api_id = r_api.json()["results"][0]["submission_id"]
# 3. Seteaza creds durabile in accounts per-env (US-013, canal web fallback)
# 3. Seteaza creds durabile in accounts per-env (canal web fallback)
conn = get_connection()
try:
web_creds_enc = encrypt_creds({"email": "web@test.ro", "password": "pass_web"})
# US-013: scrie in slotul per-env test (rar_env default = 'test')
# scrie in slotul per-env test (rar_env default = 'test')
conn.execute(
"UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1 WHERE id=1",
(web_creds_enc,),
@@ -522,7 +522,7 @@ class TestE2EMixedQueue:
try:
processed = 0
# Drena coada simuland bucla worker completa (T1/D4: creds efemere + fallback per-env)
# Drena coada simuland bucla worker completa: creds efemere + fallback per-env
for _ in range(10): # limita de siguranta
claimed = w.claim_one(conn)
if claimed is None:
@@ -532,7 +532,7 @@ class TestE2EMixedQueue:
account_id = claimed["account_id"]
rar_env = claimed.get("rar_env", "test")
# T1/D4: creds din submission (API efemer) OR fallback per-env (US-013)
# creds din submission (API efemer) OR fallback per-env
creds = w._creds_for(claimed, settings) or w._creds_from_account(conn, account_id, rar_env=rar_env)
assert creds is not None, \
f"Creds None pentru submission {sid} — fallback durabil trebuie sa existe"
@@ -566,9 +566,9 @@ class TestE2EMixedQueue:
assert mock_rar.post_calls == 2
def test_purjare_creds_efemere_nu_sterge_durabile(self, env, monkeypatch):
"""T1/Gate purjare (OV-5): dupa login, submissions.rar_creds_enc sterse DAR per-env INTACT.
"""Dupa login, submissions.rar_creds_enc sterse DAR per-env INTACT.
Worker sterge DOAR submissions.rar_creds_enc (efemere), NU accounts.rar_creds_{env}_enc (durabile, US-013).
Worker sterge DOAR submissions.rar_creds_enc (efemere), NU accounts.rar_creds_{env}_enc (durabile).
"""
import app.worker.__main__ as w
from app.crypto import encrypt_creds
@@ -590,7 +590,7 @@ class TestE2EMixedQueue:
assert r_api.status_code == 200
sub_id = r_api.json()["results"][0]["submission_id"]
# Seteaza creds durabile pe cont (slotul per-env, US-013)
# Seteaza creds durabile pe cont (slotul per-env)
conn = get_connection()
try:
web_creds_enc = encrypt_creds({"email": "web@test.ro", "password": "pass_web"})
@@ -634,7 +634,7 @@ class TestE2EMixedQueue:
# =========================================================================== #
# Masina de stari (sect. 6 din plan): tranzitii explicite per submission #
# Masina de stari: tranzitii explicite per submission #
# =========================================================================== #
class TestStateMachine:
@@ -797,7 +797,7 @@ class TestStateMachine:
# =========================================================================== #
# Registrul de esecuri (sect. 8): tipuri de eroare RAR si recuperare #
# Registrul de esecuri: tipuri de eroare RAR si recuperare #
# =========================================================================== #
class TestFailureRegistry:
@@ -929,12 +929,12 @@ class TestFailureRegistry:
# =========================================================================== #
# T16 — Purjare (purge_expired + purge_after setat la commit) #
# Purjare (purge_expired + purge_after setat la commit) #
# =========================================================================== #
class TestPurjare:
def test_purge_expired_sterge_sent_expirate(self, env):
"""T16: purge_expired sterge submissions cu status='sent' si purge_after in trecut."""
"""purge_expired sterge submissions cu status='sent' si purge_after in trecut."""
from app.worker.__main__ import purge_expired
client, get_connection, get_settings = env
@@ -958,7 +958,7 @@ class TestPurjare:
conn.close()
def test_purge_expired_nu_sterge_queued(self, env):
"""T16: purge_expired NU sterge non-sent (queued/error) chiar daca purge_after < now."""
"""purge_expired NU sterge non-sent (queued/error) chiar daca purge_after < now."""
from app.worker.__main__ import purge_expired
client, get_connection, get_settings = env
@@ -981,7 +981,7 @@ class TestPurjare:
conn.close()
def test_purge_after_setat_la_commit_import(self, env):
"""T16: submissions create la commit import au purge_after setat (!=NULL)."""
"""submissions create la commit import au purge_after setat (!=NULL)."""
client, get_connection, get_settings = env
import_id = _upload_and_map(client, rows=[_HEADER, _ROW_1])
@@ -1005,7 +1005,7 @@ class TestPurjare:
conn.close()
def test_purge_after_setat_la_trimitere_worker(self, env):
"""T16: dupa trimitere (status='sent'), purge_after se (re)seteaza la now+90 zile."""
"""dupa trimitere (status='sent'), purge_after se (re)seteaza la now+90 zile."""
import app.worker.__main__ as w
client, get_connection, get_settings = env

View File

@@ -1,10 +1,8 @@
"""Teste US-001 (PRD 3.6): backend — persista editarea unui rand de preview.
"""Teste backend — persista editarea unui rand de preview.
Approach B: editarea scrie un patch CANONIC in import_rows.override_json (criptat
Editarea scrie un patch CANONIC in import_rows.override_json (criptat
Fernet), aplicat ULTIMUL in _resolve_row_for_preview + commit. raw_json/idempotency
raman neatinse. Ruta editare = mutatie PURA (nu re-deriva status, nu atinge submissions).
TDD: testele se scriu inainte de implementare (RED -> GREEN).
"""
from __future__ import annotations

Some files were not shown because too many files have changed in this diff Show More