# PRD 3.5 — Dashboard compact: import pe prima pagina, status cu bife, Trimiteri lizibile, Mapari complete **Stare**: inchis > Proces complet: `docs/ROADMAP.md` §5. Contractul RAR (sursa de adevar): `docs/api-rar-contract.md`. > Starea trece: `draft → aprobat → in-executie → verify-pass → inchis` (actualizata de lead). > Aceasta e o livrabila de **UI/UX** — continua 3.4 (`prd-3.4-ux-dashboard-web.md`). Atinge stratul web > (Jinja2 + HTMX, zero build) si rute de prezentare/listare. **Nu** modifica `worker/`, `mapping.py` > (logica de rezolvare), `idempotency.py`, masina de stari submissions sau contractul RAR. ## 1. Obiectiv Feedback de utilizare pe interfata 3.4: e mai buna, dar (a) importul — operatia principala — e ascuns intr-un tab, (b) bara de status are fonturi mici si etichete fara bife, (c) pagina "Coada" e neintuitiva (coloane tehnice "HTTP RAR", stare in engleza "sent", "Motiv" gol, si **nu se vede la ce comanda se refera** — doar `idPrezentare`), (d) pagina "Mapari" arata doar operatiile nerezolvate — maparile deja salvate par pierdute, iar maparile de coloane nu se vad nicaieri. Livrabila rezolva aceste patru zone, fara a schimba comportamentul backend de trimitere: 1. **Acasa = ecranul de import.** Prima pagina arata direct caseta de upload (importul e operatia principala), sub o bara de status compacta; ghidul "primii pasi" si link-urile de ajutor coboara pe un singur rand discret. Tab-ul "Import" separat dispare (era acelasi flux). 2. **Bara de status compacta, font normal, cu bife.** Doua randuri: sus doua bife (verde/rosu) pentru "Trimitere automata" si "Legatura RAR" + "Ultima autentificare RAR" in format `dd.mm.yyyy hh24:mi:ss`; jos contoarele (in asteptare / declarate / blocate). 3. **"Coada" → "Trimiteri", lizibila.** Coloane umane (Stare in romana via `labels.py`, Vehicul, Operatie, Data prestatie, Nr. prezentare RAR, Actualizat, Motiv uman). Click pe rand → detaliu complet (toate campurile, inclusiv codul HTTP tehnic si motivul integral). Detaliile comenzii se citesc din `payload_json` (text JSON simplu, nu criptat). 4. **"Mapari" complet — trei sectiuni.** (1) De rezolvat (`needs_mapping`, ca acum), (2) Mapari operatii salvate — `operations_mapping` editabil (schimba cod RAR / auto-send / sterge), (3) Formate de coloane salvate — `column_mappings` per semnatura (vezi coloanele, format data, editeaza/sterge). "Cont" ramane doar cheie API + creds RAR. Decizii de layout confirmate cu utilizatorul (AskUserQuestion, cu preview): Acasa=Import direct; status pe doua randuri cu bife; Trimiteri cu detalii in tabel + expand; toate maparile intr-un singur loc ("Mapari"). ## 2. Non-Goals (anti scope-creep) - **Fara schimbari de backend de trimitere**: worker, mapare op→cod (rezolvarea), idempotenta, reconciliere, masina de stari submissions raman neatinse. Doar prezentare + listare/editare web. - **Fara endpoint-uri API noi `/v1/*`** si fara schimbari de schema SQL **de structura**. Tabelele `operations_mapping` / `column_mappings` exista deja; doar le expunem/edita prin rute web. Exceptie controlata (acceptata la CEO review): US-009 poate adauga **un index** pe `submissions` pentru filtrare (nu coloane noi, nu tabele noi). - **Fara framework JS / build step**: ramane Jinja2 + HTMX + CSS in `base.html`. Eventualul JS e vanilla inline, minim. Fara React/Vue/Tailwind/bundler. - **Fara rescriere a fluxului de import** (parsare, mapare coloane, preview, commit raman ca logica) — doar muta upload-ul pe Acasa si imbraca rezultatul. - **Fara redesign login/signup/admin** dincolo de aplicarea acelorasi clase/etichete daca e trivial. - **Fara i18n / tema light**: texte in romana hardcodate, paleta dark din `base.html`. - **Fara modificari de worker / masina de stari / reconciliere**: US-010 (corectie inline) doar re-valideaza (`validation.py`) si re-pune randul in `queued` (re-enqueue), fara sa atinga worker-ul. - **Editarea de continut e permisa DOAR pentru randuri ne-trimise blocate** (`needs_data`/`needs_mapping`), prin US-010. Randurile `sent`/`FINALIZATA` raman **read-only** (terminal la RAR, fara anulare/corectie). ## 3. Stories atomice > Fiecare story: cea mai mica unitate care lasa sistemul functional. Backend + UI pentru acelasi > comportament = 2 stories. Toate atingerile sunt in `app/web/` (templates + routes + `labels.py`). > Verificare E2E = browser HTMX pe `http://localhost:8000/` (Playwright MCP sau `/browse`). > **Regula de aur**: fluxul import → commit → worker → FINALIZATA la RAR test NU are voie sa se strice, > iar deep-link-urile `?tab=` raman valide. ### US-001: Bara de status compacta cu bife + data formatata (backend format + UI) **Ca** operator de service **vreau** o bara de status compacta, cu font normal, cu bife clare si data ultimei autentificari completa **pentru ca** sa vad starea sistemului dintr-o privire, fara sa ghicesc. - **Depinde de**: — - **Fisiere**: `app/web/labels.py` (helper format data), `app/web/routes.py` (`fragment_status`), `app/web/templates/_status.html`, `tests/test_web_labels.py`, `tests/test_web_status.py` (~5 fisiere) - **Test intai (RED)**: `test_format_data_rar` — `2026-06-18T14:30:22` (sau forma stocata in `worker_heartbeat.last_rar_login_ok`) → `"18.06.2026 14:30:22"`; valoare lipsa → `"—"`; format invalid → fallback grijuliu (nu arunca). `tests/test_web_status.py::test_status_are_bife` — fragmentul randat contine bifa verde cand worker viu + RAR ok, rosie cand oprit/indisponibil. - **Continut**: - Helper pur in `labels.py` (ex. `format_data_rar(raw) -> str`) care produce `dd.mm.yyyy hh24:mi:ss`. - `_status.html` rescris pe **doua randuri**: rand 1 = `[bifa] Trimitere automata activa` + `[bifa] Legatura RAR OK` + `Ultima autentificare RAR: `; rand 2 = `In asteptare: N | Declarate la RAR: N | Blocate: N`. Font normal (13-14px), fara `font-size:11px/12px`. - **Accesibilitate (design review)**: starea NU se distinge doar prin culoare. Glifa difera — `✓` (✓) pentru activ/OK, `✗` (✗) pentru oprit/indisponibil — plus textul difera (activa/oprita, functionala/indisponibila). Culoarea (verde/rosu) e redundanta, nu singurul semnal. - Pastreaza avertismentul "cont in asteptare de activare" (regresia reparata in 3.4) si poll-ul 15s. - **Acceptance criteria**: - [x] Data ultimei autentificari apare ca `dd.mm.yyyy hh24:mi:ss` (test pe helper, pur). - [x] Doua stari binare au bifa verde/rosie dupa starea reala (worker viu/mort, RAR ok/indisponibil). - [x] Niciun text din bara nu mai foloseste `font-size` sub 13px. - [x] `python3 -m pytest -q` trece. - **Verificare E2E**: browser pe `/` — bara compacta, bife corecte, data formatata romaneste. ### US-002: Acasa devine ecranul de import (upload inline + help compact, scoatere tab Import) **Ca** operator **vreau** sa import direct de pe prima pagina **pentru ca** importul e ce fac cel mai des. - **Depinde de**: US-001 (bara status finala deasupra) - **Fisiere**: `app/web/templates/dashboard.html` (lista tab-uri), `app/web/routes.py` (`_TABS_VALIDE`, `_render_panel`, `fragment_import`/`fragment_acasa`), `app/web/templates/_acasa.html`, `app/web/templates/_upload.html` (reutilizat), `tests/test_web_dashboard.py` (~5 fisiere) - **Test intai (RED)**: `test_acasa_contine_upload` — fragmentul `/_fragments/acasa` contine formularul de upload (`hx-post` catre ruta de import existenta); `test_tab_import_redirect` — `?tab=import` nu mai e tab separat (redirect la `acasa` sau eticheta absenta din tab-bar), iar deep-link-ul nu da 404. - **Continut**: - `_acasa.html`: caseta de upload (din `_upload.html`) ca prim element; sub ea, "primii pasi" pe un **singur rand** compact (`o Cont RAR o Cheie API * Import`) + un rand de ajutor cu link-uri mici (Ghid / Coada / Mapari). Upload-ul porneste stepper-ul existent (target `#import-section`). - **Ierarhie (design review)**: upload-ul e vizual DOMINANT (titlu clar + caseta mare); checklist-ul "primii pasi" si ajutorul sunt subordonate (font mai mic, sub upload), nu concureaza cu el. Prima pagina are un singur centru de greutate: importa un fisier. - Scoate `("import", ...)` din lista de tab-uri din `dashboard.html`; pastreaza `?tab=import` valid (redirect la `acasa`) pentru orice URL salvat. Stepper-ul, mapare-coloane, preview, commit raman. - "Incarca alt fisier" din stepper trimite inapoi la Acasa (nu la un tab inexistent). - **Acceptance criteria**: - [x] Pe `/` (tab implicit Acasa) caseta de upload e vizibila fara click suplimentar. - [x] Tab-bar-ul nu mai are "Import"; `?tab=import` nu da 404 (redirect/echivalent la Acasa). - [x] Fluxul upload → mapare coloane → preview → commit functioneaza neschimbat (target/csrf intacte). - [x] Link-urile de ajutor + checklist incap pe randuri compacte, font normal. - [x] `python3 -m pytest -q` trece. - **Verificare E2E**: browser — incarc un fisier de pe Acasa, parcurg stepper-ul pana la commit. ### US-003: Prezentare detalii comanda din payload (backend pur, testat) **Ca** operator **vreau** ca fiecare trimitere sa-mi spuna despre ce vehicul/operatie e vorba **pentru ca** acum vad doar `idPrezentare` si nu stiu ce comanda am trimis si ce nu. - **Depinde de**: — - **Fisiere**: `app/payload_view.py` (nou, helper PARTAJAT web+API), `app/api/v1/router.py` (refactor mic: `GET /v1/prezentari` foloseste acelasi helper, nu o copie), `tests/test_payload_view.py` (~3 fisiere). Decizie eng review (DRY): un singur modul de extragere payload→campuri afisabile, ca sa nu diverge intre canalul web si cel API (router.py:247-264 face azi extragerea sa proprie). - **Test intai (RED)**: `test_detalii_din_payload` — dat un `payload_json` (text JSON: `vin`, `numar`/ `numarInmatriculare`, `odometru_final`, `data_prestatie`, `cod_prestatie`/`cod_op_service`/`denumire`), helperul intoarce un dict de prezentare `{vehicul_nr, vin_scurt, operatie, data_prestatie, odometru, cod}`; `test_payload_partial` — campuri lipsa → `"—"`/gol fara exceptie; `test_payload_invalid` → fallback grijuliu (nu arunca); `test_payload_coercion_excel` — odometru `"123.0"`/numeric si VIN non-string (coercion Excel) afisate curat (`str()` defensiv), chei API vs import (`numar` vs `numarInmatriculare`) ambele rezolvate. - **Continut**: functie pura care primeste randul submission (sau `payload_json`) si produce campurile de afisat. `vin_scurt` = forma trunchiata pentru tabel (VIN integral ramane in detaliu). Citeste cheile defensiv (canalele API si import pot diferi usor; `payload_json` e plaintext — vezi `router.py`). - **Acceptance criteria**: - [x] Helper pur (fara DB, fara request), 100% acoperit de teste pe cazurile plin/partial/invalid. - [x] Nu arunca niciodata pe payload malformat (degradeaza la `—`). - [x] `python3 -m pytest -q` trece. - **Verificare E2E**: — (acoperit de US-004). ### US-004: "Coada" → "Trimiteri" — tabel lizibil + detaliu la click **Ca** operator **vreau** un tabel de trimiteri pe care il inteleg, cu detaliile comenzii **pentru ca** "HTTP RAR"/"sent"/"Motiv gol" nu-mi spun nimic si nu stiu la ce comanda se refera randul. - **Depinde de**: US-003 (prezentare payload), US-001 (`labels.py` pt stare umana) - **Fisiere**: `app/web/templates/_coada.html` (titlu + tab label), `app/web/templates/_submissions.html`, `app/web/templates/dashboard.html` (eticheta tab "Coada"→"Trimiteri", `tab_id` ramane `coada`), `app/web/routes.py` (`fragment_submissions` imbogatit + ruta detaliu `/_fragments/trimitere/{id}`), `app/web/templates/_trimitere_detaliu.html` (nou), `tests/test_web_submissions.py` (~6 fisiere) - **Test intai (RED)**: `test_submissions_coloane_umane` — tabelul contine antete RO (Stare, Vehicul, Operatie, Data prestatie, Nr. prezentare RAR, Motiv) si **nu** mai contine "HTTP RAR" ca antet principal, nici status brut englezesc afisat ca atare (folose `labels.eticheta_stare`); `test_detaliu_trimitere` — `/_fragments/trimitere/{id}` intoarce detaliul complet scoped pe cont (404 cross-account). - **Continut**: - Eticheta tab "Coada" → **"Trimiteri"** (pastreaza `tab_id="coada"` ca deep-link `?tab=coada` sa ramana valid). Titlu sectiune "Trimiteri catre RAR". - `_submissions.html`: coloane = `#`, **Stare** (`eticheta_stare` text RO + pill culoare), **Vehicul** (nr + VIN scurt, din US-003), **Operatie**, **Data prestatie**, **Nr. prezentare RAR** (`id_prezentare` sau `—`), **Actualizat**, **Motiv** (text uman; pt `needs_data` → ex. "lipsa odometru"). Codul HTTP tehnic NU mai e coloana principala — coboara in detaliu. `query`-ul include `payload_json` (pt US-003) pe langa campurile actuale. - Click pe rand → `hx-get="/_fragments/trimitere/{id}"` → `_trimitere_detaliu.html` cu toate campurile (vehicul integral, operatie+cod, odometru, data, stare, `rar_status_code`, `rar_error` integral, retry, timestamps). Scoped pe contul sesiunii. - **Detaliul se randeaza intr-un PANOU DEDICAT** (ex. `#trimitere-detaliu` sub/langa tabel), NU inline in randul din tabel. Motiv (CEO review, Finding #1): `_submissions.html` are `hx-trigger="every 10s"`; un expand inline ar fi sters de poll-ul de refresh. Panoul dedicat nu e prins de poll. - **Vizibilitate (design review)**: la deschidere, panoul trebuie sa fie evident — scroll-to panou si/sau evidentiere a randului selectat in tabel. Altfel pare ca "nu s-a intamplat nimic" (panoul apare sub fold). - **Acceptance criteria**: - [x] Antetele coloanelor sunt in romana; starea afisata e text uman (nu "sent"). - [x] Fiecare rand arata vehicul + operatie + data (din payload), nu doar `idPrezentare`. - [x] Click pe rand deschide detaliul complet, scoped pe cont (404 la id-ul altui cont). - [x] Motivul pentru `needs_data`/`error` apare in coloana Motiv (nu gol cand exista `rar_error`). - [x] `python3 -m pytest -q` trece. - **Verificare E2E**: browser — tab Trimiteri, identific un rand dupa vehicul, deschid detaliul. ### US-005: Listare + editare mapari operatii salvate (backend) **Ca** operator **vreau** sa vad si sa pot schimba maparile operatie→cod pe care le-am salvat **pentru ca** acum, dupa ce mapez si trimit, ele dispar din ecran si par pierdute. - **Depinde de**: — - **Fisiere**: `app/web/routes.py` (query `operations_mapping` + rute edit/delete), `tests/test_web_mapari_salvate.py` (~2 fisiere) - **Test intai (RED)**: `test_lista_mapari_salvate` — intoarce randurile `operations_mapping` ale contului cu `nume_prestatie` jonctionat din nomenclator; `test_editeaza_mapare_salvata` — POST schimba `cod_prestatie`/`auto_send` doar pe contul propriu (cross-account interzis), verifica `cod_prestatie` exista in nomenclator (ca la `/mapari` actual); `test_sterge_mapare_salvata` — DELETE scoped pe cont. - **Continut**: functie de listare scoped pe cont + rute web `POST /mapari/salvate` (update) si `POST /mapari/salvate/sterge` (delete) cu CSRF + PRG/HTMX swap. **Nu** schimba logica de rezolvare din `mapping.py`; doar CRUD pe tabela existenta. **Re-rezolvare obligatorie** (promovata din optional la CEO review): la schimbarea unui cod, submission-urile blocate (`needs_mapping`) pe acel `cod_op_service` se re-rezolva automat, reutilizand helperul de re-rezolvare existent (`reresolve_account`/echivalent) — fara cod nou de rezolvare. Inchide pain-ul "am mapat dar nu vad efectul". - **Acceptance criteria**: - [x] Listarea e scoped pe contul sesiunii (fara leak cross-account — vezi C6 din 3.3a). - [x] Editarea respinge cod inexistent in nomenclator si cont strain. - [x] Stergerea afecteaza doar contul propriu. - [x] La editarea unui cod, submission-urile `needs_mapping` pe acel `cod_op_service` se deblocheaza automat (test: rand blocat → editez maparea → randul trece din `needs_mapping`). - [x] `python3 -m pytest -q` trece. - **Verificare E2E**: — (acoperit de US-006). ### US-006: Listare + editare/stergere formate de coloane salvate (backend) **Ca** operator **vreau** sa vad formatele de import memorate si sa le pot edita/sterge **pentru ca** nu stiu ce coloane sunt retinute si ce se intampla cand vin cu un fisier cu alte coloane. - **Depinde de**: — - **Fisiere**: `app/web/routes.py` (query `column_mappings` + rute edit/delete), `tests/test_web_formate_coloane.py` (~2 fisiere) - **Test intai (RED)**: `test_lista_formate_coloane` — intoarce randurile `column_mappings` ale contului cu coloanele (din `signature_coloane`/`json_mapare`), `format_data` si un contor de utilizare (cate `import_batches`/submissions folosesc acea semnatura — best-effort, sau omis daca nu e ieftin); `test_sterge_format_coloane` — DELETE scoped pe cont; `test_editeaza_format_coloane` — POST schimba `json_mapare`/`format_data` pentru o semnatura, scoped pe cont. - **Continut**: listare scoped pe cont + rute `POST /formate-coloane/...` (edit/delete) cu CSRF. Comportament documentat (nu cod nou de import): un fisier cu **alte coloane** = semnatura noua = format nou separat (`UNIQUE (account_id, signature_coloane)`), nu suprascrie; **acelasi antet** = maparea retinuta se reaplica automat la urmatorul import (comportament existent din 2.4). - **Acceptance criteria**: - [x] Listarea arata coloanele fiecarui format + format data, scoped pe cont. - [x] Stergerea/editarea afecteaza doar contul propriu (fara leak cross-account). - [x] `python3 -m pytest -q` trece. - **Verificare E2E**: — (acoperit de US-006 UI in US-007). ### US-007: Pagina "Mapari" cu trei sectiuni (UI) **Ca** operator **vreau** o singura pagina "Mapari" cu tot ce tine de mapari **pentru ca** sa rezolv ce e blocat si sa-mi gestionez maparile salvate (operatii + coloane) intr-un loc. - **Depinde de**: US-005, US-006 - **Fisiere**: `app/web/templates/_mapari.html`, `app/web/routes.py` (`_render_mapari`/`fragment_mapari` imbogatit sa treaca cele 3 seturi de date), `app/web/templates/_cont.html` (ramane doar cheie API + creds RAR — fara mapari), `tests/test_web_mapari_ui.py` (~4 fisiere) - **Test intai (RED)**: `test_mapari_trei_sectiuni` — fragmentul `/_fragments/mapari` contine cele trei sectiuni (De rezolvat / Mapari operatii salvate / Formate de coloane salvate); `test_cont_fara_mapari` — `/_fragments/cont` nu mai contine sectiuni de mapari. - **Continut**: - `_mapari.html` reorganizat pe 3 sectiuni: (1) **De rezolvat** = `pending_unmapped` (ca acum), (2) **Mapari operatii salvate** = lista din US-005, fiecare cu select cod RAR + checkbox auto-send + buton sterge (HTMX swap pe `#mapari-section`), (3) **Formate de coloane salvate** = lista din US-006, fiecare cu coloanele afisate + format data + actiuni edit/sterge. Empty states prietenoase per sectiune. - `_cont.html`: confirma ca nu contine mapari (cheie API + creds RAR doar). - **Acceptance criteria**: - [x] "Mapari" arata cele 3 sectiuni; sectiunile goale au mesaj prietenos, nu lipsesc tacit. - [x] Salvarea/stergerea reincarca sectiunea via HTMX fara reload de pagina. - [x] "Cont" nu mai contine mapari. - [x] `python3 -m pytest -q` trece. - **Verificare E2E**: browser — vad maparile salvate dupa ce am mapat+trimis; editez un cod; vad formatul de coloane al fisierului importat. ### US-008: Feedback clar pentru randuri respinse la import (lipsa odometru / needs_data) **Ca** operator **vreau** sa inteleg de ce un rand nu s-a importat **pentru ca** am incarcat un fisier fara odometru si pur si simplu "nu s-a importat", fara explicatie. - **Depinde de**: US-002 (importul pe Acasa) - **Fisiere**: `app/web/templates/_preview_import.html`, `app/web/routes.py` (mesaj preview, daca e cazul), `tests/test_web_preview_motive.py` (~3 fisiere) - **Test intai (RED)**: `test_preview_arata_motiv_needs_data` — un rand `needs_data` din lipsa odometru apare in preview cu motiv explicit ("lipsa odometru" / mesajul de validare), nu doar numarat la "blocate". - **Continut**: preview-ul de import (cele 6 stari deja existente din 2.5) afiseaza pentru randurile `needs_data`/`needs_review` **motivul** (din validare/`error`), ca operatorul sa stie ce sa corecteze. Reutilizeaza mesajele de validare existente (`validation.py`); fara reguli noi de validare. - **Acceptance criteria**: - [x] Un rand fara odometru apare explicit cu motivul, nu doar in contorul "blocate". - [x] Contoarele preview (ok/needs_data/...) raman corecte. - [x] `python3 -m pytest -q` trece. - **Verificare E2E**: browser — import un fisier fara odometru, vad in preview de ce e blocat randul. ### US-009: Filtrare/cautare in "Trimiteri" (stare / vehicul / data prestatie) **Ca** operator cu sute de trimiteri **vreau** sa filtrez lista **pentru ca** sa gasesc rapid o comanda sau toate cele blocate, fara sa derulez tot. - **Depinde de**: US-003, US-004 - **Fisiere**: `app/web/routes.py` (`fragment_submissions` accepta parametri de filtru), `app/web/templates/_coada.html` (controale filtru), `app/web/templates/_submissions.html`, `app/schema.sql` (index pe `submissions(account_id, status)` daca lipseste — exista deja `idx_submissions_account_status`, deci probabil zero schimbari), `tests/test_web_filtrare.py` (~4 fisiere) - **Test intai (RED)**: `test_filtru_stare` — `?status=needs_data` intoarce doar acele randuri (scoped pe cont); `test_filtru_vehicul` — cautare text pe nr/VIN (case-insensitive); `test_filtru_data` — interval `data_prestatie`. Toate scoped pe cont, fara leak. - **Continut**: controale HTMX (select stare + input text vehicul + interval data) care reincarca `/_fragments/submissions` cu query string; filtrarea pe vehicul/data se face dupa parsarea `payload_json` (text JSON), filtrarea pe stare in SQL (foloseste indexul existent). Pastreaza poll-ul (poll-ul re-trimite filtrul curent). Empty state "nimic pe filtrul curent" **+ buton „sterge filtrele"** (design review) cand exista un filtru activ. - **Acceptance criteria**: - [x] Filtrele combina (stare + vehicul + data) si raman aplicate la refresh-ul de 10s. - [x] Filtrarea e scoped pe cont (fara leak cross-account). - [x] Nu necesita coloane/tabele noi (cel mult confirma indexul existent). - [x] `python3 -m pytest -q` trece. - **Verificare E2E**: browser — filtrez dupa "blocate" si dupa un numar de inmatriculare. ### US-010: Corectie inline pentru randuri ne-trimise blocate (needs_data) **Ca** operator **vreau** sa completez un camp lipsa (ex. odometru) direct pe randul blocat si sa-l re-trimit **pentru ca** acum trebuie sa refac tot fisierul de import doar pentru un camp. - **Depinde de**: US-003, US-004 - **Fisiere**: `app/web/routes.py` (ruta `POST /trimitere/{id}/corecteaza`), `app/web/templates/_trimitere_detaliu.html` (form de corectie pe randurile `needs_data`/`needs_mapping`), `tests/test_web_corectie.py` (~3 fisiere) - **Test intai (RED)**: `test_corectie_needs_data` — un rand `needs_data` din lipsa odometru: completez odometru → re-validare (`validation.py`) → status `queued`, payload actualizat, idempotency recalculata; `test_corectie_sent_interzis` — un rand `sent`/`FINALIZATA` NU poate fi editat (403/refuz, read-only); `test_corectie_coliziune_idempotency` — daca noua cheie coincide cu alt submission existent, corectia se opreste cu mesaj „exista deja o trimitere identica (rand #N)", fara IntegrityError/500 si fara duplicat; `test_corectie_cont_strain` — interzis cross-account. - **Continut**: pe panoul de detaliu (US-004), pentru randuri `needs_data`/`needs_mapping`, un mini-form cu campurile relevante; la submit re-valideaza prin `validation.py` (fara reguli noi), reconstruieste `payload_json` + recalculeaza `idempotency_key` (canonicalize → build_key, ca la enqueue), seteaza status `queued` si `next_attempt_at=now`. **Nu** atinge worker-ul / masina de stari (doar re-enqueue). Randurile `sent`/`FINALIZATA` raman read-only (gard explicit). - **Coliziune idempotency** (decizie eng review): INAINTE de UPDATE, verifica daca noua `idempotency_key` exista deja pe alt submission al contului; daca da, opreste corectia si afiseaza „exista deja o trimitere identica (rand #N)" cu link la acel rand. Fara 500, fara duplicat. (UNIQUE pe `idempotency_key` ar arunca IntegrityError altfel.) - **Loc + eroare (design review)**: formul de corectie traieste IN panoul de detaliu (US-004), nu intr-o sectiune separata. Eroarea de validare se afiseaza clar pe campul invalid (nu un mesaj generic sus). - **Acceptance criteria**: - [x] Un rand `needs_data` corectat valid trece in `queued` cu payload + idempotency actualizate. - [x] Randurile `sent`/`FINALIZATA` nu pot fi editate (gard testat). - [x] Coliziunea de idempotency e prinsa si comunicata clar (rand-duplicat identificat), fara 500. - [x] Corectia respinge date inca invalide (mesaj de validare) si conturi straine. - [x] `python3 -m pytest -q` trece. - **Verificare E2E**: browser — corectez odometru pe un rand blocat, il vad trecand in asteptare; cu worker `--send` pe RAR test, ajunge FINALIZATA. ### US-011: Badge cu contoare pe tab-uri (atentionari) **Ca** operator **vreau** sa vad pe eticheta tab-ului cate lucruri ma asteapta **pentru ca** sa stiu unde sa intervin fara sa deschid fiecare tab. - **Depinde de**: US-007 (mapari), US-004 (trimiteri) - **Fisiere**: `app/web/templates/dashboard.html` (badge pe eticheta tab), `app/web/routes.py` (contoarele deja calculate in `fragment_status`/panel context, transmise la tab-bar), `tests/test_web_badge.py` (~3 fisiere) - **Test intai (RED)**: `test_badge_mapari` — cand exista operatii `needs_mapping`, eticheta "Mapari" poarta un numar; `test_badge_trimiteri_blocate` — cand exista randuri blocate, "Trimiteri" poarta marcaj; `test_badge_zero_ascuns` — fara nimic de rezolvat, niciun badge. - **Continut**: numar mic pe eticheta tab-ului, alimentat din contoarele existente (needs_mapping pt Mapari, blocate pt Trimiteri). Pur prezentare; reutilizeaza ce calculeaza deja status-ul. Accesibil (text in `aria-label`, nu doar culoare). - **Acceptance criteria**: - [x] Badge apare doar cand contorul > 0; dispare la zero. - [x] Numarul e corect si scoped pe cont. - [x] `aria-label`-ul tab-ului include sensul badge-ului (nu doar pastila colorata). - [x] `python3 -m pytest -q` trece. - **Verificare E2E**: browser — cu o operatie nemapata, "Mapari" arata "(1)"; dupa rezolvare, dispare. ## 4. Riscuri - **Scoaterea tab-ului "Import" rupe deep-link-uri/teste** (`?tab=import`, link-uri din `_acasa.html`, "Incarca alt fisier" din stepper). Mitigare: `?tab=import` → redirect la `acasa`; grep dupa toate referintele `tab=import`/`/_fragments/import` inainte de stergere; test dedicat (US-002). - **Citirea `payload_json` pentru detalii** depinde de forma payload-ului, care difera usor intre canalul API si import. Mitigare: helper pur defensiv cu fallback `—` (US-003), testat pe ambele forme; nu se bazeaza pe o cheie obligatorie. - **Leak cross-account** pe noile listari/editari (mapari salvate, formate coloane, detaliu trimitere). Mitigare: toate scoped pe contul sesiunii cu regula NULL→1 (C6/3.3a), test cross-account per ruta noua, re-folosind pattern-ul `account_scope_clause` (3.2). - **Afisare PII (VIN/nr) pe ecran** in Trimiteri. Acceptabil: e proprietatea contului autentificat, scoped pe sesiune; nu se expune in loguri si nu apare in raspunsuri 422 (handler existent in `main.py`). - **Aglomerare "Mapari" cu 3 sectiuni**. Mitigare: empty states + colaps vizual cand o sectiune e goala. ## 5. Intrebari deschise > Se rezolva cu utilizatorul ÎNAINTE de executie (poarta de aprobare PRD). - ~~Detaliul trimiterii: expand inline sau panou?~~ **REZOLVAT (CEO review):** panou dedicat `#trimitere-detaliu`, nu inline — altfel poll-ul de 10s sterge expand-ul (vezi US-004). - "Editare" format de coloane: redeschidem editorul de mapare campuri (`_mapcoloane.html`) prefiltrat pe semnatura salvata, sau permitem doar stergere + re-import? (propunere MVP: stergere + vizualizare; edit de campuri = nice-to-have daca incape in story). - Contor de utilizare pe formate de coloane: il afisam (cost de query) sau il omitem in v1? (propunere: omitem daca nu e ieftin — nu e critic). ## 6. Valuri de executie (graful de dependente) ``` Val 1: [US-001] [US-003] [US-005] [US-006] ← fara dependente, fisiere distincte → paralel (max 3-4) Val 2: [US-002] [US-004] ← US-002←US-001 ; US-004←US-003+US-001 Val 3: [US-007] [US-008] [US-009] [US-010] ← US-007←US-005+US-006 ; US-008←US-002 ; US-009←US-003+US-004 ; US-010←US-003+US-004 Val 4: [US-011] ← US-011←US-007+US-004 (contoare din ambele) ``` > Atentie la fisiere partajate intre valuri: `routes.py`, `dashboard.html`, `_status.html`, > `_submissions.html` si `_trimitere_detaliu.html` (US-004/009/010) sunt atinse de mai multe stories — > secventiaza-le sau worktree + merge de catre lead (vezi anti-pattern ROADMAP). In special US-009 si > US-010 ating ambele acelasi panou de detaliu/tabel — ruleaza-le secvential, nu in paralel. ## 7. Review-uri de plan ### CEO review (2026-06-19) — SELECTIVE EXPANSION - **Abordare aleasa**: A (cele 8 stories complete), cu expansiuni cherry-pick acceptate. - **Expansiuni acceptate** (adaugate ca stories): US-009 filtrare/cautare Trimiteri, US-010 corectie inline pentru `needs_data`, US-011 badge contoare pe tab-uri. - **Finding promovat**: re-rezolvarea automata la editarea unei mapari salvate, din "optional" in scope obligatoriu (US-005). - **Finding de robustete inchis**: detaliul Trimiteri merge in **panou dedicat**, nu inline — altfel poll-ul de 10s din `_submissions.html` ar sterge expand-ul (US-004 + §5). - **Schimbare de scope fata de draft**: editarea de continut e acum permisa pentru randuri ne-trimise blocate (US-010); `sent`/`FINALIZATA` raman read-only. Vezi §2 non-goals actualizat. ### Eng review (2026-06-19) - **Step 0**: 11 stories e mult, dar e o livrabila UI sparta in stories atomice TDD (conventia proiectului), nu overbuild. Risc real = contentia pe fisiere partajate, nu numarul. Scope confirmat. - **Decizie idempotency (US-010)**: corectia detecteaza coliziunea de `idempotency_key` INAINTE de UPDATE si o comunica clar (rand-duplicat identificat), fara 500/duplicat. - **Decizie DRY (US-003)**: helper partajat `app/payload_view.py` folosit si de web si de `GET /v1/prezentari` — o singura sursa de extragere payload→campuri. - **Aserțiune adaugata**: test ca `submissions.payload_json` e plaintext (US-003) — daca vreodata se cripteaza, testul cade si stim sa adaptam. - **Plafon perf notat (US-009)**: filtrarea pe vehicul/data parseaza payload in Python per rand; OK la scara actuala, `json_extract()` daca devine necesar. Nu blocheaza. - **Secventiere intarita**: US-004 (schelet tabel+panou) → apoi US-009 si US-010, strict secvential pe `_submissions.html`/`_trimitere_detaliu.html`/`fragment_submissions`. NU paraleliza valul 3 pe ele. ### Design review (2026-06-19) — rating 7/10 → 9/10 dupa fixuri Layout-urile au fost alese vizual cu utilizatorul (mockup-uri ASCII in AskUserQuestion). Patru cerinte de design adaugate ca AC: - **Accesibilitate bife (US-001)**: glife distincte (✓/✗) + text, nu doar culoare (daltonism). - **Ierarhie Acasa (US-002)**: upload-ul vizual dominant; checklist + ajutor subordonate. - **Vizibilitate panou (US-004)**: scroll-to / evidentiere rand la deschiderea detaliului. - **Stari de eroare (US-009/010)**: „sterge filtrele" + empty state; eroare de validare pe campul invalid, in panoul de detaliu (decizie utilizator). ## 8. Raport review-uri de plan (consolidat) | Review | Data | Rezultat | Decizii cheie | |---|---|---|---| | CEO (SELECTIVE EXPANSION) | 2026-06-19 | Aprobat cu expansiuni | Abordare A; +US-009/010/011; re-rezolvare US-005 obligatorie; detaliu in panou (nu inline) | | Eng | 2026-06-19 | Aprobat | Coliziune idempotency US-010 detectata pre-UPDATE; helper partajat `payload_view.py`; secventiere val 3 | | Design | 2026-06-19 | Aprobat (9/10) | Bife accesibile; ierarhie Acasa; vizibilitate panou; stari de eroare | **Scope final**: 11 stories (US-001…US-011), in 4 valuri. Backend de trimitere (worker, masina de stari, reconciliere, idempotenta ca logica) neatins; singura mutatie de date noua = corectia US-010 (re-enqueue randuri ne-trimise) + posibil un index (US-009). **DECIZII NEREZOLVATE**: niciuna care sa blocheze executia — raman 2 intrebari de finete in §5 (editare format coloane: stergere+vizualizare in MVP; contor utilizare formate: omis daca nu e ieftin), ambele cu propunere si fara impact pe arhitectura. Urmatorul pas (ROADMAP §5): `**Stare**: aprobat` → EXECUTE (TDD pe valuri). Poarta umana: aprobarea PRD. --- ## Raport VERIFY > Completat de subagentul verificator (context curat, ROADMAP §5.6) — 2026-06-19. **Verdict global: PASS.** Toate cele 11 stories verificate prin cod + teste. Regresia de aur intacta. Non-Goals respectate. - **Suita**: `python3 -m pytest -q` → **483 passed** (de la 434 baseline 3.4; +49 teste noi). Verde. - **PASS/FAIL per story**: toate US-001…US-011 PASS. Dovezi (verificator context curat): - US-001: `format_data_rar` (dd.mm.yyyy hh24:mi:ss, lipsa→"—", invalid→fallback fara exceptie); bife accesibile cu glife `✓`/`✗` + text distinct + culoare redundanta; fara font-size <13px. - US-002: Acasa include upload (`hx-post="/_import/upload"`), tab Import scos, `?tab=import`→Acasa (fara 404). - US-003: `app/payload_view.py` pur/defensiv, refolosit de `GET /v1/prezentari` (DRY), payload_json brut neexpus. - US-004: coloane RO, stare umana, detaliu in panou dedicat `#trimitere-detaliu` (nu inline), 404 cross-account. - US-005/006: CRUD `operations_mapping`/`column_mappings` scoped pe cont, re-rezolvare la edit cod. - US-007: `_fragments/mapari` 3 sectiuni cu empty states; `_fragments/cont` fara mapari. - US-008: preview arata mesajul de validare pentru randuri needs_data. - US-009: filtre stare(SQL)/vehicul/data scoped pe cont; empty state cu "sterge filtrele". - US-010: corectie needs_data→queued cu payload+idempotency recalculate; sent read-only (403); coliziune idempotency prinsa pre-UPDATE; cross-account 404. - US-011: badge Mapari(needs_mapping)/Trimiteri(blocate), ascuns la zero, scoped, aria-label cu sens. - **Regresia de aur**: flux import→commit→coada + canal API `POST /v1/prezentari` intacte (`test_import_ui/e2e`, `test_api`, `test_web_tabs`); deep-link-uri `?tab=` valide. - **Non-Goals**: `git diff --stat` confirma `app/worker/`, `app/idempotency.py`, `app/mapping.py`, `app/schema.sql` NEATINSE; CHECK status pastreaza cele 6 stari; niciun endpoint `/v1/*` nou. - **E2E live RAR**: neprobat in sesiune (fara credentiale RAR live, identic cu 3.4) — recomandata probare manuala `./start.sh test both --send` + browser pe `http://localhost:8000/`. ### Findings `/code-review` (high) — reparate inainte de inchidere 1. **Corectie + needs_mapping (sever)**: ruta `POST /trimitere/{id}/corecteaza` re-punea in `queued` fara re-rezolvarea prestatiilor → un cod nemapat putea ajunge la RAR cu `codPrestatie: null` (FINALIZATA ireversibil). **Fix**: re-ruleaza `resolve_prestatii` + `has_no_auto_send` (ca `reresolve_account`); cod nemapat ramane `needs_mapping`. Test: `test_corectie_needs_mapping_nu_ajunge_in_coada`. 2. **Filtru dupa LIMIT 200**: cautarea pe vehicul/data rata randuri mai vechi de 200. **Fix**: fara LIMIT in SQL cand filtrul text/data e activ, plafonare dupa filtrare. 3. **Coliziune idempotency non-atomica (cursa TOCTOU → 500)**: **Fix**: `try/except sqlite3.IntegrityError` in jurul UPDATE-ului `queued`, mesaj prietenos in loc de 500. 4. **Comparatie data non-ISO gresita la filtru**: **Fix**: `_is_iso_date` — compar doar date ISO YYYY-MM-DD. Findings de cleanup (scope-clause hand-coded in `_status_counts`/`_get_acasa_context`, `_render_panel_*` duplicate) sunt preexistente din 3.4, in afara scope-ului 3.5 — neatinse intentionat.