Files
rar-autopass/docs/prd/prd-5.10-ux-filtre-pill-paginare-mapari-meniu.md
Claude Agent 5a964a1a8d feat(5.10): UX trimiteri (pill filtre, paginare, editare) + Mapari in meniu + branding ROMFAST
14 stories TDD prin echipa de workeri (lead orchestreaza, 3 teammates pe valuri cu fisiere disjuncte; routes.py + base.html serializate ca fisiere fierbinti).

- US-001 fix filtrare data (_iso_date_prefix pe garda+comparatie, prinde timestamp cu ora)
- US-002/007 operatie service distincta in payload_view + afisare in detaliu
- US-003 pill-uri categorii (button/aria-pressed; needs_mapping --warn, needs_data/error --err); fara lista ID-uri/dropdown
- US-004 paginare numerotata 25/pag (total ramificat SQL-COUNT vs fetch-all+slice, clamp page, poll pastreaza pagina)
- US-005 VIN block-level sub nr
- US-006/006b editare cod RAR + validare nomenclator + recalcul idempotency (needs_data/needs_mapping via /corecteaza, error via /repune)
- US-008 card eroare 3-niveluri doar pe read-only + rezumat top-of-form
- US-009 Mapari in meniu hamburger; scoatere tab-bar + role=tablist orfan
- US-010/011 pagina Mapari consolidata + butoane icon SVG + dirty-state (fara kebab/emoji)
- US-012/012b header centrat + logo ROMFAST (/static/romfast_logo.png) in header
- US-013 paleta azur ROMFAST (#2E74D6/#1F66C9) + IBM Plex Sans/Mono self-host (woff2 reale)
- US-014 selector tema ciclic Light/Dark/Petrol/Auto + anti-FOUC pe 4 stari

Backend trimitere (worker/masina stari/idempotenta/mapping) + schema NEATINSE (UI/UX pur + 1 fix de filtrare).
VERIFY context curat PASS; /code-review high: 1 finding material reparat (US-006b). Regresie 896 passed, 1 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 20:20:58 +00:00

42 KiB
Raw Blame History

PRD 5.10 — UX trimiteri (pill filtre, paginare, detaliu) + Mapari in meniu

Stare: inchis (2026-06-25 — 14 stories + fix US-006b TDD prin echipa; VERIFY PASS; /code-review high 1 finding material reparat; regresie 896 passed / 1 skipped / 0 failed; fonturi IBM Plex reale)

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

1. Obiectiv

Curatare UX a dashboard-ului pe doua zone: (a) tabelul de trimiteri — pill-uri de filtrare pe categorii de problema in locul listei expandate de ID-uri, filtrare pe interval de data care chiar functioneaza pe timestamp-uri, paginare numerotata, VIN sub numarul de inmatriculare, editarea operatiei RAR si afisarea operatiei de service in detaliu, plus erori mai putin verbose in formularul de editare; (b) Mapari — mutata in meniul hamburger (fara tab-uri pe pagina principala), o singura pagina cu sectiunile consolidate si butoane de salvare/stergere vizibile.

Pur UI/UX + un fix de filtrare backend. Backend-ul de trimitere (worker, masina de stari, idempotenta, mapping-rezolvare) ramane NEATINS.

2. Non-Goals (anti scope-creep)

  • Nu atingem worker-ul, masina de stari, idempotenta sau logica de mapare operatie→cod.
  • Nu permitem editarea operatiei RAR pe stari trimise/in curs (sent/sending/queued) — la RAR FINALIZATA e terminal; editarea apare doar pe needs_data/needs_mapping/error.
  • Nu schimbam paginarea client-side existenta din tabelele Mapari (datatable data-dt).
  • Nu schimbam mecanismul de persistenta/anti-FOUC al temei (PRD 5.3) — il extindem cu teme noi, nu il rescriem.
  • Nu adaugam filtre noi (doar pill-uri pe categoriile de problema deja existente + fix data).
  • Nu modificam contractul API POST /v1/prezentari / GET /v1/prezentari (operatia de service afisata vine din payload-ul deja stocat, nu cere camp nou).
  • Nu reproiectam meniul hamburger in sine — doar adaugam intrarea „Mapari" si scoatem tab-bar-ul.
  • Acceptat explicit (review C4): doua idiomuri de paginare coexista — server-side numerotat pe Trimiteri (US-004) vs client-side data-dt (Inapoi/Inainte) pe Mapari. Diferenta e intentionata, nu „de reparat" ulterior.
  • Acceptat explicit (review C2): butonul ciclic de tema (US-014) are cost de descoperire (4 teme fara optiuni vizibile). Compensat prin aria-label curenta+urmatoarea + tooltip; nu trecem la meniu/popover in aceasta livrabila.

3. Stories atomice

Fiecare story: cea mai mica unitate care lasa sistemul functional. Backend + UI pentru acelasi comportament = 2 stories.

US-001: Fix filtrare pe interval de data (backend)

Ca operator vreau ca filtrul „Data de la / pana la" sa returneze toate trimiterile din acea perioada pentru ca acum, fiindca data_prestatie poate avea ora/minut/secunda, comparatia de string exclude randurile si tabelul apare gol.

  • Depinde de: —
  • Fisiere: app/web/routes.py (fragment_submissions, _is_iso_date), tests/test_web_filtre_submissions.py (~2 fisiere)
  • Test intai (RED): tests/test_web_filtre_submissions.pytest_filtru_data_include_timestamp_cu_ora, test_filtru_data_interval_inclusiv_capete, test_filtru_data_ignora_valori_ne_data
  • Acceptance criteria:
    • (must-fix, review C1) Cauza reala = garda _is_iso_date cere len(s)==10 (routes.py:640-649), deci orice data_prestatie cu ora e exclusa inainte de comparatie. Fix: introdu _iso_date_prefix(value) -> str | None (intoarce value[:10] daca parseaza ca YYYY-MM-DD, altfel None) si foloseste-l atat pentru garda cat si pentru comparatie. Nu modifica doar liniile de comparatie — altfel bug-ul ramane.
    • Filtrarea compara doar portiunea de data (primele 10 caractere, YYYY-MM-DD) a lui data_prestatie, chiar daca valoarea contine ora/minut/secunda (ex. 2026-06-20 14:35:07 sau 2026-06-20T14:35:07).
    • Intervalul e inclusiv la ambele capete: data_de <= data(rand) <= data_pana.
    • O singura limita (doar data_de sau doar data_pana) functioneaza corect.
    • Valorile care nu incep cu o data ISO valida raman excluse din filtru (comportament actual pastrat).
    • python3 -m pytest tests/test_web_filtre_submissions.py -q trece.
  • Verificare E2E: browser pe / — import/seed cu o trimitere cu data_prestatie ce contine ora; filtru pe acea zi → randul apare.

US-002: Expune operatia de service in view-ul de rand si detaliu (backend)

Ca operator vreau sa vad operatia de service originala (codul intern / denumirea venita prin API sau import CSV) pentru ca vreau sa stiu ce a cerut service-ul, nu doar codul RAR mapat.

  • Depinde de: —
  • Fisiere: app/payload_view.py (sau app/web/routes.py _detaliu_ctx/prezentare_din_payload), tests/test_payload_view.py (~2 fisiere)
  • Test intai (RED): tests/test_payload_view.pytest_operatie_service_din_cod_op_service, test_operatie_service_din_denumire, test_fara_operatie_service_cand_lipseste
  • Acceptance criteria:
    • (must-fix, review M1) prezentare_din_payload colapseaza acum service-op si RAR-op in chei suprapuse (operatie = denumire or cod, cod_rar = cod_prestatie; payload_view.py:111-113). Adauga chei noi distincte (ex. op_service_cod + op_service_denumire) din cod_op_service/denumire, separate de operatia RAR mapata.
    • Conventie goala (must-fix, review M1): payload_view emite EMPTY="—" pentru valori lipsa (payload_view.py:119-127), dar US-007 cere „randul nu apare deloc (fara — gol)". Alege O conventie: fie campul nou intoarce ""/None (nu ), fie template-ul testeaza fata de '—' (ca randul VIN, _submissions.html:63). Documenteaz-o aici si in US-007.
    • Cand payload-ul nu contine operatie de service (a venit direct cu cod_prestatie), campul e gol conform conventiei de mai sus, fara a arunca.
    • Helper-ul ramane pur (fara DB), defensiv la payload lipsa/corupt.
    • python3 -m pytest tests/test_payload_view.py -q trece.
  • Verificare E2E: POST /v1/prezentari cu cod_op_service+denumire → randul are operatia de service in context.

US-003: Pill-uri de filtrare pe categorii de problema (UI)

Ca operator vreau pill-uri „Date incomplete / Lipsa cod / Eroare" cu numar in sectiunea de filtrare, in loc de lista expandata #40 TMB...3456 / B28ERR ...si inca 3 pentru ca lista de ID-uri e zgomotoasa; vreau sa apas un pill ca sa filtrez direct pe acea categorie.

  • Depinde de: —
  • Fisiere: app/web/templates/_coada.html, app/web/templates/_status.html, app/web/routes.py (counts per categorie pe fragmentul de filtre), tests/test_web_pill_filtre.py (~4 fisiere)
  • Test intai (RED): tests/test_web_pill_filtre.pytest_pill_per_categorie_cu_numar, test_pill_click_seteaza_status, test_fara_lista_id_uri
  • Acceptance criteria:
    • In sectiunea de filtrare apar pill-uri: Date incomplete (needs_data), Lipsa cod (needs_mapping), Eroare (error), fiecare cu numarul total scoped pe cont; pill-ul lipseste/e ascuns cand numarul e 0.
    • (must-fix, review M3/design-H1) Lista de ID-uri si blocul blocate_actionabil sunt in _status.html:68-77 (nu _coada.html), generate de _blocate_actionabil (routes.py:562-594). La eliminarea ID-urilor, scoate si codul mort care calcula prezentare_din_payload/vin_partial per rand. Pill-urile reutilizeaza contoarele deja calculate (_status_counts/n) — fara filtru backend nou.
    • (must-fix, review design-M1) Decide explicit soarta intregului bloc „Necesita atentia ta" + a contorului „Blocate": pill-urile inlocuiesc link-urile de categorie (nu triplu-encoda aceeasi informatie cu pill + link + dropdown).
    • (must-fix, review M3/A3) Pill-urile sunt elemente focalizabile reale (<button>/<a>, nu <span onclick>), cu focus-visible, activare Enter/Space, si stare activa via aria-pressed (nu doar culoare).
    • Click pe un pill filtreaza tabelul pe acea stare (seteaza status= pe fragment_submissions, deja suportat routes.py:699); pill activ evidentiat; click pe activ revine la „toate".
    • (must-fix, review M3) Sincronizare cu dropdown-ul status din _coada.html:36-44: ori dropdown-ul reflecta pill-ul activ, ori e eliminat — fara desync (click pill vs valoare dropdown stale).
    • Exista si un control „Toate" (toggle) care reseteaza filtrul de categorie. Stare „toate zero" (steady-state sanatos): definita explicit (rand ascuns vs afisaj muted „nicio problema"), nu gap neexplicat.
    • (must-fix, review S1/A5) Matrice de stil pill rezolvata fara contradictie rosu/galben vs accent: inactiv = contur/text pe culoarea categoriei (--err/--warn); activ = umplere pe culoarea categoriei (NU accent albastru — altfel pill rosu „Date incomplete" devine albastru cand e activ). Contrast text pe pill-ul activ verificat AA in toate cele 3 teme.
    • python3 -m pytest tests/test_web_pill_filtre.py -q trece.
  • Verificare E2E: browser pe / — pill-uri cu numere; click pe „Date incomplete" → tabel filtrat; click din nou → toate.

US-004: Paginare numerotata pe tabelul de trimiteri (backend + UI)

Ca operator vreau paginare numerotata pe tabelul de trimiteri pentru ca acum se incarca max 200 randuri fara navigare si nu pot ajunge la cele mai vechi.

  • Depinde de: US-001 (filtrul de data corect intra in acelasi handler)
  • Fisiere: app/web/routes.py (fragment_submissions: param page, total + slice ramificat — vezi C1), app/web/templates/_submissions.html, app/web/templates/_coada.html (poll hx-include + page), tests/test_web_paginare_submissions.py (~3 fisiere)
  • Test intai (RED): tests/test_web_paginare_submissions.pytest_pagina_implicita_25, test_pagina_2_offset, test_total_si_numar_pagini, test_paginarea_pastreaza_filtrele, test_pagina_peste_total_revine_la_ultima, test_poll_pastreaza_pagina
  • Acceptance criteria:
    • (must-fix, review H1) Numararea totalului ramifica dupa tipul de filtru: fara filtru Python (status-only / niciun filtru) → SQL COUNT(*) + LIMIT 25 OFFSET (page-1)*25; cu filtru vehicul/data activ → fetch-all (fara LIMIT, ca azi) → filtreaza in Python → total=len(filtrat) → slice [offset:offset+25]. SQL COUNT(*)/LIMIT/OFFSET e gresit cand filtrul Python e activ. Plafonul de 200 randuri din bucla se inlocuieste cu „fetch-all-then-slice" pe calea filtrata (altfel paginile >8 dispar silentios).
    • Marime pagina fixa 25 randuri; raspunsul include numarul total si pagina curenta.
    • (must-fix, review H2) page in afara intervalului se clampeaza la [1, ceil(total/25)] (nu pagina goala); schimbarea unui filtru reseteaza page la 1.
    • Controale: Inapoi, numere de pagina, Inainte ; pagina curenta evidentiata cu aria-current="page"; capetele dezactivate (disabled) la prima/ultima pagina; pager-ul e ascuns cand pages<=1 sau total==0 (empty state inlocuieste tabelul). „afiseaza X-Y din N" intr-o regiune aria-live="polite".
    • Schimbarea paginii pastreaza filtrele active (status/pill, vehicul, data_de, data_pana) — link-urile de paginare poarta toti parametrii de filtru curenti.
    • (must-fix, review L2) Poll-ul de 15s (_coada.html hx-include="#filtre-trimiteri") NU trebuie sa reseteze pagina: include page curent in include-ul de poll (hidden input actualizat de paginare) SAU documenteaza explicit reset-pe-poll. Test: test_poll_pastreaza_pagina.
    • python3 -m pytest tests/test_web_paginare_submissions.py -q trece.
  • Verificare E2E: browser pe / cu >25 trimiteri — navigare intre pagini, filtru aplicat ramane la schimbarea paginii, poll-ul nu te scoate de pe pagina curenta.

US-005: VIN sub numarul de inmatriculare in tabel (UI)

Ca operator vreau ca VIN-ul sa apara pe rand propriu sub numarul de inmatriculare pentru ca pe aceeasi linie e ingramadit si greu de citit.

  • Depinde de: —
  • Fisiere: app/web/templates/_submissions.html, tests/test_web_submissions_layout.py (~2 fisiere)
  • Test intai (RED): tests/test_web_submissions_layout.pytest_vin_pe_rand_separat_sub_nr
  • Acceptance criteria:
    • (nota, review L1) VIN-ul e deja randat in _submissions.html:61-65, dar inline (<span> dupa nr, aceeasi celula). Schimbarea e mica: element block sub nr (nu structura noua). Testul asserteaza un element block-level, nu doar prezenta.
    • In coloana Vehicul, numarul de inmatriculare e pe primul rand; VIN-ul (sau partiala VIN) apare dedesubt, in stil muted, nu pe aceeasi linie.
    • Cand VIN-ul lipseste, nu apare rand gol (garda != '—' exista deja, _submissions.html:63).
    • Layout-ul ramane fara scroll orizontal pe tabel (scopat .tabel-trimiteri, consistent cu PRD 5.8).
    • python3 -m pytest tests/test_web_submissions_layout.py -q trece.
  • Verificare E2E: browser pe / — VIN sub numar in fiecare rand.

US-006: Editare operatie RAR in formularul de detaliu (UI + backend corectie)

Ca operator vreau sa pot schimba operatia RAR (cod_prestatie) din formularul de detaliu pe trimiterile blocate pentru ca uneori codul mapat e gresit si vreau sa-l corectez inainte de re-trimitere.

  • Depinde de: —
  • Fisiere: app/web/templates/_trimitere_detaliu.html, app/web/routes.py (post_corectie_trimitere ~979-1124), tests/test_web_editare_op_rar.py (~3 fisiere)
  • Test intai (RED): tests/test_web_editare_op_rar.pytest_editabil_arata_select_cod_rar, test_salvare_schimba_cod_si_repune_in_coada, test_idempotency_key_se_schimba, test_cod_invalid_respins, test_sent_nu_arata_select
  • Acceptance criteria:
    • Pe needs_data/needs_mapping/error, formularul afiseaza un select cu codurile din nomenclator pentru operatia RAR, pre-selectat pe codul curent.
    • (must-fix, review H3) Riscul „refoloseste exact calea existenta" e GRESIT formulat: post_corectie_trimitere aplica DOAR campurile vehiculului (vin/nr/data/odometru), nu citeste niciun cod de prestatie (routes.py:1009-1012). Story-ul cere logica noua in handler: (a) camp nou cod_prestatie in form; (b) validare fata de nomenclator (oglindeste routes.py:953-961); (c) injectare in content["prestatii"][i]["cod_prestatie"] inainte de resolve_prestatii. DUPA injectare, restul caii existente (canonicalize → build_key → check coliziune idempotency → re-queue, routes.py:1028-1104) recalculeaza corect payload+cheie (build_key hashuieste cod_prestatie, idempotency.py:34).
    • Codul nou e validat fata de nomenclator; un cod necunoscut e respins (nu se injecteaza, nu se trimite raw — vezi invariantul cod_prestatie validat la ingestie din CLAUDE.md).
    • Test test_idempotency_key_se_schimba: cheia de idempotency difera dupa schimbarea codului (nu doar status=queued).
    • Daca payload-ul are mai multe prestatii, story-ul tinteste prima/operatia editabila explicit (nu ambiguu).
    • Pe sent/sending/queued operatia RAR ramane read-only (fara select).
    • Scoped pe sesiune + CSRF, 404 cross-account.
    • python3 -m pytest tests/test_web_editare_op_rar.py -q trece.
  • Verificare E2E: browser pe / — pe o trimitere needs_data, schimba codul RAR din select, salveaza → cod nou aplicat + cheie idempotency noua; cod invalid → respins; pe o trimitere sent, codul e read-only.

US-007: Afisare operatie de service in detaliu (UI)

Ca operator vreau sa vad operatia de service originala in formularul de detaliu pentru ca vreau sa stiu ce a cerut service-ul prin API/CSV, alaturi de codul RAR.

  • Depinde de: US-002
  • Fisiere: app/web/templates/_trimitere_detaliu.html, tests/test_web_detaliu_op_service.py (~2 fisiere)
  • Test intai (RED): tests/test_web_detaliu_op_service.pytest_detaliu_arata_operatie_service, test_detaliu_omite_cand_lipseste
  • Acceptance criteria:
    • In detaliu apare „Operatie service" (cod intern + denumire) cand a existat in payload, distinct de „Operatie RAR".
    • Cand operatia de service lipseste (a venit direct cu cod_prestatie), randul nu apare deloc (fara „—" gol).
    • Apare atat in contextul editabil cat si in cel read-only.
    • python3 -m pytest tests/test_web_detaliu_op_service.py -q trece.
  • Verificare E2E: browser pe / — trimitere venita cu cod_op_service arata operatia de service in detaliu; una venita cu cod_prestatie direct nu o arata.

US-008: Simplificare eroare in formularul de editare (UI)

Ca operator vreau ca in formularul de editare sa apara doar textul simplu al erorii (subliniat), nu blocul verbose pe 3 niveluri pentru ca „De ce / Cum repari" + prefixul tehnic dubleaza un mesaj deja descriptiv (ex. „odometruFinal trebuie sa fie un numar intreg (ca string).").

  • Depinde de: —
  • Fisiere: app/web/templates/_trimitere_detaliu.html (zona editabila), tests/test_web_detaliu_eroare_simpla.py (~2 fisiere)
  • Test intai (RED): tests/test_web_detaliu_eroare_simpla.pytest_form_editare_fara_card_3niveluri, test_eroare_pe_camp_doar_text_simplu
  • Acceptance criteria:
    • (must-fix, review M4) Blocul erori_3n/card_erori e acum randat inainte de form, in afara ramurii {% if editabil %} (_trimitere_detaliu.html:22-27) — deci apare si pe read-only. Ascunde-l DOAR in editare: muta-l/infasoara-l in {% if not editabil %} ca sa-l pastrezi pe contextele read-only (catalogul PRD 5.4).
    • Eroarea per camp ramane sub input ca text simplu subliniat (mesajul descriptiv). Nota: macro-ul camp printeaza deja doar err_map.get(nume) (mesajul, fara cod de camp) — verifica doar ca message nu inglobeaza numele campului.
    • (must-fix, review M6/design) Erorile fara camp (field is None) nu trebuie sa dispara silentios cand scoatem cardul 3n: defineste unde apar (ex. un rezumat simplu top-of-form) in formularul de editare.
    • Restul contextelor (lista compacta / detaliu read-only) raman neschimbate — simplificarea e scopata doar pe formularul de editare.
    • python3 -m pytest tests/test_web_detaliu_eroare_simpla.py -q trece.
  • Verificare E2E: browser pe / — corectie cu odometru invalid → sub camp apare doar textul erorii subliniat, fara cardul pe 3 niveluri.

US-009: Mapari in meniul hamburger + scoatere tab-uri (UI)

Ca operator vreau ca Mapari sa fie o intrare in meniul hamburger, nu tab pe pagina principala pentru ca vreau pagina principala curata (doar Acasa), iar tab-bar-ul Acasa/Mapari incurca.

  • Depinde de: —
  • Fisiere: app/web/templates/base.html (meniu #cont-menu), app/web/templates/dashboard.html (tab-bar), app/web/routes.py (rutare pagina Mapari, ex. /?tab=mapari sau /mapari), tests/test_web_mapari_meniu.py (~3 fisiere)
  • Test intai (RED): tests/test_web_mapari_meniu.pytest_meniu_contine_mapari, test_pagina_principala_fara_tabbar_mapari, test_ruta_mapari_randeaza_sectiunea
  • Acceptance criteria:
    • Meniul hamburger contine o intrare Mapari (cu badge pentru needs_mapping daca exista contoare). Badge-ul muta din tab-bar pe item-ul de meniu — sursa contorului ramane badges.mapari (routes.py:423-424); verifica sa nu ramana badge dangling.
    • Tab-bar-ul Acasa/Mapari de pe pagina principala e eliminat; Acasa devine continutul principal direct.
    • (must-fix, review design-C1) Scoate si schela ARIA role="tablist" orfana + JS-ul de navigare cu sageti din dashboard.html:16-88 (un role="tablist" cu un singur tab e o violare ARIA). Curata plumbing-ul active_tab ramas fara sens daca nu mai e folosit.
    • Exista o ruta dedicata care randeaza pagina Mapari (server-side, deep-link). ?tab=mapari ramane valid (_TABS_VALIDE include mapari, routes.py:155; randat de _render_panel_for_tab) — nu da 404. Verifica sa nu ramana hx-get catre un element de tab eliminat (banner/badge).
    • python3 -m pytest tests/test_web_mapari_meniu.py -q trece.
  • Verificare E2E: browser pe / — fara tab-bar; din ☰ → Mapari → pagina Mapari.

US-010: Restructurare pagina Mapari intr-o singura pagina consolidata (UI)

Ca operator vreau o singura pagina Mapari cu „De rezolvat" prima, apoi salvate / reguli text / formate coloane, fara sectiunea de ajutor si fara textul gol pentru ca sectiunile separate si textele de ajutor ma incurca.

  • Depinde de: US-009
  • Fisiere: app/web/templates/_mapari.html, tests/test_web_mapari_layout.py (~2 fisiere)
  • Test intai (RED): tests/test_web_mapari_layout.pytest_de_rezolvat_prima, test_fara_ajutor_si_empty_text, test_ordine_sectiuni
  • Acceptance criteria:
    • Ordinea sectiunilor: (1) De rezolvat prima, (2) Mapari operatii salvate, (3) Reguli automate (text), (4) Formate de coloane salvate.
    • Sectiunea de ajutor (<details class="ajutor-mapari">) e eliminata.
    • Textul empty-state „Nicio operatie nemapata — tot ce a venit s-a tradus in coduri RAR. Importa un fisier nou..." e eliminat (sectiunea „De rezolvat" goala nu mai afiseaza acel paragraf).
    • Fiecare rand din „De rezolvat" pastreaza select-ul + butonul de salvare vizibil (vezi US-011).
    • python3 -m pytest tests/test_web_mapari_layout.py -q trece.
  • Verificare E2E: browser pe pagina Mapari — ordine corecta, fara ajutor, fara empty-text.

US-012: Branding header „by ROMFAST" + titlu centrat (UI)

Ca utilizator vreau ca header-ul sa aiba branding-ul „by ROMFAST" si titlul pe mijloc pentru ca vreau identitate vizuala clara, parte din familia ROMFAST/ROA, nu un header anonim.

Sistem de design complet: DESIGN.md (sectiunile „Header & branding" + „Culori de brand").

  • Depinde de: —
  • Fisiere: app/web/templates/base.html (header + CSS), tests/test_web_header_branding.py (~2 fisiere)
  • Test intai (RED): tests/test_web_header_branding.pytest_header_contine_by_romfast, test_titlu_centrat
  • Acceptance criteria:
    • Titlul „Gateway RAR AUTOPASS" e centrat in header (grila 3 coloane: controale — tema, versiune, ☰ — la dreapta, fara a strica centrarea optica a titlului).
    • (must-fix, review S2) Plaseaza explicit badge-ul env (test/prod, azi base.html:320) in grila — celula stanga (echilibru) sau langa titlu — si defineste ordinea de colaps pe mobil (3-col cu titlu centrat + controale care wrap e fragil).
    • Sub titlu, mic: wordmark „by ROMFAST" redat ca text stilizat — by in --muted, ROM in #D1342F (rosu logo), FAST in #2E74D6 (albastru logo). NU se foloseste PNG-ul 3D al logo-ului.
    • Responsiv: pe mobil wordmark-ul ramane sub titlu, controalele nu se suprapun (degrada elegant).
    • Light + dark OK (wordmark pe culori proprii, lizibil pe ambele fundaluri).
    • python3 -m pytest tests/test_web_header_branding.py -q trece.
  • Verificare E2E: browser pe / desktop + mobil — titlu centrat, „by ROMFAST" cu ROM rosu / FAST albastru, controale la dreapta.

US-013: Tema de culori ROMFAST + tipografie (UI)

Ca utilizator vreau o paleta cu accent albastru ROMFAST (ca romfast.ro) si o tipografie coerenta pentru ca acum totul e gri si fara identitate, iar produsul e parte din familia ROA.

Valori complete: DESIGN.md (sectiunile „Decizie cromatica" + „Tipografie").

  • Depinde de: design-consultation (DONE — DESIGN.md scris)
  • Fisiere: app/web/templates/base.html (variabile :root + [data-theme="light"] + font-family), app/web/static/fonts/ (woff2 IBM Plex), tests/test_web_tema_culori.py (~3 fisiere)
  • Test intai (RED): tests/test_web_tema_culori.pytest_paleta_accent_azur_definita, test_font_ibm_plex_aplicat, test_contrast_aa_pe_text_principal
  • Acceptance criteria:
    • Accentul devine azur ROMFAST: --accent:#2E74D6 (dark) / #1F66C9 (light), aplicat prin variabile CSS (butoane primare, pill activ, linkuri, focus) — fara culori noi hardcodate imprastiate.
    • Neutrele actualizate conform DESIGN.md (dark --bg:#0f1218/--card:#181c24; light --bg:#f5f7fa/--card:#ffffff); stari --ok/--warn/--err pastrate AA per tema.
    • Tipografie: font-family UI = IBM Plex Sans (fallback system-ui); monospace (coduri RAR/VIN/nr.) = IBM Plex Mono. Self-host woff2 cu font-display:swap (subset latin-ext pentru diacritice). Fallback de sistem nu strica layout-ul.
    • (must-fix, review M3/design) font-display:swap produce un FOUT system-ui→IBM Plex; pe coloanele tabulare (VIN/coduri, tabular-nums base.html:54) o nepotrivire de metrici da reflow. Defineste fallback cu metrici ajustate (size-adjust/ascent-override) SAU accepta explicit FOUT-ul si confirma ca tabularele nu fac reflow vizibil.
    • Contrastul textului principal ramane AA in ambele teme; accentul-ca-text pe alb foloseste varianta inchisa (#1F66C9).
    • Comutatorul light/dark existent (PRD 5.3) + anti-FOUC functioneaza in continuare.
    • python3 -m pytest tests/test_web_tema_culori.py -q trece.
  • Verificare E2E: browser pe / — accent azur + IBM Plex in light si dark, pill-uri rotunjite ca pe romfast.ro, contrast verificat.

US-014: Selector de tema ciclic (Light/Dark/Petrol/Auto) (UI)

Ca utilizator vreau ca butonul de tema sa cicleze prin mai multe teme (inclusiv una petrol) pentru ca vreau sa-mi aleg aspectul, nu doar light/dark, ca pe demoanaf.ro.

Spec complet: DESIGN.md (sectiunile „Selector de tema" + paleta „Petrol").

  • Depinde de: US-013 (paleta azur + variabilele de baza)
  • Fisiere: app/web/templates/base.html (script anti-FOUC + [data-theme="petrol"] + JS comutator + buton), tests/test_web_selector_tema.py (~2 fisiere)
  • Test intai (RED): tests/test_web_selector_tema.pytest_petrol_theme_definit, test_buton_cicleaza_temele
  • Acceptance criteria:
    • Butonul de tema cicleaza la click: Light → Dark → Petrol → Auto → Light; iconita + aria-label/title reflecta tema curenta.
    • (must-fix, review A2/design) Buton accesibil: aria-label anunta curenta + urmatoarea („Tema: Petrol, apasa pentru Auto"); schimbarea anuntata via regiune aria-live="polite". Tooltip enumera ciclul. Cost de descoperire (4 teme intr-un buton fara optiuni vizibile) acceptat explicit in Non-Goals.
    • Tema noua Petrol definita ca [data-theme="petrol"] cu valorile din DESIGN.md (accent --accent:#0E7C7B, neutre petrol-inchise).
    • Auto urmeaza prefers-color-scheme (rezolva la Light/Dark azur); nu seteaza data-theme fix.
    • (must-fix, review M4/design) Scriptul anti-FOUC din <head> (base.html:19-29) cunoaste azi doar light/dark. Extinde-l atomic sa enumere toate cele 4 stari; o valoare localStorage.theme legacy/necunoscuta are fallback definit (nu blink, nu stare invalida). Rezolva „Auto" la light/dark inainte de primul paint.
    • (must-fix, review S3/design) Wordmark-ul „FAST" #2E74D6 (albastru) coexista pe ecranul Petrol cu accentul teal #0E7C7B — verifica armonia/contrastul wordmark-ului (ROM rosu + FAST albastru) pe toate cele 3 teme concrete, nu doar light/dark.
    • Toate suprafetele raman lizibile (AA pe text principal) in fiecare din cele 3 teme concrete.
    • python3 -m pytest tests/test_web_selector_tema.py -q trece.
  • Verificare E2E: browser pe / — click pe buton cicleaza Light→Dark→Petrol→Auto; refresh pastreaza tema; Petrol are accent teal, fara blink la load.

US-011: Butoane icon salvare/stergere vizibile + evidentiere modificari nesalvate (UI)

Ca operator vreau butoane mici cu icon de salvare/stergere mereu vizibile pe randurile de mapari, evidentiate cand am modificari nesalvate pentru ca acum trebuie sa intru intr-un meniu contextual (kebab) si nu imi dau seama ca trebuie sa apas „Salveaza".

  • Depinde de: US-010
  • Fisiere: app/web/templates/_mapari.html, app/web/templates/base.html (CSS/JS mic pentru stare „dirty"), tests/test_web_mapari_actiuni.py (~3 fisiere)
  • Test intai (RED): tests/test_web_mapari_actiuni.pytest_butoane_icon_vizibile_pe_rand, test_fara_kebab_menu
  • Acceptance criteria:
    • Pe „Mapari operatii salvate" si „Reguli automate (text)", actiunile Salveaza/Sterge sunt butoane mici cu icon mereu vizibile pe rand, nu ascunse in meniu kebab.
    • (must-fix, review A1 — decizie de taste, vezi poarta) Icon-urile sunt SVG/text stilizate ca icon (reuse .icon-btn), NU emoji brute 💾/🗑: emoji-ul nu se recoloreaza pe teme/dirty-state si randeaza inconsistent intre OS-uri. Numele accesibil din aria-label; glifa decorativa aria-hidden.
    • Meniul kebab (position:fixed) e eliminat.
    • (must-fix, review S4) Cand utilizatorul schimba select-ul unui rand, butonul de salvare devine evidentiat concret (ex. fundal --accent + nu emoji) ca semnal de „modificari nesalvate"; in starea normala e discret. Starea „dirty" e efemera per-render (un swap outerHTML o reseteaza — fara persistenta asteptata).
    • Confirmarea la stergere (hx-confirm) se pastreaza.
    • (must-fix, review A6) Accesibil: butoanele au aria-label descriptiv; pe mobil regula 44px e scopata ca icon-urile sa ramana icon-size (nu full-width ca butoanele normale min-height:44px;width:100%), cu zona de atingere adecvata.
    • python3 -m pytest tests/test_web_mapari_actiuni.py -q trece.
  • Verificare E2E: browser pe pagina Mapari — schimba un cod RAR la o mapare salvata → butonul de salvare se evidentiaza; salveaza/sterge din icon-uri vizibile.

4. Riscuri

  • Filtrarea pe data (US-001): data_prestatie poate veni in formate diferite (ISO cu/ fara ora, T vs spatiu). Mitigare: normalizeaza la primele 10 caractere doar cand sunt o data ISO valida; pastreaza excluderea valorilor ne-ISO (comportament existent) + test parametrizat pe formate.
  • Paginare + filtru in Python (US-004): filtrele vehicul/data se aplica post-SQL; numararea totalului trebuie sa respecte filtrul, nu doar COUNT(*) SQL. Mitigare: numara dupa filtrare la scara actuala (plafon perf deja notat in cod) si testeaza total cu filtru activ.
  • Editare operatie RAR (US-006): schimbarea codului trebuie sa recalculeze payload + idempotency ca la corectia needs_data existenta, altfel risca chei divergente. Mitigare: refoloseste exact calea de corectie/mapare existenta, nu una noua.
  • Mutare Mapari din tab (US-009): deep-link-uri vechi ?tab=mapari pot exista. Mitigare: alias/redirect, test ca nu da 404.
  • Regresie pe contexte de eroare (US-008): simplificarea trebuie scopata DOAR pe formularul de editare; lista compacta + detaliu read-only + API raman pe catalogul 3-niveluri (PRD 5.4).

5. Intrebari deschise

Rezolvate cu utilizatorul inainte de executie (poarta de aprobare PRD). Toate clarificate prin AskUserQuestion in sesiunea de planificare:

  • Pill filtre: un pill per categorie (Date incomplete / Lipsa cod / Eroare) cu numar, nu un singur pill combinat.
  • Paginare: pagini numerotate, 25 randuri/pagina, total vizibil, pastreaza filtrele.
  • Editare operatie RAR: doar pe stari editabile (needs_data/needs_mapping/error); read-only pe sent/sending/queued.
  • Layout Mapari: De rezolvat prima, apoi salvate / reguli text / formate coloane; fara ajutor, fara empty-text.
  • Butoane icon: evidentiere la modificare (dirty state) pe langa vizibilitatea permanenta.
  • Branding + paleta (US-012/US-013): rezolvate prin design-consultationDESIGN.md. Wordmark „by ROMFAST" sub titlu (ROM rosu #D1342F + FAST albastru #2E74D6); accent functional azur ROMFAST (#2E74D6/#1F66C9), consistent cu romfast.ro; font IBM Plex Sans + Mono.
  • Selector de tema (US-014): buton ciclic ca demoanaf.ro, set Light/Dark/Petrol/Auto; petrolul (directia initiala) revine ca tema selectabila.

6. Valuri de executie (graful de dependente)

Val 1 (paralel, fisiere disjuncte):
  [US-001] fix filtrare data        (routes.py)
  [US-002] operatie service in view (payload_view.py)
  [US-003] pill-uri filtre          (_coada.html / _status.html)
  [US-005] VIN sub nr               (_submissions.html)
  [US-008] eroare simpla in editare (_trimitere_detaliu.html)
  [US-009] Mapari in meniu          (base.html / dashboard.html)

Val 2 (deblocate de Val 1):
  [US-004] paginare        ← US-001 (acelasi handler)
  [US-006] editare op RAR  ← (independent, dar atinge _trimitere_detaliu.html → dupa US-008)
  [US-007] op service detaliu ← US-002
  [US-010] restructurare Mapari ← US-009

Val 3:
  [US-011] butoane icon + dirty state ← US-010

Branding/tema (dupa design-consultation, ating base.html → serializeaza intre ele):
  [US-013] paleta azur ROMFAST + IBM Plex   ← DESIGN.md (DONE)
  [US-012] header „by ROMFAST" + titlu centrat   ← (independent, dar atinge base.html → dupa US-013)
  [US-014] selector tema ciclic + Petrol/Auto    ← US-013

Nota orchestrare: US-006, US-007, US-008 ating toate _trimitere_detaliu.html → serializeaza-le pe acelasi fisier (nu in worktree-uri paralele). La fel US-010/US-011 pe _mapari.html, si US-012/US-013/US-014 pe base.html (toate trei ating header/CSS/script — un singur autor secvential).


Raport autoplan (review CEO + Design + Eng)

Rulat 2026-06-25. Codex indisponibil (usage limit pe contul OpenAI pana 2026-07-18) → toate fazele degradate la subagent-only ([codex-unavailable], single-model). Voci independente Claude pe fiecare faza (fara context prealabil). Constatarile tehnice critice verificate direct in cod de lead inainte de aplicare.

Consensus (subagent-only — o singura voce per dimensiune)

CEO          : Premise valide (bug US-001 confirmat real). Recomandare = split branding/tema. 
DESIGN       : Tare pe backend; lacune in stari vizuale + a11y controale noi (emoji, ciclu tema, pill).
ENG          : 4 must-fix verificate in cod (C1 _is_iso_date prefix; H1 total post-filtru; 
               H3 US-006 nu e reuse pur; M4 card 3n in afara ramurii editabil).

Cross-phase themes (semnalate de 2+ voci independent)

  • US-006 e cel mai riscant story — CEO (riscant, confirma calea) + Eng (H3: NU e reuse pur, cere logica noua + validare nomenclator + assert pe cheia idempotency). Aplicat in AC.
  • A11y a controalelor noi — Design (emoji, ciclu tema, pill fara semantica) e clasa de probleme pe care TDD pe substring Jinja o trece dar livreaza un experience rupt la tastatura/screen-reader. Aplicat (SVG/text, aria-pressed, aria-live).
  • Triplu-encoding contoare problema — Design (pills + „Necesita atentia ta" + badge Mapari). Aplicat: pills inlocuiesc link-urile.

Decizii auto (principiile 6) — must-fix aplicate in AC

# Faza Decizie Clasificare Principiu Rationament
1 Eng US-001: _iso_date_prefix (garda+comparatie pe [:10]) Mecanic P1/P5 Fara asta bug-ul ramane; o singura solutie corecta
2 Eng US-004: total ramificat (SQL count fara filtru Python / fetch-all+slice cu filtru) Mecanic P1 Contradictie Fisiere↔AC; SQL COUNT gresit cu filtru Python
3 Eng US-004: clamp page, ascunde pager la pages<=1, poll pastreaza pagina Mecanic P1 Edge-cases + regresie poll
4 Eng US-006: camp cod_prestatie + validare nomenclator + assert cheie idempotency (NU reuse pur) Mecanic P1/P5 Verificat in routes.py:1009-1012
5 Eng US-002/007: cheie payload distincta + conventie goala ( vs None) Mecanic P5 Chei suprapuse azi; conventie ambigua intre stories
6 Eng US-008: muta cardul 3n in {% if not editabil %} + erori fara camp Mecanic P1 Cardul e azi in afara ramurii editabil
7 Design US-003: pill-uri butoane reale (aria-pressed), inlocuiesc link-urile, matrice culori categorie (nu accent), sincron cu dropdown Mecanic P1 A11y + anti triplu-encoding
8 Design US-009: scoate schela role="tablist" orfana + JS Mecanic P1 Un tab in tablist = violare ARIA
9 Design US-011: icon-uri SVG/text (nu emoji brute), dirty-state concret, 44px scopat Taste→aplicat P1/P5 User a scris emoji; SVG satisface intentia + a11y + recolorare (vezi poarta)
10 Design US-012/013/014: env badge in grila; FOUT pe tabulare; legacy localStorage.theme; ciclu a11y; wordmark pe 3 teme Mecanic P1 Stari vizuale lipsa

Decizii de taste / User Challenge — la poarta (NU auto-decise)

  • UC-1 (CEO, single-voice): split branding/tema (US-012/013/014) intr-un PRD separat + taie Petrol & selectorul ciclic. Default = PASTRAM (user a ales explicit in sesiune: Petrol ca tema, ciclu ca demoanaf, IBM Plex). CEO ruleaza ca o singura voce (codex jos) — recomandare, nu decizie.
  • UC-2 (CEO): paginarea (US-004) poate fi prematura la scara actuala (purge 90z). Default = PASTRAM (alegere user); cost low (1 query de validat numarul max randuri/cont inainte de build).
  • T-1 (Design A1): emoji 💾/🗑 (scrise de user) → SVG/text icon. Aplicat ca SVG (satisface ambele), surfata la poarta fiindca user a scris emoji literal.

Deferat (TODOS, nu in 5.10)

  • Hardening GET-uri de listare globale/neprotejate (CLAUDE.md, semnalat de CEO ca valoare mai mare) — separat, nu expandam acest PRD.
  • Validare empirica „>200 randuri/cont" pentru US-004 si „cerere reala >2 teme" pentru US-014.

Verdict poarta (2026-06-25)

  • UC-1 → PASTRAM tot in 5.10 (14 stories, functional + branding/tema). User a confirmat la poarta; CEO single-voice = recomandare, nu verdict. Riscul FOUC/FOUT mitigat prin AC-urile adaugate.
  • UC-2 → PASTRAM paginarea (US-004). Cost mic, specificata corect dupa review.
  • T-1 → icon-uri SVG/text (nu emoji brute), aplicat in US-011.
  • APROBAT. Toate must-fix-urile aplicate in AC. Gata de executie (TDD prin echipa, vezi valuri §6) sau /ralph.

Raport VERIFY

Completat de subagentul verificator (context curat, fara transcriptul executiei) — ROADMAP §5.6. 2026-06-25.

Verdict: PASS — toate cele 14 stories PASS, 0 FAIL. Regresie completa 892 passed / 1 skipped / 0 failed (+49 teste fata de baseline 843; skipped = test live RAR, opt-in). Backend trimitere (worker/masina stari/idempotenta/mapping) + schema-send NEATINSE (Non-Goals respectate).

PASS per story cu dovezi verificate direct in cod:

  • US-001 PASS — _iso_date_prefix folosit ATAT pentru garda CAT si comparatie (routes.py), interval inclusiv, ne-ISO excluse.
  • US-002 PASS — chei distincte op_service_cod/op_service_denumire, conventie goala "" (nu ).
  • US-003 PASS — pill-uri <button> cu aria-pressed; needs_mapping --warn, needs_data/error --err; dropdown status eliminat (fara desync); lista ID-uri eliminata.
  • US-004 PASS — total ramificat (SQL COUNT fara filtru Python / fetch-all+slice cu filtru); clamp page; poll pastreaza pagina (OOB f-page); pager ascuns la pages<=1.
  • US-005 PASS — VIN element block-level (<div>) sub nr, garda VIN lipsa.
  • US-006 PASS — logica noua in post_corectie_trimitere (citeste cod_prestatie, valideaza nomenclator, injecteaza in content["prestatii"][0] inainte de resolve_prestatii); cheia idempotency difera; read-only pe sent/sending/queued.
  • US-007 PASS — Operatie service distinct de Operatie RAR, absent cand lipseste, in editabil + read-only.
  • US-008 PASS — card 3n in {% if not editabil %}; erori fara camp = rezumat top-of-form in editare.
  • US-009 PASS — Mapari in #cont-menu cu badge; tab-bar + role=tablist + JS sageti eliminate; ?tab=mapari nu da 404.
  • US-010 PASS — ordine sectiuni corecta; ajutor-mapari + empty-text eliminate.
  • US-011 PASS — butoane .icon-btn SVG (fara emoji); kebab eliminat; dirty-state pe change select; hx-confirm pastrat.
  • US-012 PASS — header grid 3col centrat; env badge in grila; wordmark by ROMFAST (ROM #D1342F, FAST #2E74D6) text, nu PNG.
  • US-013 PASS — accent #2E74D6/#1F66C9/#0E7C7B; @font-face IBM Plex Sans/Mono. Rezerva fonturi woff2 placeholder REZOLVATA post-VERIFY: cele 8 fisiere au fost inlocuite cu subseturile reale IBM Plex (fontsource @fontsource/ibm-plex-*@5.0.8, woff2 valide latin + latin-ext, 400/500/700 sans + 400 mono).
  • US-014 PASS — [data-theme=petrol] #0E7C7B; ciclu Light->Dark->Petrol->Auto; anti-FOUC extins la 4 stari + fallback legacy; aria-label curenta+urmatoarea + aria-live.

Limitari documentate:

  • E2E browser NEPROBAT in sandbox (fara browser interactiv, consistent cu 5.8/5.9). Recomandat la deploy: ./start.sh test both --send + browser pe http://localhost:8000/ pentru proba vizuala (pill-uri/click, paginare >25, VIN sub nr, ciclu tema fara blink, header centrat, Mapari din ☰).
  • Live RAR --send neprobat (UI pur; backend trimitere neatins — risc minim).

CLOSE — /code-review high (2026-06-25)

1 finding material reparat TDD (US-006b): US-006 acoperea doar needs_data/needs_mapping, dar AC + intrebarea deschisa cer si error. Fix: constanta separata _EDITABILE_OP=(needs_data,needs_mapping,error) pentru selectul de cod RAR; pe error editarea codului trece prin /repune (re-queue), cu validare nomenclator, re-rezolvare, canonicalize + build_key (cheie idempotency noua), check coliziune (pre-UPDATE + IntegrityError), error→queued (rar_error NULL / retry_count 0). _CORECTABILE + post_corectie_trimitere neatinse (fara regresie US-010/US-011). 9/9 teste editare; regresie completa 896 passed / 1 skipped / 0 failed.

Findings minore (debt acceptat, non-blocante): (a) comparatie data bruta d_prefix > data_pana — sigura prin UI (<input type=date> zero-padded), vulnerabila doar la URL fabricat; (b) dublu load_nomenclator cand nomenclatorul e legitim gol; (c) linkurile de paginare reafiseaza vehicul cu majuscule (cosmetic); (d) duplicare cale validare+inject+idempotency intre post_corectie_trimitere si post_repune_trimitere (candidat de extras intr-un helper).

US-012b (decizie user post-review): in header se foloseste LOGO-ul PNG real (/static/romfast_logo.png, .brand-logo ~28px) in loc de wordmark-ul text din US-012. Fundal transparent + culori proprii -> lizibil pe toate temele. test_web_header_branding.py actualizat sa verifice <img romfast_logo.png> + alt. DESIGN.md actualizat. Regresie ramane 896 passed.

Stare finala: VERIFY PASS + fix code-review (US-006b) + logo header (US-012b) aplicate. Gata de commit (poarta umana).