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

431 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- /autoplan restore point: ~/.gstack/projects/romfast-rar-autopass/main-autoplan-restore-20260625-120049.md -->
# 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.py``test_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.py``test_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.py``test_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.py``test_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.py``test_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.py``test_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.py``test_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.py``test_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.py``test_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.py``test_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.py``test_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.py``test_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.py``test_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.py``test_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:
- [x] Pill filtre: **un pill per categorie** (Date incomplete / Lipsa cod / Eroare) cu numar, nu un singur pill combinat.
- [x] Paginare: **pagini numerotate**, 25 randuri/pagina, total vizibil, pastreaza filtrele.
- [x] Editare operatie RAR: **doar pe stari editabile** (`needs_data`/`needs_mapping`/`error`); read-only pe `sent`/`sending`/`queued`.
- [x] Layout Mapari: **De rezolvat prima**, apoi salvate / reguli text / formate coloane; fara ajutor, fara empty-text.
- [x] Butoane icon: **evidentiere la modificare** (dirty state) pe langa vizibilitatea permanenta.
- [x] Branding + paleta (US-012/US-013): rezolvate prin `design-consultation``DESIGN.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.
- [x] 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).**