diff --git a/app/api/v1/router.py b/app/api/v1/router.py
index ecad129..e9c05b0 100644
--- a/app/api/v1/router.py
+++ b/app/api/v1/router.py
@@ -77,13 +77,61 @@ def _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode) ->
def _erori_nemapate(unmapped: list[dict]) -> list[dict]:
- """Erori 3 niveluri (COD_NEMAPAT) pentru raspunsul on_unmapped_error=True."""
+ """Coduri nemapate imbogatite cu 3 niveluri (COD_NEMAPAT), pentru raspuns onest."""
return [
{**u, **err_eroare("COD_NEMAPAT", cauza=f"cod {u.get('cod_op_service')} necunoscut/fara mapare RAR")}
for u in unmapped
]
+def _motiv_clasificare(cl: dict) -> str | None:
+ """Rezumat uman pe o linie pentru un rezultat de clasificare (PRD 5.7).
+
+ None cand status='queued'. Acopera toate ramurile de blocaj: erori de continut
+ (needs_data), coduri nemapate (needs_mapping) si auto_send oprit (needs_mapping).
+ """
+ if cl["status"] == "queued":
+ return None
+ if cl["errors"]:
+ return "; ".join(
+ (e.get("problema") or e.get("message") or "") for e in cl["errors"]
+ ).strip("; ") or "Date incomplete (respinse de RAR)."
+ if cl["unmapped"]:
+ coduri = ", ".join((u.get("cod_op_service") or "") for u in cl["unmapped"])
+ return f"Coduri fara mapare RAR: {coduri}"
+ if cl["status"] == "needs_mapping":
+ return "Cod cu trimitere automata oprita; confirmare manuala inainte de trimitere."
+ return None
+
+
+def _rezultat_enqueue(submission_id: int | None, cl: dict, **extra) -> SubmissionResult:
+ """SubmissionResult onest dintr-un rezultat de clasificare (PRD 5.7).
+
+ Populeaza erori (validare continut), nemapate (coduri fara mapare) si motiv (uman)
+ pentru orice status != 'queued'. Aditiv: pe 'queued' toate raman goale/None.
+ """
+ return SubmissionResult(
+ submission_id=submission_id,
+ status=cl["status"],
+ erori=list(cl["errors"]),
+ nemapate=_erori_nemapate(cl["unmapped"]),
+ motiv=_motiv_clasificare(cl),
+ **extra,
+ )
+
+
+def _rezultat_respins(submission_id: int | None, cl: dict) -> SubmissionResult:
+ """Rezultat pentru on_unmapped_error=True: status='error', fara enqueue/reactivare.
+
+ `erori` pastreaza COD_NEMAPAT (compat clienti vechi); `nemapate` + `motiv` adaugate.
+ """
+ nem = _erori_nemapate(cl["unmapped"])
+ return SubmissionResult(
+ submission_id=submission_id, status="error",
+ erori=nem, nemapate=nem, motiv=_motiv_clasificare(cl),
+ )
+
+
@router.post("/prezentari", response_model=PrezentariResponse)
def create_prezentari(
req: PrezentareRequest,
@@ -142,10 +190,7 @@ def create_prezentari(
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode)
if cl["blocked_error"]:
# on_unmapped_error=True: nu reactivam; randul ramane 'error'.
- results.append(SubmissionResult(
- submission_id=existing["id"], status="error",
- erori=_erori_nemapate(cl["unmapped"]),
- ))
+ results.append(_rezultat_respins(existing["id"], cl))
continue
cur = conn.execute(
"UPDATE submissions SET status=?, payload_json=?, rar_error=?, "
@@ -163,9 +208,9 @@ def create_prezentari(
"UPDATE accounts SET rar_creds_enc=? WHERE id=?",
(encrypt_creds(req.rar_credentials.model_dump()), acct),
)
- results.append(SubmissionResult(
- submission_id=existing["id"], status=cl["status"], reactivated=True,
- ))
+ # Raspuns onest si la reactivare (PRD 5.7): daca re-clasificarea
+ # cade pe needs_data/needs_mapping, expune motivul (nu doar status).
+ results.append(_rezultat_enqueue(existing["id"], cl, reactivated=True))
continue
# Cursa: alt POST/requeue a schimbat starea intre SELECT si UPDATE
# (rowcount==0) -> raspuns dedup pe starea CURENTA.
@@ -188,16 +233,15 @@ def create_prezentari(
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode)
if cl["blocked_error"]:
# on_unmapped_error=True: respinge fara enqueue (cod necunoscut/nemapat).
- results.append(SubmissionResult(
- submission_id=None, status="error", erori=_erori_nemapate(cl["unmapped"]),
- ))
+ results.append(_rezultat_respins(None, cl))
continue
cur = conn.execute(
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error, rar_creds_enc) "
"VALUES (?, ?, ?, ?, ?, ?)",
(key, acct, cl["status"], json.dumps(cl["content"], ensure_ascii=False), cl["rar_error"], creds_enc),
)
- results.append(SubmissionResult(submission_id=int(cur.lastrowid), status=cl["status"]))
+ # Raspuns onest (PRD 5.7): pe needs_data/needs_mapping expune erori/nemapate/motiv.
+ results.append(_rezultat_enqueue(int(cur.lastrowid), cl))
# US-004: audit cerere API per cont. Doar metadate (count + distributie status),
# NICIUN camp de payload PII integral. Reuse conn (T4 — fara contentie WAL).
diff --git a/app/models.py b/app/models.py
index 3fec07b..8315450 100644
--- a/app/models.py
+++ b/app/models.py
@@ -106,9 +106,15 @@ class SubmissionResult(BaseModel):
# cheie de continut a fost RE-ACTIVAT (re-clasificat + creds actualizate) la resubmit.
# `deduped` pastreaza semantica actuala (clientii vechi care testeaza `deduped` nu se sparg).
reactivated: bool = False
- # Populat cand status='error' din cauza on_unmapped='error': erori 3 niveluri
- # (COD_NEMAPAT) pentru fiecare cod necunoscut/nemapat. Gol altfel.
+ # Raspuns ONEST pentru randuri blocate (PRD 5.7): orice status != 'queued' isi
+ # expune motivul, ca integratorul sa nu trateze un needs_data/needs_mapping drept succes.
+ # erori = validare de continut (needs_data), 3 niveluri [{field, cod, problema, cauza, fix, message}].
+ # Pe ramura on_unmapped_error='error' pastreaza COD_NEMAPAT (compat).
+ # nemapate = coduri fara mapare RAR (needs_mapping / respins), 3 niveluri + cod_op_service/denumire.
+ # motiv = rezumat uman pe o linie (None cand status='queued').
erori: list[dict] = []
+ nemapate: list[dict] = []
+ motiv: str | None = None
class PrezentariResponse(BaseModel):
diff --git a/app/web/routes.py b/app/web/routes.py
index bd00333..20c0faa 100644
--- a/app/web/routes.py
+++ b/app/web/routes.py
@@ -771,10 +771,57 @@ def _payload_form_values(payload_json) -> dict:
}
+def _nemapate_pentru_submission(row, nomenclator: list[dict]) -> list[dict]:
+ """Operatiile nemapate ale UNUI submission needs_mapping, cu sugestii fuzzy (PRD 5.7).
+
+ Echivalentul `pending_unmapped` restrans la un singur rand: parseaza payload_json,
+ aduna prestatiile fara cod_prestatie (cu cod_op_service) si ataseaza sugestii din
+ `nomenclator` (pasat de apelant — evita un SELECT redundant in _detaliu_ctx). Goala
+ daca randul nu e needs_mapping sau nu are operatii nemapate reale (ex. needs_mapping
+ din auto_send=0 — codul exista deja, doar trimiterea e oprita).
+ """
+ if row["status"] != "needs_mapping":
+ return []
+ try:
+ content = json.loads(row["payload_json"]) if row["payload_json"] else {}
+ if not isinstance(content, dict):
+ content = {}
+ except (ValueError, TypeError):
+ content = {}
+ seen: set[str] = set()
+ out: list[dict] = []
+ for item in content.get("prestatii") or []:
+ if not isinstance(item, dict) or (item.get("cod_prestatie") or ""):
+ continue
+ op = (item.get("cod_op_service") or "").strip()
+ if not op or op in seen:
+ continue
+ seen.add(op)
+ out.append({
+ "cod_op_service": op,
+ "denumire": item.get("denumire"),
+ "suggestions": suggest_codes(item.get("denumire"), nomenclator, limit=5),
+ })
+ return out
+
+
def _detaliu_ctx(request: Request, row, *, message: str | None = None,
- error: bool = False, corectie_errors: list | None = None) -> dict:
- """Context pentru _trimitere_detaliu.html dintr-un rand de submission."""
+ error: bool = False, corectie_errors: list | None = None,
+ conn=None, account_id: int | None = None) -> dict:
+ """Context pentru _trimitere_detaliu.html dintr-un rand de submission.
+
+ `conn`+`account_id` (optional): cand sunt date si randul e needs_mapping, expune
+ `nemapate_inline` + `nomenclator` pentru maparea inline din panou (PRD 5.7).
+ """
eticheta = eticheta_stare(row["status"])
+ nemapate_inline: list[dict] = []
+ nomenclator: list[dict] = []
+ if conn is not None and row["status"] == "needs_mapping":
+ # Un singur SELECT pe nomenclator: il refolosim si pentru sugestii si pentru dropdown.
+ nomenclator = load_nomenclator(conn)
+ nemapate_inline = _nemapate_pentru_submission(row, nomenclator)
+ if not nemapate_inline:
+ nomenclator = [] # nu expunem dropdown-ul cand nu exista operatii de mapat
ctx = {
"request": request,
"csrf_token": get_csrf_token(request),
@@ -797,6 +844,9 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
"editabil": row["status"] in _CORECTABILE,
# US-011: error/needs_data/needs_mapping pot fi sterse / re-puse in coada
"gestionabil": row["status"] in _GESTIONABILE_WEB,
+ # PRD 5.7: mapare inline (operatii nemapate ale acestui rand + nomenclator)
+ "nemapate_inline": nemapate_inline,
+ "nomenclator": nomenclator,
"corectie_msg": message,
"corectie_error": error,
"corectie_errors": corectie_errors or [],
@@ -829,7 +879,64 @@ def fragment_trimitere_detaliu(request: Request, submission_id: int) -> HTMLResp
row = _fetch_submission_scoped(conn, account_id, submission_id)
if not row:
raise HTTPException(status_code=404, detail="trimitere inexistenta")
- return templates.TemplateResponse("_trimitere_detaliu.html", _detaliu_ctx(request, row))
+ return templates.TemplateResponse(
+ "_trimitere_detaliu.html",
+ _detaliu_ctx(request, row, conn=conn, account_id=account_id),
+ )
+ finally:
+ conn.close()
+
+
+@router.post("/trimitere/{submission_id}/mapeaza", response_class=HTMLResponse)
+async def post_mapeaza_inline(request: Request, submission_id: int) -> HTMLResponse:
+ """Mapare inline din panoul de detaliu (PRD 5.7): alege cod RAR pentru o operatie nemapata.
+
+ Reutilizeaza EXACT save_mapping + reresolve_account (ca tab-ul Mapari) — fara logica
+ noua de clasificare. Re-rezolva scoped pe batch-ul randului (canal API batch_id IS NULL
+ SAU import batch), deblocand si randurile-frate cu aceeasi operatie. Scoped pe sesiune
+ (404 cross-account/inexistent), CSRF obligatoriu, gard pe status needs_mapping.
+ """
+ account_id = require_login(request)
+ form = await request.form()
+ verify_csrf(request, str(form.get("csrf_token") or ""))
+ cod_op_service = str(form.get("cod_op_service") or "").strip()
+ cod_prestatie = str(form.get("cod_prestatie") or "").strip().upper()
+ auto_send = str(form.get("auto_send") or "") not in ("", "false", "0", "off")
+ conn = get_connection()
+ try:
+ row = _fetch_submission_scoped(conn, account_id, submission_id)
+ if not row:
+ raise HTTPException(status_code=404, detail="trimitere inexistenta")
+ if row["status"] != "needs_mapping":
+ raise HTTPException(status_code=403, detail="trimitere fara operatii de mapat")
+ if not cod_op_service or not cod_prestatie:
+ return templates.TemplateResponse(
+ "_trimitere_detaliu.html",
+ _detaliu_ctx(request, row, conn=conn, account_id=account_id, error=True,
+ message="Alege un cod RAR pentru operatie."),
+ )
+ exists = conn.execute(
+ "SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod_prestatie,)
+ ).fetchone()
+ if not exists:
+ return templates.TemplateResponse(
+ "_trimitere_detaliu.html",
+ _detaliu_ctx(request, row, conn=conn, account_id=account_id, error=True,
+ message=f"Cod necunoscut in nomenclator: {cod_prestatie}."),
+ )
+ save_mapping(conn, account_id, cod_op_service, cod_prestatie, auto_send)
+ # Re-rezolva scoped pe canalul randului: batch_id None (API) sau batch import.
+ reresolve_account(conn, account_id, batch_id=row["batch_id"])
+ row2 = _fetch_submission_scoped(conn, account_id, submission_id)
+ eticheta = eticheta_stare(row2["status"])
+ resp = templates.TemplateResponse(
+ "_trimitere_detaliu.html",
+ _detaliu_ctx(request, row2, conn=conn, account_id=account_id,
+ message=f"Mapat {cod_op_service} -> {cod_prestatie}. "
+ f"Stare noua: {eticheta[0]}."),
+ )
+ resp.headers["HX-Trigger"] = "trimiteriChanged"
+ return resp
finally:
conn.close()
@@ -895,8 +1002,8 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
row2 = _fetch_submission_scoped(conn, account_id, submission_id)
return templates.TemplateResponse(
"_trimitere_detaliu.html",
- _detaliu_ctx(request, row2, error=True,
- message="Lipseste inca un cod RAR — rezolva operatia in tab-ul Mapari."),
+ _detaliu_ctx(request, row2, conn=conn, account_id=account_id, error=True,
+ message="Lipseste inca un cod RAR — alege-l mai jos sau in tab-ul Mapari."),
)
if has_no_auto_send(resolved, mapping_meta):
@@ -999,7 +1106,8 @@ async def post_repune_trimitere(request: Request, submission_id: int) -> HTMLRes
row = _fetch_submission_scoped(conn, account_id, submission_id)
resp = templates.TemplateResponse(
"_trimitere_detaliu.html",
- _detaliu_ctx(request, row, message="Re-pus in coada — pleaca la urmatoarea trimitere."),
+ _detaliu_ctx(request, row, conn=conn, account_id=account_id,
+ message="Re-pus in coada — pleaca la urmatoarea trimitere."),
)
resp.headers["HX-Trigger"] = "trimiteriChanged"
return resp
diff --git a/app/web/templates/_trimitere_detaliu.html b/app/web/templates/_trimitere_detaliu.html
index c108884..41692aa 100644
--- a/app/web/templates/_trimitere_detaliu.html
+++ b/app/web/templates/_trimitere_detaliu.html
@@ -1,4 +1,5 @@
{% from "_eroare.html" import card_erori %}
+{% import '_macros.html' as ui %}
Detaliu trimitere #{{ id }}
@@ -65,6 +66,53 @@
{% endif %}
+ {# === Mapare inline (PRD 5.7): alege cod RAR pentru operatiile nemapate ale acestui rand === #}
+ {% if nemapate_inline %}
+
+
Mapeaza codul operatiei
+
+ Alege codul RAR pentru fiecare operatie. La salvare, randul se re-rezolva pe loc
+ (si celelalte randuri cu aceeasi operatie).
+
+ {% for op in nemapate_inline %}
+ {% set top = op.suggestions[0] if op.suggestions else None %}
+ {% set preselect = top.cod_prestatie if (top and top.score >= 60) else '' %}
+
+ {% endfor %}
+
+ {% endif %}
+
{# === Corectie inline (US-010): doar randuri ne-trimise blocate === #}
{% if editabil %}
{% set err_map = {} %}
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index eb4150d..93cc21e 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -48,7 +48,7 @@ Reguli de contract (detalii in `docs/api-rar-contract.md`): `FINALIZATA` e termi
> PRD-uri (`docs/prd/prd-X.Y-*.md`), linkate in coloana Detalii. La fiecare livrabila terminata:
> schimba statusul + data + linkul PRD si actualizeaza "Ultima actualizare".
-**Ultima actualizare**: 2026-06-23 — FIX out-of-process (raportat din client VFP): `cod_prestatie` necunoscut in nomenclator era trimis raw la RAR → **HTTP 500** (`ORA-12899`, coloana `COD_PRESTATIE` max 5 car.) + record PARTIAL `FINALIZATA` (RAR ne-tranzactional) pe care reconcilierea il marca fals `sent`. Reparat: validare `cod_prestatie` fata de nomenclator la ingestie (cod necunoscut → tratat ca operatie de mapat, nu se mai trimite raw) + optiune boolean `on_unmapped_error` (`false` default → needs_mapping | `true` → respinge) per-cerere cu default per-cont `accounts.on_unmapped_error_default` (migrare aditiva). Confirmat live raspunsul RAR (500 pe cod intern vs 200 pe `OE-1`). Inclus si in `c842e33`: fix lease orfan worker (nepotrivire format data sending_since vs cutoff → orice rand `sending` parea expirat) + guard anti-dublu-POST + fix UI `hx-confirm` mostenit pe randuri (alerta de stergere la click pe rand). Teste: **748 passed** (cele 2 esecuri pre-existente fara legatura). Contract + CLAUDE.md actualizate. | 5.6 IMPLEMENTAT + VERIFY PASS (asteapta commit). Cele 14 stories din PRD 5.6 livrate TDD (RED->GREEN), `pytest -q` **741 passed, 0 failed**. Lifecycle trimiteri blocate (Val A primul, decizie #18): `app/submissions_admin.py` (sterge/repune scoped, 404-before-409); reactivare dedup peste `error` cu CAS + invalidare sesiune worker la creds noi (T1) + propagare `accounts.rar_creds_enc` (#17) + camp aditiv `reactivated:true` (#19); retentie randuri blocate 30z + `purge_after` curatat la reactivare/requeue (T2); API `DELETE`/`/repune` (200+JSON, #20); UI butoane + bulk + banner "Necesita atentia ta" actionabil cu deep-link. Observabilitate: `app/observ.py log_event` (dublu canal `app_events` DB + `RotatingFileHandler` per-proces, redactare creds/PII la scriere via `app/security.redact_pii`/`vin_partial`), `request_id` middleware + `X-Request-ID` pe toate raspunsurile (T8), handler global excepții -> 500 envelope 6-chei + request_id (T7), audit cerere API (`api_prezentari`/`api_auth_esuat`) + audit worker (`rar_login`/tranzitii), tab "Jurnal" filtrabil scoped (non-admin doar contul sau), retentie jurnal 90z. Live RAR `--send` NEPROBAT in sesiune (recomandat la deploy: confirma `rar_login` ok + `submission_sent` in jurnal). PRD actualizat cu raport VERIFY; contract actualizat cu endpointurile noi (T10). | ISTORIC: HOTFIX livrat + 5.6 APROBAT. Hotfix 500 pe `POST /v1/prezentari` (raportat din client Visual FoxPro): `AUTOPASS_CREDS_KEY` din `.env` nu respecta formatul Fernet (32 bytes url-safe base64) → `ValueError` la primul `encrypt_creds` → 500 brut. Reparat: cheie Fernet valida in `.env` + `crypto.validate_creds_key()` apelata in `main.lifespan` (fail-fast la startup, mesaj clar in loc de 500 la primul POST). Confirmat live: POST VFP → 200 `queued`; trimitere reala pe RAR test → `sent idPrezentare=68818` (verificat independent in finalizate). Corectat si mesajul fals din dashboard pentru starea `error` in `labels.py` ("se reincearca automat" → starea e terminala, NU se reincearca). Investigatia a expus 3 goluri structurale (500 brut fara traducere 3 niveluri; lipsa jurnal de aplicatie la nivel de eveniment; lacune de lifecycle — randuri blocate permanente, dedup blocat de un rand `error`, banner "Necesita atentia ta" neactionabil) → **PRD 5.6 APROBAT** (14 stories; decizii §5 rezolvate cu user). PRD: [prd-5.6](prd/prd-5.6-observabilitate-jurnal.md). | ISTORIC: 5.5 LIVRAT (uniformizare/standardizare UI/UX: tabele la grila Trimiteri, meniu hamburger + tab-bar redus Acasa/Mapari, sterge Ajutor de pe Acasa, panou admin cu selectie+bulk pe model nou `accounts.status`. 9 stories in 3 valuri, UI pur cu o singura exceptie backend = stare cont; stergere soft cu purjare PII imediata GDPR. VERIFY 671 teste + E2E browser (2 bug-uri prinse) + `/code-review high` (2 bug-uri reale reparate). Commit `1fbd894`, vezi randul 5.5). | ISTORIC: 5.4 LIVRAT (Erori pe 3 niveluri problema+cauza+fix pe API si UI: catalog central pur `app/errors.py` ca SINGURA sursa de adevar cod→{problema,fix}, consumat de API+UI+worker — face imposibila divergenta intre canale, acelasi invariant ca 5.2. 8 stories in 5 valuri. Tot ADITIV: `field`/`message`/`error` pastrate la octet, adaugam `cod/problema/cauza/fix`; `rar_error` stocat = SUPERSET (chei vechi intacte → `labels.py` nu se rupe intre valuri, zero migrare). Scope = fluxul de declarare; login/signup/CSRF neatinse. UI progresiv: lista compacta, 3 niveluri complete in detaliu/preview, AA light+dark. VERIFY context curat PASS 628 teste (byte-compat+superset verificate direct, E2E API+web; live RAR neprobat — lipsa creds key). `/code-review high`: 2 bug-uri reale reparate in `labels.py` (`motiv_uman` fara ramura 3-niveluri → 401 creds garbled in coloana Motiv; `parse_erori` element gol pe `{}`). 631 teste. Backend trimitere + schema NEATINSE. PRD: [prd-5.4](prd/prd-5.4-erori-3-niveluri.md)). | ISTORIC: 5.3 LIVRAT (Light/Dark mode: tema light ca bloc `[data-theme="light"]` peste variabilele `:root` — dark NESCHIMBAT la octet; comutator soare/luna in header pe toate paginile, default OS-aware cu fallback dark, persistenta `localStorage` doar la comutare explicita, script anti-FOUC in `` pre-paint; suprafetele de stare hardcodate convertite la `color-mix` in `base.html` + 7 fragmente. Zero backend — pur frontend. VERIFY 2 runde: r1 FAIL a prins literalii dark ramasi in 7 fragmente HTMX (text invizibil in light, test vacuu pe doar base.html) → fix US-003 + test care scaneaza fragmentele; r2 PASS E2E browser (banner light ~13:1 contrast, toggle instant+persista+anti-FOUC, dark identic). `/code-review` high: 1 finding reparat (light `--ok` green sub AA ca text → green-700, ~5.0:1). 584 teste. PRD: [prd-5.3](prd/prd-5.3-light-dark-mode.md)). | ISTORIC: 5.2 LIVRAT (Endpoint dry-run `POST /v1/prezentari/valideaza`: valideaza payload + mapare si intoarce verdictul real — `status_estimat` queued/needs_data/needs_mapping + erori `[{field,message}]` + coduri nemapate + prestatii rezolvate — FARA enqueue, FARA creds, zero scriere DB). 1 story TDD. Cheia de design: helper pur partajat `classify_prezentare` folosit de AMBELE rute, ca dry-run-ul sa nu poata diverge de trimiterea reala (invariant de corectitudine); `create_prezentari` refactorizat pe el cu comportament identic. Scope minim per decizie user: doar validare+mapare (fara idempotency/duplicat, `idempotency.py` neatins), hub `/integrare` amanat ca follow-up (descoperibilitate). VERIFY context curat PASS (577 teste; E2E API cu cele 3 verdicte + COUNT(*)=0 dupa dry-run + fara leak creds in raspuns; regresia de aur verde; live RAR `FINALIZATA` neprobat — lipsa creds key, endpoint read-only nu atinge worker/coada/schema). `/code-review` high: 0 findings (refactor faithful, mutable-default Pydantic-safe, import local necesar anti-circular). PRD: [prd-5.2](prd/prd-5.2-dryrun-valideaza.md). | ISTORIC: 5.1 LIVRAT (Hub de integrare `/integrare`: exemple cod multi-limbaj + retetar VFP cu 2 dialecte + `GET /v1/ping` readiness + export Postman/OpenAPI + "Testeaza conexiunea"). 4 stories in 2 valuri (Val 1 = US-001/US-002/US-004 paralel pe fisiere disjuncte via Agent team; Val 2 = US-003 UI). Atentie operationala: US-003 a rulat intr-un worktree branched din ultimul commit (FARA modificarile necomise ale US-004 din working-tree) si la "copiere manuala" a SUPRASCRIS `routes.py`, stergand ruta `POST /integrare/test-cheie` (8 teste 404) — reparat prin re-aplicarea rutei de catre autorul US-004 pe `routes.py` curent. Lectie: stories care ating acelasi fisier in valuri diferite + worktree = clobber daca worktree-ul nu vede working-tree-ul; foloseste fisiere disjuncte SAU merge atent de catre lead. VERIFY context curat PASS (568 teste) + E2E browser Playwright (deep-link server-side, IA pe 2 niveluri, VFP cu 3 niveluri de tab comuta corect, copy, htmx test-cheie → fragment eroare, 0 erori consola) + enqueue live (`POST /v1/prezentari` → queued); live RAR `FINALIZATA` NEPROBAT in sesiune (lipsa `AUTOPASS_CREDS_KEY`/creds RAR test) — risc minim, backend trimitere NEATINS. `/code-review` high a prins 4 bug-uri reale (toate in suprafata noua, reparate + lock-uite cu teste): snippet C# JSON multi-linie nevalid (CS1010), snippet VFP `json.dumps(indent=0)` inca cu newline-uri → string literal rupt in ambele dialecte, snippet Node `node:buffer` nu exporta FormData → TypeError, script `_integrare.html` ne-scoped acumuland event-listeneri pe tab-bar-ul principal la fiecare swap htmx (scoped pe `#integrare-section`). Notat ca cleanup viitor (nereparat): `_render_integrare` dubleaza SQL `are_creds`/`are_cheie`, `ping` cu 2 conexiuni DB + `account_for_key` de 2 ori, `_campuri_obligatorii` necache-uit, panouri limbaj copy-paste (candidat macro Jinja2). Backend trimitere (worker/masina stari/idempotenta/mapping) si schema NEATINSE. PRD: [prd-5.1](prd/prd-5.1-hub-integrare.md). | ISTORIC: 3.6 INCHIS (editare celule in preview + Acasa unificata). CLOSE: `/code-review` high a prins 1 bug real (decriptare `override_json` neprotejata de try/except in ambele cai de preview — 500 pe tot batch-ul la rotatie cheie Fernet vs. `raw_json` care degrada gratios), reparat in `import_router.preview_import` + `routes._web_compute_preview`; duplicarea `_override_of`/canonicalize notata ca cleanup viitor. 523 teste pass. 7 stories in 3 valuri, executate de 2 echipe in paralel (TeamCreate) pe fisiere disjuncte (core: routes/import/templates; mapari: `_mapari.html`) + US-007 secvential. Livrate: tab "Trimiteri" eliminat→sectiune "Trimiterile tale" sub upload pe Acasa (US-003); upload bara slim accentuata cu hero la first-run (US-004); editare de celule in preview prin `import_rows.override_json` (Approach B, Fernet, patch canonic aplicat ULTIMUL in `_resolve_row_for_preview`+`commit_import` — completeaza inclusiv coloane ABSENTE din fisier), mutatie pura cu status rederivat (US-001); buton Editeaza pe rand cu swap pe `
`+OOB contoare (nu pe sectiune), form propriu, mutual-exclusion, reuse grila `_trimitere_detaliu.html` (US-002); Mapari + formate de coloane ca tabele `.tablewrap`, H4 auto_send stocat (US-005/006); bifa "auto-send"→comutator etichetat pe COADA ("Pune automat in coada"/"Tine pentru verificare"), scoped pe operatie, `name=auto_send` pastrat (zero backend) (US-007). 523 teste pass. **VERIFY**: E2E browser pe `/` (Acasa unificata, upload slim, editare rand needs_data→ok cu swap pe rand + contoare OOB, Mapari tabelar + comutator) + LIVE pe RAR test — import fara coloana data → editarea completeaza data (override) → commit → worker login RAR test → `postPrezentare` → `sent` cu `idPrezentare=68696`, confirmat independent in lista finalizate RAR. 3 bug-uri JS (htmx 1.9.12) prinse DOAR la E2E in browser (invizibile la TestClient) si reparate: `useTemplateFragments=true` (raspunsul `
`+OOB era parsat in context de tabel → `swapError` + contoare pierdute), re-activare `confirm-btn` deferita pe tick (race `editing=true` tranzitoriu), `n-hint` ok-count actualizat de `updateN`. Backend trimitere (worker, masina stari, idempotenta-logica, mapping-rezolvare) NEATINS — singura atingere de schema: 1 coloana nullable `override_json` cu migrare defensiva. PRD: [prd-3.6](prd/prd-3.6-editare-preview-acasa-unificata.md). | ISTORIC: 3.5 LIVRAT (dashboard compact). 11 stories in 4 valuri, TDD. US-001 bara status compacta pe 2 randuri cu bife accesibile (glife ✓/✗ + text, nu doar culoare) + `format_data_rar` (dd.mm.yyyy hh24:mi:ss, helper pur). US-002 Acasa = ecranul de import (upload dominant inline, tab Import scos, `?tab=import`→Acasa fara 404). US-003 helper pur partajat `app/payload_view.py` (payload→campuri afisabile, defensiv, coercion Excel) refolosit si de `GET /v1/prezentari` (DRY). US-004 "Coada"→"Trimiteri": coloane RO + stare umana + detaliu complet la click in panou dedicat `#trimitere-detaliu` (nu inline — poll 10s), scoped 404 cross-account. US-005/006 CRUD mapari operatii + formate coloane salvate (scoped, re-rezolvare auto la edit cod). US-007 "Mapari" 3 sectiuni (de rezolvat / op salvate / formate coloane), "Cont" doar cheie+creds. US-008 motiv (mesaj validare) pe randuri needs_data in preview. US-009 filtre Trimiteri (stare SQL / vehicul+data Python) scoped + "sterge filtrele". US-010 corectie inline needs_data→queued cu payload+idempotency recalculate, sent read-only (403), coliziune idempotency prinsa pre-UPDATE. US-011 badge contoare pe tab-uri (Mapari/Trimiteri), scoped, aria-label. VERIFY context curat PASS (483 teste; E2E browser/RAR LIVE neprobat — recomandata probare manuala `--send`). `/code-review` high a prins 4 findings reale, toate reparate: corectie needs_mapping re-rezolva prestatiile (nu mai poate trimite cod nul la RAR), filtru fara LIMIT silentios, coliziune idempotency atomica (try/except IntegrityError), comparatie data doar ISO. Backend trimitere (worker, masina stari, idempotenta-logica, mapping-rezolvare, schema) NEATINS. PRD: [prd-3.5](prd/prd-3.5-dashboard-compact-trimiteri-mapari.md). Urmeaza Etapa 4 (4.1 mapare AI/MCP).
+**Ultima actualizare**: 2026-06-23 — 5.7 LIVRAT (raspuns API onest la blocaje + mapare inline din detaliu). Raportat din client VFP: `POST /v1/prezentari` intorcea `submission_id`+`status` FARA motiv pe randuri blocate (`erori` se popula doar pe ramura `on_unmapped_error=True`) → un `needs_data`/`needs_mapping` parea succes ("raspuns fara erori"). Reparat ADITIV: `SubmissionResult` += `nemapate` + `motiv`; `create_prezentari` populeaza `erori` (validare continut, 3 niveluri) / `nemapate` (coduri fara mapare, COD_NEMAPAT) / `motiv` (rezumat uman) pe TOATE caile non-`queued` — enqueue, respins (`on_unmapped_error=True`) si reactivare dedup peste `error`. UI: mapare inline in panoul de detaliu trimitere (`POST /trimitere/{id}/mapeaza`, reuse EXACT `save_mapping`+`reresolve_account`, scoped sesiune + CSRF, re-rezolva pe `batch_id`-ul randului → deblocheaza si randurile-frate; apare doar pe operatii nemapate reale, nu pe auto_send=0) — fara drum prin tab-ul Mapari. `/code-review high`: 2 buguri reale reparate (reactivarea omitea `erori`/`nemapate`/`motiv`; dublu `load_nomenclator` in `_detaliu_ctx`), restul candidatilor infirmati la verify (parse `auto_send` corect via `or ""`; lipsa `conn/account_id` pe ramuri de corectie nereachable cu needs_mapping+unmapped). `pytest -q` **765 passed, 0 failed**. PRD: [prd-5.7](prd/prd-5.7-raspuns-onest-mapare-inline.md). | ISTORIC: FIX out-of-process (raportat din client VFP): `cod_prestatie` necunoscut in nomenclator era trimis raw la RAR → **HTTP 500** (`ORA-12899`, coloana `COD_PRESTATIE` max 5 car.) + record PARTIAL `FINALIZATA` (RAR ne-tranzactional) pe care reconcilierea il marca fals `sent`. Reparat: validare `cod_prestatie` fata de nomenclator la ingestie (cod necunoscut → tratat ca operatie de mapat, nu se mai trimite raw) + optiune boolean `on_unmapped_error` (`false` default → needs_mapping | `true` → respinge) per-cerere cu default per-cont `accounts.on_unmapped_error_default` (migrare aditiva). Confirmat live raspunsul RAR (500 pe cod intern vs 200 pe `OE-1`). Inclus si in `c842e33`: fix lease orfan worker (nepotrivire format data sending_since vs cutoff → orice rand `sending` parea expirat) + guard anti-dublu-POST + fix UI `hx-confirm` mostenit pe randuri (alerta de stergere la click pe rand). Teste: **748 passed** (cele 2 esecuri pre-existente fara legatura). Contract + CLAUDE.md actualizate. | 5.6 IMPLEMENTAT + VERIFY PASS (asteapta commit). Cele 14 stories din PRD 5.6 livrate TDD (RED->GREEN), `pytest -q` **741 passed, 0 failed**. Lifecycle trimiteri blocate (Val A primul, decizie #18): `app/submissions_admin.py` (sterge/repune scoped, 404-before-409); reactivare dedup peste `error` cu CAS + invalidare sesiune worker la creds noi (T1) + propagare `accounts.rar_creds_enc` (#17) + camp aditiv `reactivated:true` (#19); retentie randuri blocate 30z + `purge_after` curatat la reactivare/requeue (T2); API `DELETE`/`/repune` (200+JSON, #20); UI butoane + bulk + banner "Necesita atentia ta" actionabil cu deep-link. Observabilitate: `app/observ.py log_event` (dublu canal `app_events` DB + `RotatingFileHandler` per-proces, redactare creds/PII la scriere via `app/security.redact_pii`/`vin_partial`), `request_id` middleware + `X-Request-ID` pe toate raspunsurile (T8), handler global excepții -> 500 envelope 6-chei + request_id (T7), audit cerere API (`api_prezentari`/`api_auth_esuat`) + audit worker (`rar_login`/tranzitii), tab "Jurnal" filtrabil scoped (non-admin doar contul sau), retentie jurnal 90z. Live RAR `--send` NEPROBAT in sesiune (recomandat la deploy: confirma `rar_login` ok + `submission_sent` in jurnal). PRD actualizat cu raport VERIFY; contract actualizat cu endpointurile noi (T10). | ISTORIC: HOTFIX livrat + 5.6 APROBAT. Hotfix 500 pe `POST /v1/prezentari` (raportat din client Visual FoxPro): `AUTOPASS_CREDS_KEY` din `.env` nu respecta formatul Fernet (32 bytes url-safe base64) → `ValueError` la primul `encrypt_creds` → 500 brut. Reparat: cheie Fernet valida in `.env` + `crypto.validate_creds_key()` apelata in `main.lifespan` (fail-fast la startup, mesaj clar in loc de 500 la primul POST). Confirmat live: POST VFP → 200 `queued`; trimitere reala pe RAR test → `sent idPrezentare=68818` (verificat independent in finalizate). Corectat si mesajul fals din dashboard pentru starea `error` in `labels.py` ("se reincearca automat" → starea e terminala, NU se reincearca). Investigatia a expus 3 goluri structurale (500 brut fara traducere 3 niveluri; lipsa jurnal de aplicatie la nivel de eveniment; lacune de lifecycle — randuri blocate permanente, dedup blocat de un rand `error`, banner "Necesita atentia ta" neactionabil) → **PRD 5.6 APROBAT** (14 stories; decizii §5 rezolvate cu user). PRD: [prd-5.6](prd/prd-5.6-observabilitate-jurnal.md). | ISTORIC: 5.5 LIVRAT (uniformizare/standardizare UI/UX: tabele la grila Trimiteri, meniu hamburger + tab-bar redus Acasa/Mapari, sterge Ajutor de pe Acasa, panou admin cu selectie+bulk pe model nou `accounts.status`. 9 stories in 3 valuri, UI pur cu o singura exceptie backend = stare cont; stergere soft cu purjare PII imediata GDPR. VERIFY 671 teste + E2E browser (2 bug-uri prinse) + `/code-review high` (2 bug-uri reale reparate). Commit `1fbd894`, vezi randul 5.5). | ISTORIC: 5.4 LIVRAT (Erori pe 3 niveluri problema+cauza+fix pe API si UI: catalog central pur `app/errors.py` ca SINGURA sursa de adevar cod→{problema,fix}, consumat de API+UI+worker — face imposibila divergenta intre canale, acelasi invariant ca 5.2. 8 stories in 5 valuri. Tot ADITIV: `field`/`message`/`error` pastrate la octet, adaugam `cod/problema/cauza/fix`; `rar_error` stocat = SUPERSET (chei vechi intacte → `labels.py` nu se rupe intre valuri, zero migrare). Scope = fluxul de declarare; login/signup/CSRF neatinse. UI progresiv: lista compacta, 3 niveluri complete in detaliu/preview, AA light+dark. VERIFY context curat PASS 628 teste (byte-compat+superset verificate direct, E2E API+web; live RAR neprobat — lipsa creds key). `/code-review high`: 2 bug-uri reale reparate in `labels.py` (`motiv_uman` fara ramura 3-niveluri → 401 creds garbled in coloana Motiv; `parse_erori` element gol pe `{}`). 631 teste. Backend trimitere + schema NEATINSE. PRD: [prd-5.4](prd/prd-5.4-erori-3-niveluri.md)). | ISTORIC: 5.3 LIVRAT (Light/Dark mode: tema light ca bloc `[data-theme="light"]` peste variabilele `:root` — dark NESCHIMBAT la octet; comutator soare/luna in header pe toate paginile, default OS-aware cu fallback dark, persistenta `localStorage` doar la comutare explicita, script anti-FOUC in `` pre-paint; suprafetele de stare hardcodate convertite la `color-mix` in `base.html` + 7 fragmente. Zero backend — pur frontend. VERIFY 2 runde: r1 FAIL a prins literalii dark ramasi in 7 fragmente HTMX (text invizibil in light, test vacuu pe doar base.html) → fix US-003 + test care scaneaza fragmentele; r2 PASS E2E browser (banner light ~13:1 contrast, toggle instant+persista+anti-FOUC, dark identic). `/code-review` high: 1 finding reparat (light `--ok` green sub AA ca text → green-700, ~5.0:1). 584 teste. PRD: [prd-5.3](prd/prd-5.3-light-dark-mode.md)). | ISTORIC: 5.2 LIVRAT (Endpoint dry-run `POST /v1/prezentari/valideaza`: valideaza payload + mapare si intoarce verdictul real — `status_estimat` queued/needs_data/needs_mapping + erori `[{field,message}]` + coduri nemapate + prestatii rezolvate — FARA enqueue, FARA creds, zero scriere DB). 1 story TDD. Cheia de design: helper pur partajat `classify_prezentare` folosit de AMBELE rute, ca dry-run-ul sa nu poata diverge de trimiterea reala (invariant de corectitudine); `create_prezentari` refactorizat pe el cu comportament identic. Scope minim per decizie user: doar validare+mapare (fara idempotency/duplicat, `idempotency.py` neatins), hub `/integrare` amanat ca follow-up (descoperibilitate). VERIFY context curat PASS (577 teste; E2E API cu cele 3 verdicte + COUNT(*)=0 dupa dry-run + fara leak creds in raspuns; regresia de aur verde; live RAR `FINALIZATA` neprobat — lipsa creds key, endpoint read-only nu atinge worker/coada/schema). `/code-review` high: 0 findings (refactor faithful, mutable-default Pydantic-safe, import local necesar anti-circular). PRD: [prd-5.2](prd/prd-5.2-dryrun-valideaza.md). | ISTORIC: 5.1 LIVRAT (Hub de integrare `/integrare`: exemple cod multi-limbaj + retetar VFP cu 2 dialecte + `GET /v1/ping` readiness + export Postman/OpenAPI + "Testeaza conexiunea"). 4 stories in 2 valuri (Val 1 = US-001/US-002/US-004 paralel pe fisiere disjuncte via Agent team; Val 2 = US-003 UI). Atentie operationala: US-003 a rulat intr-un worktree branched din ultimul commit (FARA modificarile necomise ale US-004 din working-tree) si la "copiere manuala" a SUPRASCRIS `routes.py`, stergand ruta `POST /integrare/test-cheie` (8 teste 404) — reparat prin re-aplicarea rutei de catre autorul US-004 pe `routes.py` curent. Lectie: stories care ating acelasi fisier in valuri diferite + worktree = clobber daca worktree-ul nu vede working-tree-ul; foloseste fisiere disjuncte SAU merge atent de catre lead. VERIFY context curat PASS (568 teste) + E2E browser Playwright (deep-link server-side, IA pe 2 niveluri, VFP cu 3 niveluri de tab comuta corect, copy, htmx test-cheie → fragment eroare, 0 erori consola) + enqueue live (`POST /v1/prezentari` → queued); live RAR `FINALIZATA` NEPROBAT in sesiune (lipsa `AUTOPASS_CREDS_KEY`/creds RAR test) — risc minim, backend trimitere NEATINS. `/code-review` high a prins 4 bug-uri reale (toate in suprafata noua, reparate + lock-uite cu teste): snippet C# JSON multi-linie nevalid (CS1010), snippet VFP `json.dumps(indent=0)` inca cu newline-uri → string literal rupt in ambele dialecte, snippet Node `node:buffer` nu exporta FormData → TypeError, script `_integrare.html` ne-scoped acumuland event-listeneri pe tab-bar-ul principal la fiecare swap htmx (scoped pe `#integrare-section`). Notat ca cleanup viitor (nereparat): `_render_integrare` dubleaza SQL `are_creds`/`are_cheie`, `ping` cu 2 conexiuni DB + `account_for_key` de 2 ori, `_campuri_obligatorii` necache-uit, panouri limbaj copy-paste (candidat macro Jinja2). Backend trimitere (worker/masina stari/idempotenta/mapping) si schema NEATINSE. PRD: [prd-5.1](prd/prd-5.1-hub-integrare.md). | ISTORIC: 3.6 INCHIS (editare celule in preview + Acasa unificata). CLOSE: `/code-review` high a prins 1 bug real (decriptare `override_json` neprotejata de try/except in ambele cai de preview — 500 pe tot batch-ul la rotatie cheie Fernet vs. `raw_json` care degrada gratios), reparat in `import_router.preview_import` + `routes._web_compute_preview`; duplicarea `_override_of`/canonicalize notata ca cleanup viitor. 523 teste pass. 7 stories in 3 valuri, executate de 2 echipe in paralel (TeamCreate) pe fisiere disjuncte (core: routes/import/templates; mapari: `_mapari.html`) + US-007 secvential. Livrate: tab "Trimiteri" eliminat→sectiune "Trimiterile tale" sub upload pe Acasa (US-003); upload bara slim accentuata cu hero la first-run (US-004); editare de celule in preview prin `import_rows.override_json` (Approach B, Fernet, patch canonic aplicat ULTIMUL in `_resolve_row_for_preview`+`commit_import` — completeaza inclusiv coloane ABSENTE din fisier), mutatie pura cu status rederivat (US-001); buton Editeaza pe rand cu swap pe `
`+OOB contoare (nu pe sectiune), form propriu, mutual-exclusion, reuse grila `_trimitere_detaliu.html` (US-002); Mapari + formate de coloane ca tabele `.tablewrap`, H4 auto_send stocat (US-005/006); bifa "auto-send"→comutator etichetat pe COADA ("Pune automat in coada"/"Tine pentru verificare"), scoped pe operatie, `name=auto_send` pastrat (zero backend) (US-007). 523 teste pass. **VERIFY**: E2E browser pe `/` (Acasa unificata, upload slim, editare rand needs_data→ok cu swap pe rand + contoare OOB, Mapari tabelar + comutator) + LIVE pe RAR test — import fara coloana data → editarea completeaza data (override) → commit → worker login RAR test → `postPrezentare` → `sent` cu `idPrezentare=68696`, confirmat independent in lista finalizate RAR. 3 bug-uri JS (htmx 1.9.12) prinse DOAR la E2E in browser (invizibile la TestClient) si reparate: `useTemplateFragments=true` (raspunsul `
`+OOB era parsat in context de tabel → `swapError` + contoare pierdute), re-activare `confirm-btn` deferita pe tick (race `editing=true` tranzitoriu), `n-hint` ok-count actualizat de `updateN`. Backend trimitere (worker, masina stari, idempotenta-logica, mapping-rezolvare) NEATINS — singura atingere de schema: 1 coloana nullable `override_json` cu migrare defensiva. PRD: [prd-3.6](prd/prd-3.6-editare-preview-acasa-unificata.md). | ISTORIC: 3.5 LIVRAT (dashboard compact). 11 stories in 4 valuri, TDD. US-001 bara status compacta pe 2 randuri cu bife accesibile (glife ✓/✗ + text, nu doar culoare) + `format_data_rar` (dd.mm.yyyy hh24:mi:ss, helper pur). US-002 Acasa = ecranul de import (upload dominant inline, tab Import scos, `?tab=import`→Acasa fara 404). US-003 helper pur partajat `app/payload_view.py` (payload→campuri afisabile, defensiv, coercion Excel) refolosit si de `GET /v1/prezentari` (DRY). US-004 "Coada"→"Trimiteri": coloane RO + stare umana + detaliu complet la click in panou dedicat `#trimitere-detaliu` (nu inline — poll 10s), scoped 404 cross-account. US-005/006 CRUD mapari operatii + formate coloane salvate (scoped, re-rezolvare auto la edit cod). US-007 "Mapari" 3 sectiuni (de rezolvat / op salvate / formate coloane), "Cont" doar cheie+creds. US-008 motiv (mesaj validare) pe randuri needs_data in preview. US-009 filtre Trimiteri (stare SQL / vehicul+data Python) scoped + "sterge filtrele". US-010 corectie inline needs_data→queued cu payload+idempotency recalculate, sent read-only (403), coliziune idempotency prinsa pre-UPDATE. US-011 badge contoare pe tab-uri (Mapari/Trimiteri), scoped, aria-label. VERIFY context curat PASS (483 teste; E2E browser/RAR LIVE neprobat — recomandata probare manuala `--send`). `/code-review` high a prins 4 findings reale, toate reparate: corectie needs_mapping re-rezolva prestatiile (nu mai poate trimite cod nul la RAR), filtru fara LIMIT silentios, coliziune idempotency atomica (try/except IntegrityError), comparatie data doar ISO. Backend trimitere (worker, masina stari, idempotenta-logica, mapping-rezolvare, schema) NEATINS. PRD: [prd-3.5](prd/prd-3.5-dashboard-compact-trimiteri-mapari.md). Urmeaza Etapa 4 (4.1 mapare AI/MCP).
> 2026-06-18 — 3.4 LIVRAT (interfata web ergonomica: tab-uri + wizard + microcopy). US-001 modul pur `app/web/labels.py` (stari tehnice→text uman + clasa CSS; test parametrizat din CHECK-ul `schema.sql` iese rosu la stare nemapata). US-002 bara status `/_fragments/status` + `_status.html` (etichete umane, defalcare blocate pe motiv, poll 15s, scoped pe cont). US-003 shell 6 tab-uri (Acasa·Import·Coada·Mapari·Cont·Nomenclator) cu deep-link `?tab=`, panou activ randat server-side, fragmente inactive lazy pe click, ARIA real (tablist/tab/tabpanel + aria-selected + navigare cu sageti). US-004 stepper import 4 pasi (PUR vizual, `hx-target="#import-section"` + csrf pastrate). US-005 Acasa onboarding checklist auto-bifat (are_creds/are_trimiteri) + colaps cand totul gata + empty states prietenoase Coada/Mapari. VERIFY lead-driven (TestClient ACs + 434 pytest pass; E2E browser/RAR LIVE neprobat in sesiune — recomandata probare manuala `--send`). Fix izolare teste (reset `ratelimit._hits` in fixturi, 429 la rulare subset). `/code-review` high: regasit avertisment "cont in asteptare de activare" (regresie din scoaterea `/_fragments/banner`) re-introdus in bara status + culori hardcodate→variabile paleta. 434 teste pass. Backend trimitere neatins. PRD: [prd-3.4](prd/prd-3.4-ux-dashboard-web.md). Urmeaza Etapa 4 (4.1 mapare AI/MCP).
@@ -102,6 +102,7 @@ Reguli de contract (detalii in `docs/api-rar-contract.md`): `FINALIZATA` e termi
| 5.5 | Uniformizare/standardizare UI/UX: tabele la grila Trimiteri (Mapari compact + toggle Auto/Manual + Ajutor; Nomenclator), meniu hamburger (Cont/Integrare/Nomenclator/Admin/logout) cu tab-bar redus la Acasa+Mapari, sterge Ajutor de pe Acasa, panou admin cu selectie+bulk (Activeaza/Blocheaza/Arhiveaza/Sterge) pe model nou de stare cont | DONE | 2026-06-23 | 9 stories, 3 valuri. UI pur (reskin+reasezare) cu O SINGURA exceptie backend: `accounts.status` (pending/active/blocked/archived/deleted, migrare defensiva, gate worker `claim_one` pastrand `active=1 ⇔ status='active'`). Macro autosend rescris compact pastrand semantica de prezenta `auto_send` (zero backend). Stergere = SOFT: tombstone, dar PII (creds RAR + chei API + CUI) purjate IMEDIAT la stergere (GDPR/L.142), nu prin retentie. VERIFY 671 teste pass (+40); E2E browser Playwright a prins 2 bug-uri invizibile la TestClient (bara bulk `display:flex` inline invingea `[hidden]`; arhivate cadeau sub "in asteptare" → grupare pe status). `/code-review high`: 2 bug-uri reale reparate (soft delete pastra creds+CUI fara purjare → purjare PII la stergere; apostrof in nume rupea `confirm()` inline kebab) + cleanup `_lifecycle_route`. Debt acceptat: `/admin/deactivate`+`set_active` pastrate legacy (test CLI). Backend trimitere neatins (exceptie: gate cont). Commit `1fbd894`. Design: [5.5-uniformizare-ui](design/5.5-uniformizare-ui.md). PRD: [prd-5.5](prd/prd-5.5-uniformizare-ui.md) |
| 5.4 | Erori pe 3 niveluri (problema + cauza + fix) pe API si UI | DONE | 2026-06-22 | Catalog central pur `app/errors.py` (24 coduri, `eroare()` → `{field,cod,problema,cauza,fix,message}`, ridica pe cod necunoscut) ca SINGURA sursa de adevar, consumat de API+UI+worker (acelasi invariant anti-divergenta ca 5.2). 8 stories, 5 valuri. Tot ADITIV (decizie user): `field`/`message`/`error` pastrate la octet, adaugam 3 niveluri + cod stabil; `rar_error` stocat = SUPERSET (chei vechi intacte → `labels.py` nu se rupe intre valuri, zero migrare). Scope = fluxul de declarare (validare continut, RAR 400/401, import, mapare op→cod); login/signup/CSRF NEATINSE. US-001 catalog; US-002 `validation.py` (cod+3n, byte-compat); US-003 `mapping.py`+`/valideaza` (needs_mapping/auto_send superset, `nemapate` 3n); US-004 worker RAR 400→`RAR_VALIDARE` (field_errors passthrough) + 401→`RAR_CREDS_INVALIDE` (clasificare transient neschimbata, fara echo creds); US-005 `import_router` detalii superset; US-006 `labels.parse_erori` (degradeaza gratios, lectia 3.6) + macro `_eroare.html` (progresiv: lista compacta, 3n complete in detaliu/preview; AA light+dark, accent≠rosu); US-007 upload/mapcoloane web prin macro; US-008 contract documentat. VERIFY context curat PASS (628 teste; byte-compat+superset verificate direct; E2E API `/valideaza` 3 niveluri + regresia de aur queued; E2E web fragmente upload+detaliu; live RAR `FINALIZATA` neprobat — lipsa creds key, endpoint-urile/UI noi nu ating trimiterea). `/code-review high`: 2 bug-uri reale reparate in `labels.py` — `motiv_uman` nu avea ramura pentru dict-ul 3-niveluri (401 creds → text garbled "field: None; cod:..." in coloana Motiv) + `parse_erori` intorcea element gol pe `{}`/`[{}]` (cutie de eroare goala); cleanup notat ca viitor (dublare `parse_erori`/`motiv_uman`, enrichment COD_NEMAPAT pe 2 straturi, upload handlers copy-paste cross-channel `routes.py`/`import_router`, `json.loads` mort in ramura COLOANE_FORMAT_JSON). 631 teste. Backend trimitere (worker masina stari/idempotenta/mapping-rezolvare) si schema NEATINSE. PRD: [prd-5.4](prd/prd-5.4-erori-3-niveluri.md) |
| 5.6 | Observabilitate & jurnal aplicatie + lifecycle trimiteri blocate: jurnal structurat de evenimente (tabela `app_events` + log text) cu vizualizator in dashboard, handler global 500→eroare 3 niveluri + `request_id`, audit cereri API + login RAR + ciclu trimiteri (worker), redactare PII/creds, retentie/purjare; plus stergere/re-pune in coada randuri blocate (UI+API), dedup care nu mai e blocat de un rand `error`, purjare randuri blocate, banner "Necesita atentia ta" actionabil (link + identificare rand) | APROBAT | 2026-06-23 | Nascut din incidentul 500 (client VFP). HOTFIX deja livrat in afara procesului (US-000): cheie Fernet valida in `.env` + `crypto.validate_creds_key()` fail-fast in `main.lifespan` + mesaj `error` corectat in `labels.py`; confirmat live POST→200, send RAR test→`idPrezentare=68818`. 14 stories ramase: observabilitate US-001..008, lifecycle US-009..013, banner US-014. Decizii §5 rezolvate cu user: retentie jurnal 90z (`AUTOPASS_LOG_RETENTION_DAYS`) + `RotatingFileHandler`; tipuri evenimente extensibile; jurnal non-admin scoped pe cont (admin vede tot); la resubmit doar `error` se re-activeaza (needs_* raman deduped); retentie randuri blocate 30z (`AUTOPASS_BLOCKED_RETENTION_DAYS`); stergere UI cu confirmare simpla + bulk pe lista. NEIMPLEMENTAT inca (doar hotfix-ul e in cod). PRD: [prd-5.6](prd/prd-5.6-observabilitate-jurnal.md) |
+| 5.7 | Raspuns API onest la blocaje (`erori`/`nemapate`/`motiv` pe orice status != `queued`) + mapare inline din panoul de detaliu trimitere | DONE | 2026-06-23 | Raportat din client VFP: `POST /v1/prezentari` raspundea `submission_id`+`status` fara motiv pe randuri blocate (`erori` doar pe `on_unmapped_error=True`) → `needs_data`/`needs_mapping` parea succes. 3 stories TDD. US-001 (backend API): `SubmissionResult` += `nemapate`+`motiv` (ADITIV), `create_prezentari` populeaza `erori`/`nemapate`/`motiv` pe enqueue + respins + reactivare via helperele `_rezultat_enqueue`/`_rezultat_respins`/`_motiv_clasificare`; `on_unmapped_error=True` pastreaza `erori`=COD_NEMAPAT (compat). US-002 (web): ruta `POST /trimitere/{id}/mapeaza` (reuse EXACT `save_mapping`+`reresolve_account`, scoped sesiune 404 + CSRF, re-rezolva pe `batch_id`-ul randului) + `_nemapate_pentru_submission` + context in `_detaliu_ctx`. US-003 (UI): sectiune "Mapeaza codul operatiei" in `_trimitere_detaliu.html` (selector cod RAR cu sugestie fuzzy preselectata >=60, `ui.autosend_toggle`), doar pe operatii nemapate reale. `/code-review high`: 2 buguri reale reparate (reactivarea omitea `erori`/`nemapate`/`motiv`; dublu `load_nomenclator`), restul infirmate. `pytest -q` **765 passed, 0 failed**. Live RAR `--send` NEPROBAT (recomandat la deploy: mapare inline → `queued` → worker trimite). Backend trimitere (worker/masina stari/idempotenta) si schema NEATINSE. PRD: [prd-5.7](prd/prd-5.7-raspuns-onest-mapare-inline.md) |
### Etapa 4 — Deprioritizat (post Etapa 5, daca apare nevoia din uz real)
diff --git a/docs/prd/prd-5.7-raspuns-onest-mapare-inline.md b/docs/prd/prd-5.7-raspuns-onest-mapare-inline.md
new file mode 100644
index 0000000..9a74aa6
--- /dev/null
+++ b/docs/prd/prd-5.7-raspuns-onest-mapare-inline.md
@@ -0,0 +1,160 @@
+# PRD 5.7 — Raspuns API onest la blocaje + mapare inline din detaliu
+
+**Stare**: inchis
+
+> Proces complet: `docs/ROADMAP.md` §5. Contract RAR (sursa de adevar): `docs/api-rar-contract.md`.
+> Starea trece: `draft → aprobat → in-executie → verify-pass → inchis` (actualizata de lead).
+
+## 1. Obiectiv
+
+Doua goluri raportate din clientul Visual FoxPro (2026-06-23):
+
+1. **Raspunsul `POST /v1/prezentari` minte prin omisiune.** Cand o prezentare e blocata
+ (`needs_data` — date respinse de RAR; `needs_mapping` — cod fara mapare), API-ul intoarce
+ `{submission_id, status}` **fara** detalii: campul `erori` ramane gol (se populeaza azi DOAR pe
+ ramura `on_unmapped_error=True`). Un integrator care verifica "am `submission_id` si `erori` e
+ gol → e ok" trateaza un rand blocat ca succes. Screenshot live: rand `needs_data` (data in viitor)
+ pe care clientul l-a primit "fara erori". Reparam: raspunsul devine **onest** — pentru orice status
+ != `queued` expune `erori` (validare continut), `nemapate` (coduri fara mapare) si un `motiv`
+ uman pe o linie. Strict **aditiv** (campuri noi + populare a unui camp existent) — clientii vechi
+ nu se rup.
+
+2. **Maparea unui cod nemapat cere drum prin tab-ul Mapari.** Din panoul de detaliu al unei
+ trimiteri `needs_mapping`, userul vede "Lipseste codul prestatiei" dar trebuie sa navigheze in
+ alt tab ca sa o mapeze. Adaugam **mapare inline**: chiar in panoul de detaliu, fiecare operatie
+ nemapata primeste un selector de cod RAR (cu sugestii fuzzy preselectate) si un buton care
+ salveaza maparea + re-rezolva submission-ul pe loc.
+
+Etapa 5 (ergonomie & integrare). Niciun apel live la RAR.
+
+## 2. Non-Goals (anti scope-creep)
+
+- **NU schimba masina de stari, worker-ul, idempotency-ul, schema.** Zero coloane noi. `classify_prezentare`
+ ramane sursa unica de clasificare (invariantul dry-run din 5.2 se pastreaza).
+- **NU schimba comportamentul `on_unmapped_error`** (ramura `error`/respins): `erori` cu `COD_NEMAPAT`
+ ramane exact ca azi (lock-uit de `test_on_unmapped_error_respinge_fara_enqueue`). Doar **adaugam**
+ `nemapate` + `motiv` si pe acea ramura.
+- **NU imbogateste raspunsul `deduped`** cu erori/motiv: un dedup intoarce starea curenta a randului
+ existent fara re-clasificare (clientul poate cere `GET /v1/prezentari/{id}` pentru detaliu). Pastram
+ semantica actuala a caii de dedup.
+- **NU atinge `/valideaza`** (dry-run-ul are deja `erori`+`nemapate`; ramane referinta de forma).
+- **NU schimba editorul din tab-ul Mapari** — maparea inline il completeaza, nu il inlocuieste;
+ reutilizeaza `save_mapping` + `reresolve_account` (aceeasi logica, fara ramura noua).
+- **NU mapare inline pe `needs_data`** — acolo exista deja corectia de campuri (US-010, 3.5).
+ Inline-ul de mapare apare DOAR pe `needs_mapping` cu operatii nemapate reale.
+
+## 3. Stories atomice
+
+### US-001: Raspuns `POST /v1/prezentari` onest pentru randuri blocate
+**Ca** integrator (ROAAUTO / soft propriu / punte VFP) **vreau** ca raspunsul la enqueue sa-mi spuna
+**de ce** un rand e blocat (nu doar `status`), **pentru ca** sa nu tratez un `needs_data`/`needs_mapping`
+drept succes si sa pot reactiona programatic.
+
+- **Depinde de**: —
+- **Fisiere**: `app/models.py` (`SubmissionResult` += `nemapate`, `motiv`), `app/api/v1/router.py`
+ (`create_prezentari` populeaza din `cl`), `tests/test_api.py` + `tests/test_mapping.py` (~4 fisiere)
+- **Test intai (RED)**: `tests/test_api.py` —
+ - `test_needs_data_intoarce_erori` (data in viitor → `status="needs_data"`, `erori` negol cu
+ `cod="DATA_VIITOR"` pe `field="data_prestatie"`, `motiv` negol)
+ - `test_vin_invalid_intoarce_erori_pe_camp` (VIN cu O/I/Q → `erori` cu `field="vin"`)
+ - `test_needs_mapping_intoarce_nemapate` (cod necunoscut → `status="needs_mapping"`, `nemapate`
+ negol cu `cod_op_service`, `erori=[]`, `motiv` negol)
+ - `test_queued_fara_erori_nemapate` (valid → `erori=[]`, `nemapate=[]`, `motiv` null)
+ - `tests/test_mapping.py::test_on_unmapped_error_pastreaza_erori` (regresie: `on_unmapped_error=True`
+ inca da `erori[0].cod=="COD_NEMAPAT"`; + acum `nemapate` negol)
+- **Acceptance criteria**:
+ - [ ] `SubmissionResult` are campurile noi `nemapate: list[dict] = []` si `motiv: str | None = None`
+ (aditiv; `erori` ramane).
+ - [ ] Pe enqueue cu `status="needs_data"`: `erori` = exact `validate_prezentare` (3 niveluri),
+ `nemapate=[]`, `motiv` = rezumat uman pe o linie.
+ - [ ] Pe enqueue cu `status="needs_mapping"` din coduri nemapate: `nemapate` =
+ `[{cod_op_service, denumire, cod, problema, cauza, fix, message}]`, `erori=[]`, `motiv` negol.
+ - [ ] Pe enqueue cu `status="needs_mapping"` din `auto_send=0`: `motiv` explica "confirmare manuala"
+ (chiar daca `erori`/`nemapate` sunt goale).
+ - [ ] Pe `status="queued"`: `erori=[]`, `nemapate=[]`, `motiv=None`.
+ - [ ] Ramura `on_unmapped_error=True` (`status="error"`, `submission_id=None`): `erori` neschimbat
+ (`COD_NEMAPAT`), `nemapate` negol, `motiv` negol.
+ - [ ] Raspunsul nu contine niciodata echo de creds (`password`) — validat pe corpul serializat.
+- **Verificare E2E**: `POST /v1/prezentari` din client (sau curl) cu data in viitor → JSON cu `erori`
+ vizibile; cu cod intern → `nemapate`.
+
+### US-002: Backend mapare inline din panoul de detaliu (ruta + context)
+**Ca** operator in dashboard **vreau** sa mapez codul unei operatii direct din detaliul trimiterii
+**pentru ca** sa deblochez randul fara sa schimb tab-ul.
+
+- **Depinde de**: —
+- **Fisiere**: `app/web/routes.py` (helper `_nemapate_pentru_submission`, context in `_detaliu_ctx`,
+ ruta `POST /trimitere/{id}/mapeaza`), `tests/test_web_mapare_inline.py` (~2 fisiere)
+- **Test intai (RED)**: `tests/test_web_mapare_inline.py` —
+ - `test_detaliu_needs_mapping_arata_operatii_nemapate` (GET detaliu pe `needs_mapping` → HTML
+ contine selectorul de cod RAR + denumirea operatiei + cel putin o sugestie)
+ - `test_mapeaza_inline_deblocheaza_randul` (POST `/trimitere/{id}/mapeaza` cu cod valid →
+ submission trece `queued`, maparea apare in `operations_mapping`)
+ - `test_mapeaza_inline_cod_necunoscut_respins` (cod inexistent in nomenclator → mesaj eroare,
+ submission ramane `needs_mapping`, nicio mapare salvata)
+ - `test_mapeaza_inline_scoped_pe_sesiune` (POST pe submission al altui cont → 404, fara mapare)
+ - `test_mapeaza_inline_csrf_obligatoriu` (fara token → 403)
+ - `test_mapeaza_inline_respecta_batch` (submission din import cu `batch_id` → re-rezolvarea il atinge)
+- **Acceptance criteria**:
+ - [ ] `_detaliu_ctx` pe `status="needs_mapping"` expune `nemapate_inline` =
+ `[{cod_op_service, denumire, suggestions:[{cod_prestatie, nume_prestatie, score}]}]` + `nomenclator`.
+ - [ ] `POST /trimitere/{id}/mapeaza` (Form: `cod_op_service`, `cod_prestatie`, `auto_send`, `csrf_token`):
+ verifica CSRF, scope pe sesiune (404 cross-account/inexistent ca restul rutelor), respinge cod
+ absent din nomenclator (re-randeaza detaliul cu mesaj), altfel `save_mapping` + `reresolve_account`
+ cu `batch_id` al randului, re-randeaza detaliul cu starea noua + mesaj.
+ - [ ] Re-rezolvarea foloseste EXACT `save_mapping`/`reresolve_account` existente (fara logica noua de
+ clasificare); randurile-frate cu aceeasi operatie din acelasi batch se deblocheaza si ele.
+ - [ ] Raspunsul trimite `HX-Trigger: trimiteriChanged` (lista Trimiteri se reimprospateaza).
+- **Verificare E2E**: dashboard pe `needs_mapping` → alegi cod RAR inline → randul devine `queued`.
+
+### US-003: UI mapare inline in `_trimitere_detaliu.html`
+**Ca** operator **vreau** un formular clar in detaliu (operatie + sugestii + selector cod + salveaza)
+**pentru ca** maparea sa fie evidenta si rapida.
+
+- **Depinde de**: US-002
+- **Fisiere**: `app/web/templates/_trimitere_detaliu.html`, (eventual reuse din `_macros.html`),
+ `tests/test_web_mapare_inline.py` (asserts pe HTML)
+- **Test intai (RED)**: acoperit de US-002 (`test_detaliu_needs_mapping_arata_operatii_nemapate`)
+- **Acceptance criteria**:
+ - [ ] Sectiune "Mapeaza codul operatiei" vizibila DOAR cand `status="needs_mapping"` si exista
+ `nemapate_inline`; un `