Files
rar-autopass/docs/prd/prd-5.15-propagare-design-dashboard-editare.md
Claude Agent 4caf055c53 docs(prd): 5.15 revizuit prin /plan-ceo-review (SELECTIVE EXPANSION)
Review CEO + spec-review independent (scor 7/10). Scope 8 -> 10 stories / 6 valuri.

Decizii incorporate (D6-D10):
- D6 strip sanatate mereu-vizibil deasupra contoarelor (zero-silent-failures)
- D7 operatia ramane in op_service + copiata in obs (nu se muta)
- D8 obs EXCLUS din idempotenta (idempotency.py:98) - AC US-005 corectat
- D9 secventiere 5.15 inainte de 5.14
- D10 4 extinderi acceptate: US-009 salvare mapare din chip, US-010 bulk-fix,
  require dinamic odometruInitial + keyboard-first (US-007)

Remedieri din spec-review independent:
- #1 contradictie prestatii: itemii pastreaza op_service/denumire, idPrezentare
  in payload.py (rupea D7/US-009 in forma initiala)
- #2 sent_today/month: status='sent' AND date(updated_at), fara migrare
- #3 US-006 numeste liniile de rewrite din handler-e (nu "fara schimbare logica")
- #5/#6 nota suprafata JS + click target "De corectat"

TODOS += premisa mobil nevalidata, dedup teme grafit~dark, optiune PRD separat US-009/010.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:31:41 +00:00

423 lines
32 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.

# PRD 5.15 — Propagare design landing in aplicatie (dashboard compact + editare slim, VIN unic, prestatii multi-select)
**Stare**: draft
> Proces complet: `docs/ROADMAP.md` §5. Contract RAR (sursa de adevar): `docs/api-rar-contract.md`.
> Sistemul de design al landing-ului: `app/web/templates/landing.html` (commit 41aa385), `DESIGN.md`.
> Starea trece: `draft -> aprobat -> in-executie -> verify-pass -> inchis`.
## 1. Obiectiv
Propagam sistemul de design al landing-ului comercial (carduri/liste/formulare compacte,
slim, si cele 4 teme grafit/cobalt/cupru/hartie) in aplicatia reala. Concret: dashboard-ul
Acasa primeste cardurile-contor + lista de trimiteri slim din mockup-ul hero, iar formularul
de editare trimitere primeste designul compact din mockup-ul "prestatie noua", cu **un singur
camp VIN**, **Observatii** ca text liber pentru operatiile de service si **prestatii ca chips
multi-select** de coduri RAR. Userul a cerut explicit replicarea acestor doua mockup-uri pentru
ca ii place cat de compacte/slim sunt.
Decizii de produs confirmate cu userul (poarta de aprobare a acestui PRD):
- **D1**: cardurile-contor INLOCUIESC bara de status actuala (`_status.html`); pastram doar
indicatorii de sanatate worker/RAR intr-o forma compacta.
- **D2**: temele sunt ADITIVE — pastram light/dark/petrol + Auto SI adaugam cele 4 din landing
(grafit/cobalt/cupru/hartie). Selectorul ciclic le parcurge pe toate. (grafit ~ dark si
hartie ~ light raman optiuni separate, la cererea userului.)
- **D3**: prestatiile sunt chips reale multi-select — utilizatorul poate adauga mai multe coduri
din nomenclatorul RAR si poate sterge oricare; se trimite lista `prestatii` completa (RAR
accepta lista `{codPrestatie, idPrezentare:null}``docs/api-rar-contract.md` §payload).
- **D4** (contor Trimise): cardul "Trimise" arata trei valori temporale — **all-time** (principal)
+ **luna asta** + **azi** (secundar). Necesita extinderea numaratorilor cu `sent_today`/`sent_month`.
- **D5** (Observatii = operatii service): in API-ul RAR, campul `obs` e DE FAPT denumirea operatiilor
din service. Deci `obs` = text liber cu operatiile efectuate; la import, daca fisierul nu are
coloana Observatii, **concatenam denumirea operatiei de service in `obs`**. `obs` ramane in
`payload_json` (camp din contractul RAR), fara coloana noua.
Decizii din /plan-ceo-review (2026-06-28, mod SELECTIVE EXPANSION):
- **D6** (sanatate mereu-vizibila): cardurile-contor inlocuiesc bara de status, DAR sanatatea
(worker viu? RAR accesibil? ultima autentificare) ramane intr-un **strip mereu-vizibil, colorat,
deasupra contoarelor** (verde "declaratiile curg" / rosu "blocat: worker oprit / RAR inaccesibil").
Invariant: zero-silent-failures — semnalul critic NU se ingroapa sub volum. (Rafineaza D1.)
- **D7** (operatie -> obs, fara regresie de mapare): la import, denumirea operatiei RAMANE in
`op_service` (sursa pentru maparea op->cod) SI se COPIAZA in `obs`. `obs` e sink aditional, nu
mutare; fluxul needs_mapping ramane neatins. (Rafineaza D5.)
- **D8** (idempotenta obs): `obs` e EXCLUS din cheia de idempotenta (`idempotency.py:98`). Deci
editarea `obs` NU schimba cheia si NU poate crea duplicate — corecteaza AC-ul gresit din US-005.
`prestatii` ESTE in cheie (sortat dupa cod) — multi-select re-cheieaza randul (US-006).
- **D9** (secventiere): 5.15 INAINTE de 5.14 (mapare LLM). Editorul manual defineste forma listei
`prestatii` si UX-ul de confirmare; 5.14 umple codurile peste aceeasi forma.
- **D10** (extinderi acceptate, SELECTIVE EXPANSION): toate 4 intra in scope — (a) salvare mapare
din chip (US-009), (b) bulk-fix din lista (US-010), (c) require dinamic odometruInitial la chip
R-ODO/I-ODO (US-007), (d) editare keyboard-first in form slim (US-007).
Fapte verificate care fundamenteaza scope-ul (nu presupuneri):
- `vin` la RAR e **un singur camp** (17 car., MAJUSCULE, fara O/I/Q) — cerinta "fara 2 campuri
VIN" e deja respectata azi (`_form_editare.html` are un singur `vin`); ramane sa NU regresam.
- `prestatii` e deja **lista** in modelul intern (`mapping.resolve_prestatii(prestatii: list[dict])`)
si in contractul RAR — multi-select nu cere model nou, ci editor nou.
- `obs` exista deja ca alias de coloana la import (`import_router.py:71` — Observatii/Obs/Mentiuni/Note)
si ca text liber optional in contractul RAR (`obs`); azi NU e editabil in formular.
## 2. Non-Goals (anti scope-creep)
- Fara modificari pe backend-ul de trimitere: worker, masina de stari, idempotenta-logica
(`build_key`), reconciliere, contract RAR. Recalcularea idempotentei la editare foloseste
mecanismul EXISTENT (ca la 3.5/5.10), nu unul nou.
- Fara migrare de schema decat daca strict necesar. `obs` si `prestatii` traiesc in
`submissions.payload_json` (de confirmat la US-005) — fara coloane noi daca payload-ul le poarta.
- Fara stergerea functionalitatii listei de trimiteri: filtre (data/vehicul/stare), paginare,
bulk-delete pe randuri blocate, click->detaliu raman; se schimba DOAR aspectul randului (slim).
- Fara schimbarea regulilor de mapare operatie->cod sau a validarii nomenclatorului RAR
(`mapping.py`, `validation.py` raman ca atare; doar callsite-urile de editare le folosesc cu lista).
- Fara redesign al landing-ului (deja livrat in 5.x); aici doar IMPORTAM stilul lui in app.
## 3. Stories atomice
> Backend + UI pentru acelasi comportament = 2 stories. `base.html` e fisier FIERBINTE
> (serializat intre valuri — un singur autor pe val). Toate UI verificate pe un esantion de teme (o tema luminoasa + una intunecata).
### US-001: Teme aditive (light/dark/petrol + grafit/cobalt/cupru/hartie) + tokeni `--card2`/`--line2`
**Ca** operator de service **vreau** aceleasi teme ca pe landing **pentru ca** aplicatia sa para
acelasi produs, coerent vizual.
- **Depinde de**: —
- **Fisiere**: `app/web/templates/base.html`, `DESIGN.md`, `tests/test_tema.py` (~3 fisiere)
- **Test intai (RED)**: `tests/test_tema.py``test_cele_4_teme_definite`, `test_tokeni_card2_line2_in_toate_temele`, `test_anti_fouc_4_stari`, `test_migrare_localStorage_legacy`
- **Acceptance criteria**:
- [ ] Pastram temele EXISTENTE light/dark/petrol si ADAUGAM 4 teme noi grafit/cobalt/cupru/hartie,
definite prin token-urile EXISTENTE (`--bg/--card/--ink/--muted/--line/--ok/--warn/--err/--accent`)
+ DOUA noi `--card2` (fundal input/contor) si `--line2` (separator subtire). `--card2`/`--line2`
primesc valori si in light/dark/petrol (fallback rezonabil). Maparea landing->app pentru cele 4
noi: `--text->--ink`, `--sub->--muted`, `--okt->--ok`, `--errt->--err`, `--infot->--accent`.
- [ ] Selectorul ciclic parcurge TOATE: light -> dark -> petrol -> grafit -> cobalt -> cupru ->
hartie -> Auto, afiseaza eticheta temei curente, persistenta `localStorage` (D2).
- [ ] "Auto" pastrat: urmeaza `prefers-color-scheme`, rezolva la dark/grafit sau light/hartie
(decizie minora: Auto -> dark + hartie pentru light, sau dark/grafit — aliniaza cu I2).
- [ ] Script anti-FOUC in `<head>` seteaza `data-theme` sincron pre-paint pentru toate starile;
valoare necunoscuta -> Auto, fara blink. Valorile vechi raman valide (nu se mapeaza fortat).
- [ ] Contrast AA pentru text principal in toate temele (light + hartie sunt cele luminoase).
- [ ] `DESIGN.md` actualizat: sectiunea cromatica + selector tema reflecta toate temele.
- **Verificare E2E**: browser pe `/` (dashboard logat) — ciclare prin toate temele, persistenta la
refresh, fara FOUC; toate temele selectabile.
### US-002: Componente de design slim in `base.html` (CSS, fara consumatori inca)
**Ca** dezvoltator **vreau** clase reutilizabile pentru carduri-contor, lista slim, campuri slim si
chips **pentru ca** dashboard-ul si formularul sa le consume DRY, identic cu mockup-ul.
- **Depinde de**: US-001 (foloseste `--card2`/`--line2`)
- **Fisiere**: `app/web/templates/base.html`, `DESIGN.md`, `tests/test_web_responsive.py` (~3 fisiere)
- **Test intai (RED)**: `tests/test_web_responsive.py``test_clasa_contor_card`, `test_clasa_lista_slim`, `test_clasa_camp_slim`, `test_clasa_chips`
- **Acceptance criteria**:
- [ ] `.contor-card` (sau nume aliniat conventiei): cifra mare bold + eticheta mica muted, fundal
`--card2`, bordura `--line`, radius 8px, padding 10-12px; variante de culoare a cifrei prin
`.s-*` existente (verde/accent/rosu).
- [ ] `.lista-trimiteri-slim` cu rand `.trimitere-slim`: stanga = VIN mono (linia 1) + operatie·ora
muted (linia 2, 11px); dreapta = pill de stare; separator `--line2`; padding 10-14px.
Randul ramane clickabil (rol button) si pastreaza tinta 44px pe mobil.
- [ ] Varianta slim de camp formular: label 11px muted deasupra, input ~30px inaltime, fundal
`--card2`, mono pentru VIN/odometru/nr; integrata in macro-ul `camp` din `_macros.html`
printr-un flag (`slim=True`), fara a rupe randarea actuala (default neschimbat).
- [ ] `.chips` + `.chip` (cu buton `×` de stergere) pentru prestatii multi-select; accesibil
(buton real cu `aria-label`), stilat ca in mockup (accent 18%, font 10-11px).
- [ ] Zero regresie vizuala pe componentele existente (`.card/.pill/.act/.tabel-trimiteri`).
- **Verificare E2E**: pagina de proba/sandbox sau direct in US-003/004/007; vizual pe un esantion de teme + 390/1280.
### US-003: Dashboard Acasa — carduri-contor inlocuiesc bara de status
**Ca** operator **vreau** cele 3 carduri-contor compacte (Trimise / In coada / De corectat)
**pentru ca** sa vad starea dintr-o privire, ca in mockup.
- **Depinde de**: US-002
- **Fisiere**: `app/web/templates/_status.html`, `app/web/templates/_acasa.html`,
`app/web/routes.py` (`_status_counts` extins cu `sent_today`/`sent_month`), `tests/test_web_status.py`,
`tests/test_web_dashboard.py` (~5 fisiere)
- **Test intai (RED)**: `tests/test_web_status.py``test_strip_sanatate_mereu_vizibil`, `test_strip_rosu_worker_oprit`, `test_trei_contoare_card`, `test_trimise_all_time_luna_azi`, `test_fara_bara_veche`
- **Acceptance criteria**:
- [ ] **Strip de sanatate mereu-vizibil, DEASUPRA contoarelor** (D6): o linie compacta colorata —
verde "declaratiile curg" cand worker viu + RAR ok; **rosu** + text explicit cand worker
oprit SAU RAR inaccesibil ("Blocat: worker oprit" / "Blocat: RAR inaccesibil"), cu ultima
autentificare RAR. Glife accesibile ✓/✗ (nu doar culoare). Invariant zero-silent-failures:
semnalul "declaratiile NU pleaca" e imposibil de ratat, NU ingropat sub volum.
- [ ] Sub strip: card "Trimiteri RAR AUTOPASS" cu 3 contoare slim: **In coada** (queued, accent),
**Trimise** (sent, verde), **De corectat** (blocate = needs_data + needs_mapping + error, rosu).
- [ ] Cardul **Trimise** afiseaza trei valori temporale (D4): all-time (cifra principala) + "luna asta"
+ "azi" (sub-linie secundara). `_status_counts` extins cu `sent_today`/`sent_month`.
**Sursa de timp**: NU exista coloana `sent_at`; folosim `status='sent' AND date(updated_at)=...`.
Justificare (verificat): un rand `sent` nu mai primeste scrieri ulterioare pana la purge-delete
la +90z (`purge_after` se seteaza in ACEEASI scriere care marcheaza `sent`), deci `updated_at`
== momentul trimiterii pentru randurile `sent` -> fara migrare de coloana (respecta Non-Goal).
Daca pe viitor apar scrieri post-`sent`, reevalueaza o coloana `sent_at` dedicata.
- [ ] Navigarea existenta (Trimiteri/Mapari + badge needs_mapping) se pastreaza. Click pe contorul
**De corectat** deep-link-eaza in lista filtrata pe blocate (`?status=` existent din 5.x),
nu intr-o pagina noua.
- [ ] Scoped pe cont; poll-ul existent (`/_fragments/status`) randeaza noul antet fara a pierde tab-ul.
- [ ] Responsive: cele 3 contoare pe un rand pe desktop, stivuite/2-pe-rand pe mobil, fara overflow.
- **Verificare E2E**: browser pe `/` — contoare corecte vs date din DB, sanatate worker mort/viu,
poll pastreaza starea.
### US-004: Lista de trimiteri — rand slim (VIN + operatie·ora + pill)
**Ca** operator **vreau** lista de trimiteri in stil slim ca in mockup **pentru ca** e mai compacta
si mai usor de scanat, pastrand filtrele si actiunile.
- **Depinde de**: US-002
- **Fisiere**: `app/web/templates/_submissions.html`, `app/web/templates/_coada.html` (filtre raman),
`tests/test_web_submissions.py`, `tests/test_web_responsive.py` (~4 fisiere)
- **Test intai (RED)**: `tests/test_web_submissions.py``test_rand_slim_vin_operatie_pill`, `test_filtre_paginare_pastrate`, `test_bulk_doar_blocate`, `test_click_deschide_detaliu`
- **Acceptance criteria**:
- [ ] Fiecare rand: stanga VIN mono scurt (`vin_scurt`) linia 1 + operatie + ora/data muted linia 2;
dreapta pill de stare (`stare_css`/`stare_scurt`). Nr. inmatriculare, data completa si nr.
prezentare RAR raman accesibile (linie meta discreta si/sau in modalul de detaliu).
- [ ] Filtre (data/vehicul/stare — `_coada.html`), paginarea numerotata si bulk-delete pe randuri
blocate (checkbox doar pe `gestionabil`) raman FUNCTIONALE.
- [ ] Click pe rand deschide `/_fragments/trimitere/{id}` in modal (neschimbat).
- [ ] Slim layout consistent desktop si <=1024px (cardurile responsive existente nu regreseaza).
- [ ] Pill-urile de stare folosesc maparea din `labels.py` (zero etichete noi).
- **Verificare E2E**: browser — filtrare + paginare + click detaliu + bulk pe blocate, pe 4 teme,
pe 390/820/1280.
### US-005: Backend — `obs` (Observatii) editabil si persistat
**Ca** operator **vreau** sa editez Observatiile (operatiile de service in text liber) **pentru ca**
sa corectez/completez ce s-a facut, separat de codurile RAR.
- **Depinde de**: —
- **Fisiere**: `app/web/routes.py` (`/trimitere/{id}/corecteaza`),
`app/api/v1/import_router.py` (`/_import/{id}/rand/{row}/editeaza`, `EDIT_FIELDS`),
`app/validation.py` (obs optional), `app/payload_view.py` (echo obs),
`tests/test_web_corectie*.py`, `tests/test_import_review.py` (~6 fisiere)
- **Test intai (RED)**: `tests/test_web_corectie_obs.py``test_obs_editabil_persistat_corecteaza`, `test_obs_persistat_preview_editeaza`, `test_obs_optional_gol_ok`, `test_import_concateneaza_operatie_in_obs`
- **Acceptance criteria**:
- [ ] `obs` traieste in `payload_json` (camp `obs` din contractul RAR); fara coloana noua / migrare (D5).
- [ ] `obs` adaugat in `EDIT_FIELDS`; `corecteaza` si `editeaza` (preview) accepta si persista `obs`.
- [ ] `obs` optional (text liber, fara validare de continut, doar trim); apare in `payload_view`.
- [ ] `obs` se include in payload-ul trimis la RAR (camp `obs`). **`obs` e EXCLUS din cheia de
idempotenta** (`idempotency.py:98`) — deci editarea DOAR a `obs` NU schimba cheia si NU poate
crea duplicat (D8). NU recalcula/forta cheia pe baza `obs`. (Corecteaza formularea anterioara.)
- [ ] **La import** (D7): denumirea operatiei RAMANE in `op_service` (sursa pentru maparea op->cod);
daca fisierul NU are coloana Observatii, denumirea operatiei se **COPIAZA** (nu se muta) si in
`obs`; daca are coloana Observatii, se pastreaza textul ei. Format de concatenare definit
(denumiri separate prin "; "). Fluxul needs_mapping ramane neatins.
- **Verificare E2E**: `POST /trimitere/{id}/corecteaza` cu `obs` -> persistat -> vizibil in detaliu;
optional proba live RAR ca `obs` apare in FINALIZATA.
### US-006: Backend — prestatii multi-cod (lista) la editare/corectie
**Ca** operator **vreau** sa adaug/sterg mai multe coduri RAR pe o trimitere **pentru ca** o
comanda poate avea mai multe prestatii, asa cum accepta RAR.
- **Depinde de**: —
- **Fisiere**: `app/web/routes.py` (`/corecteaza`, `/repune`**rescrie logica single-`prestatii[0]`**
de azi: `cod_prestatie_curent` la `routes.py:977-982` + injectia la `1146-1164`/`1288-1324`
presupun UN cod; multi-select cere pre-fill din lista intreaga + scriere pe toti itemii),
`app/api/v1/import_router.py` (`/editeaza`, idem), `app/mapping.py` (NEATINS — deja accepta lista),
`app/validation.py` (fiecare cod in nomenclator), `tests/test_web_corectie*.py`,
`tests/test_mapping*.py` (~6 fisiere). Nota: `mapping.py` e neatins, dar call-site-urile din
handler-e cer un rewrite real (nu "fara schimbare de logica").
- **Test intai (RED)**: `tests/test_web_corectie_prestatii.py``test_mai_multe_coduri_acceptate`, `test_cod_invalid_respins`, `test_lista_goala_needs_mapping`, `test_idempotency_recalculat`, `test_odometru_initial_conditionat_R_ODO`
- **Acceptance criteria**:
- [ ] Handler-ele de editare accepta o LISTA de `cod_prestatie`, inlocuind selectul unic. **NU
reconstrui lista cu itemi goi**: handler-ele de azi injecteaza codul DOAR in `prestatii[0]`
(`routes.py:1146-1164`, `1288-1324`) — multi-select le rescrie ca: pastreaza itemii existenti
cu `cod_op_service`/`denumire` (invariant D7) si seteaza/adauga `cod_prestatie` pe ei.
`idPrezentare:null` se adauga in `payload.py` la construirea payload-ului, NU in itemul intern.
- [ ] **Pereche operatie<->cod definita**: cand exista operatii (`cod_op_service`), fiecare cod-chip
se ataseaza unei operatii (1 operatie -> 1 cod, ca azi, dar acum N operatii -> N coduri);
cand NU exista operatie (cod direct, ex. corectie pura), chip-urile sunt coduri libere intr-o
lista fara `op_service`. Aceasta pereche e ce consuma US-009 (salvare mapare op->cod).
- [ ] Fiecare cod e validat fata de nomenclator (`valid_codes`); cod necunoscut -> respins cu
mesaj (NU se trimite raw — invariant ORA-12899 din CLAUDE.md/contract).
- [ ] Lista goala de coduri -> ramane `needs_mapping` (nu se trimite fara cod).
- [ ] **Coduri duplicate** (acelasi cod adaugat de 2x) -> deduplicate inainte de persistare
(cheia sorteaza deja dupa identitate, dar lista persistata nu trebuie sa contina duplicate).
- [ ] Recalcul idempotenta dupa editare (mecanism existent), cu prinderea coliziunii ca azi.
- [ ] Se pastreaza regula `odometruInitial` obligatoriu cand lista contine `R-ODO`/`I-ODO`
(contract §payload) — validare existenta, doar verificata pe lista.
- **Verificare E2E**: `POST /corecteaza` cu 2 coduri valide -> `queued` cu `prestatii` de lungime 2;
cu un cod invalid -> respins; optional live RAR cu 2 prestatii -> FINALIZATA.
### US-007: UI — formular editare slim (VIN unic, Observatii, chips prestatii)
**Ca** operator **vreau** formularul de editare in design slim cu chips de prestatii **pentru ca**
e compact si imi arata clar codurile RAR si observatiile, ca in mockup.
- **Depinde de**: US-002, US-005, US-006
- **Fisiere**: `app/web/templates/_form_editare.html`, `app/web/templates/_macros.html`,
`app/web/templates/_trimitere_detaliu.html`, `app/web/templates/_editare_preview_modal.html`,
`tests/test_web_preview_edit.py`, `tests/test_web_detaliu*.py` (~6 fisiere)
- **Test intai (RED)**: `tests/test_web_form_editare_slim.py``test_un_singur_vin`, `test_camp_observatii_prezent`, `test_chips_multi_select_prestatii`, `test_adauga_sterge_chip`, `test_form_slim_in_ambele_modale`
- **Acceptance criteria**:
- [ ] Formularul foloseste varianta slim de camp (US-002): VIN, Data prestatiei, Nr. inmatriculare,
Observatii (textarea), prestatii (chips), Odometru — un SINGUR camp VIN (fara "Confirma VIN").
- [ ] Observatii = textarea liber, legat de `obs` (US-005).
- [ ] Prestatii = chips multi-select: fiecare cod ca chip cu `×`; un picker (dropdown din
nomenclator) adauga un cod nou; lista se trimite ca `cod_prestatie` multiplu (US-006).
- [ ] Acelasi `_form_editare.html` slujeste ambele modale (detaliu `/corecteaza` si preview
`/editeaza`), fara duplicare; degradare fara JS rezonabila (chips ca lista, picker = select).
- [ ] **Require dinamic odometruInitial** (D10c): cand lista de chips contine `R-ODO` sau `I-ODO`,
formularul DEZVALUIE si cere `odometru_initial` (contract §payload), previne 400 RAR si un
drum `needs_data`. Cand niciun chip R-ODO/I-ODO -> campul ramane optional/ascuns.
- [ ] **Editare keyboard-first** (D10d): in picker, Enter adauga chip-ul selectat; sageti
navigheaza optiunile; Esc inchide modalul; focus-ul revine logic dupa adaugare/stergere.
- [ ] Stilizare fidela mockup-ului pe toate temele; tinte 44px pe mobil; a11y (label-uri, aria,
anunt de chip adaugat/sters pentru screen-reader).
- [ ] **Suprafata JS reala** (nota de efort): chips add/remove client-side + picker navigabil cu
tastatura + management focus + reveal conditional odometruInitial = JS ne-trivial intr-un app
HTMX/minimal-JS. Fallback fara JS: picker = `<select>` simplu (server valideaza R-ODO->odo,
deci no-JS da `needs_data` corect, nu date gresite) — multi-select se degradeaza la un cod/submit.
- **Verificare E2E**: browser — editare trimitere needs_data: schimb VIN + scriu Observatii + adaug
2 coduri RAR (chips, cu tastatura) + adaug R-ODO (apare odometruInitial) + sterg un chip -> salvare
-> persistat; identic in preview import.
### US-008: Teste de regresie + E2E final pe cele 4 teme
**Ca** dezvoltator **vreau** acoperire si o trecere E2E completa **pentru ca** redesign-ul atinge
fisiere fierbinti (base.html) si nu vreau regresii pe teme/liste/formular.
- **Depinde de**: US-003, US-004, US-007
- **Fisiere**: `tests/test_web_responsive.py`, `tests/test_tema.py`, `tests/test_web_submissions.py`
(~3 fisiere)
- **Test intai (RED)**: completare scenarii lipsa (componente noi pe TOATE temele; slim list desktop+mobil)
- **Acceptance criteria**:
- [ ] `pytest -q -m "not live"` verde (fara regresii fata de baseline).
- [ ] **Test de tema robust, nu esantion**: un test parametrizat verifica fiecare token critic
(`--card2`, `--line2`, `--accent`, `--ok`, `--err`) e DEFINIT in TOATE cele 7+1 stari
(light/dark/petrol/grafit/cobalt/cupru/hartie/Auto). Ancorare pe SENTINEL CSS (nu felii
fixe `[idx:idx+N]`) — vezi regresia false-green din ROADMAP 5.13.
- [ ] E2E Playwright pe 390/820/1280, pe un dark (grafit) + un light (hartie) + petrol (verificare
ca temele vechi nu au regresat): strip sanatate, dashboard contoare, lista slim cu
filtre/paginare/bulk, formular slim cu chips, fara overflow orizontal.
### US-009: Salvare mapare din chip (compounding cu fluxul de mapare)
**Ca** operator **vreau** ca atunci cand adaug un cod RAR la o operatie sa-l pot salva ca regula
**pentru ca** data viitoare operatia sa se auto-rezolve, fara sa re-mapez manual.
- **Depinde de**: US-006, US-007
- **Fisiere**: `app/web/templates/_form_editare.html`, `app/web/routes.py` (reuse `save_mapping` +
`reresolve_account` — fara logica noua), `tests/test_web_mapare_din_chip.py` (~3 fisiere)
- **Test intai (RED)**: `tests/test_web_mapare_din_chip.py``test_salveaza_regula_din_chip`, `test_reresolve_deblocheaza_frate`, `test_optional_nu_forteaza`
- **Acceptance criteria**:
- [ ] Cand operatia (`op_service`) e cunoscuta si userul adauga un cod RAR prin chip, apare optiunea
"salveaza ca regula op->cod"; la confirmare reuse EXACT `save_mapping` + `reresolve_account`
(acelasi mecanism ca maparea inline din 5.7), scoped pe cont + CSRF.
- [ ] Re-rezolvarea deblocheaza si alte submission-uri `needs_mapping` cu aceeasi operatie (pe `batch_id`).
- [ ] Optional: daca userul nu vrea sa salveze, editarea ramane one-off (fara regula). Se compune
cu 5.14 (auto-maparea umple, salvarea din chip ramane fallback-ul uman).
- **Verificare E2E**: adaug cod la operatie nemapata + salveaza regula -> al doilea rand cu aceeasi
operatie se rezolva automat.
### US-010: Bulk-fix din lista (selectie multipla -> actiune unica)
**Ca** operator **vreau** sa corectez mai multe randuri blocate dintr-o data **pentru ca** la 2-20
de corectat/zi nu vreau sa intru in fiecare individual.
- **Depinde de**: US-004, US-006
- **Fisiere**: `app/web/templates/_submissions.html`, `app/web/routes.py` (reuse infra bulk
existenta din `_submissions` + `submissions_admin`), `tests/test_web_bulk_fix.py` (~3 fisiere)
- **Test intai (RED)**: `tests/test_web_bulk_fix.py``test_bulk_remapeaza_selectie`, `test_bulk_doar_blocate`, `test_bulk_scoped_cont`
- **Acceptance criteria**:
- [ ] Pe randurile blocate (checkbox existent pe `gestionabil`), o actiune bulk noua: aplica un cod
RAR / o remapare la toata selectia intr-o singura cerere (reuse forma `#bulk-trimiteri`).
- [ ] Scoped pe cont (404-before-409 ca la bulk-delete); doar randuri blocate eligibile.
- [ ] Fiecare rand re-validat + idempotenta recalculata individual (un cod invalid pe un rand nu
pica tot lotul — sumar "N reusite, M esuate" ca la salvarea mapcoloane D#12).
- **Verificare E2E**: selectez 3 randuri needs_mapping + aplic un cod -> toate 3 -> `queued`.
- **Verificare E2E**: rulare completa documentata in Raportul VERIFY.
## 4. Riscuri
- **base.html fisier fierbinte**: US-001/US-002 il ating amandoua + US-003/004/007 il citesc.
Serializeaza pe valuri (un singur autor pe val pe base.html), ca la 5.12/5.13.
- **Migrare teme legacy**: useri cu `localStorage.theme` = light/dark/petrol. Mitigare: maparea
grafioasa din US-001 (light->hartie, dark->grafit, petrol->grafit) + test dedicat.
- **Restyle lista = pierdere de functii**: filtre/paginare/bulk pot fi sparte de schimbarea de
markup. Mitigare: US-004 are AC explicite pentru pastrarea lor + teste lock.
- **Idempotenta la prestatii multiple**: schimbarea listei schimba cheia canonica. Mitigare:
refolosim mecanismul existent de recalcul + prindere coliziune (3.5/5.10), zero logica noua.
- **Densitate vizuala pe mobil**: randul slim cu 2 linii + pill poate aglomera. Mitigare: tinte
44px + verificare 390px in US-004/008.
- **Premisa mobil nevalidata** (din TODOS 5.13, CEO F1): valoarea slim/compact pe mobil presupune
utilizare reala pe mobil. Daca device-mix-ul e ~95% desktop, partea responsive e efort irosit.
Mitigare: nu blocheaza (designul e bun si pe desktop), dar confirma analytics inainte de a investi
in rafinari mobil viitoare.
- **7 teme = suprafata de test/intretinere** pe fisierul cel mai fierbinte: fiecare componenta noua
trebuie corecta in 7+1 stari. Istoricul (5.13) arata ca testele de tema au dat false-green o data.
Mitigare: US-008 cere test parametrizat ancorat pe SENTINEL (nu felii fixe); deduparea
grafit~dark / hartie~light ramana optiune de simplificare (reziduala, non-blocanta).
- **Secventiere cu 5.14** (D9): 5.15 defineste forma listei `prestatii`; daca 5.14 (mapare LLM)
porneste in paralel, sincronizeaza forma listei. Mitigare: 5.15 INAINTE de 5.14.
## 5. Intrebari deschise
> Toate intrebarile au fost REZOLVATE cu userul (vezi D1-D5 §1). Pastrate aici ca istoric al deciziei.
- **I1 — contor Trimise** [REZOLVAT]: arata all-time + luna asta + azi (D4). `_status_counts` extins.
- **I2 — teme** [REZOLVAT]: aditiv — light/dark/petrol + Auto + grafit/cobalt/cupru/hartie (D2).
- **I3 — stocare obs** [REZOLVAT]: in `payload_json`, fara coloana noua (D5).
- **I4 — operatii la import -> obs** [REZOLVAT]: concatenam denumirea operatiei in `obs` cand
fisierul nu are coloana Observatii (D5).
- Reziduale minore (de decis la executie, non-blocante): formatul exact de concatenare a denumirilor
in `obs`; rezolvarea "Auto" la light vs hartie; eventuala deduplicare grafit~dark / hartie~light
in eticheta selectorului.
## 6. Valuri de executie (graful de dependente)
```
Val 1: [US-001] base.html teme + tokeni (autor unic pe base.html)
Val 2: [US-002] base.html componente (dupa US-001, autor unic pe base.html)
Val 3: [US-003] [US-004] dashboard + strip sanatate + lista (consuma US-002; disjuncte) ||
[US-005] [US-006] backend obs + prestatii (fisiere backend; in paralel cu UI)
Val 4: [US-007] formular slim cu chips (dupa US-002+US-005+US-006)
Val 5: [US-009] [US-010] salvare mapare din chip || bulk-fix (dupa US-006/007 resp. US-004)
Val 6: [US-008] regresie + E2E final (dupa toate)
```
> Secventiere fata de alte PRD-uri (D9): **5.15 INAINTE de 5.14** (mapare LLM) — 5.15 fixeaza forma
> listei `prestatii` si UX-ul de confirmare; 5.14 umple codurile peste aceeasi forma.
---
## Raport VERIFY
> Completat de subagentul verificator (context curat) in faza VERIFY — vezi ROADMAP §5.6.
> PASS/FAIL per criteriu, cu dovezi (output pytest citat, E2E pe RAR test). Lipseste pana la VERIFY.
---
## GSTACK REVIEW REPORT
Review: `/plan-ceo-review` — 2026-06-28. Mod: **SELECTIVE EXPANSION**. Model: claude (opus).
Abordare aleasa de user: tot PRD-ul (8 stories) + 4 extinderi acceptate -> **10 stories**.
| Pasaj | Status | Constatari materiale |
|-------|--------|----------------------|
| Audit sistem | OK | base.html cel mai fierbinte fisier (31x/30z); 5.15 = a 5-a iteratie pe acelasi UI (smell recurent); 5.14 in flight pe acelasi seam |
| S1 Arhitectura | OK | Fara componente noi; fara migrare; rollback = revert template. Concentrare de risc pe base.html, nu coupling |
| S2 Eroare/Rescue | 2 GAP | (a) coduri duplicate in chips nedefinit -> US-006 dedupe; (b) cod necunoscut: invariant ORA-12899 pastrat |
| S4 Edge cases | 1 GAP HIGH | R-ODO/I-ODO cere odometruInitial; formularul nu il forta -> US-007 require dinamic (D10c) |
| S2/Idempotenta | 1 FIX | `obs` EXCLUS din cheie (`idempotency.py:98`) -> AC US-005 corectat (D8); `prestatii` in cheie -> re-cheiere OK |
| S6 Test | 1 GAP | 7 teme x componente pe fisier fierbinte; "esantion" prea lax -> US-008 test parametrizat ancorat pe SENTINEL |
| S8/S11 Trust | 1 HIGH | carduri-contor ascundeau sanatatea -> strip mereu-vizibil deasupra contoarelor (D6) |
| S9 Deploy | OK | Fara migrare; doar sent_today/sent_month (scoped). Rollback ieftin |
| S10 Trajectorie | 1 DECIZIE | secventiere 5.15 inainte de 5.14 (D9) |
| S11 Design/UX | OK + 4 EXT | strip trust; extinderi: salvare mapare din chip, bulk-fix, require dinamic odo, keyboard-first |
**Decizii incorporate (D6-D10):** strip sanatate mereu-vizibil (D6); operatie ramane in op_service +
copiata in obs (D7); obs exclus din idempotenta, AC corectat (D8); 5.15 inainte de 5.14 (D9); cele 4
extinderi acceptate (D10) -> US-007 imbogatit + US-009 (salvare mapare din chip) + US-010 (bulk-fix).
**Risc rezidual notat (non-blocant):** premisa "utilizare mobil reala" nevalidata (TODOS 5.13 F1);
7 teme = suprafata de test pe fisier fierbinte (deduparea grafit~dark/hartie~light ramana optiune).
**Spec-review loop (reviewer independent, context curat) — scor 7/10, verdict ISSUES -> remediat:**
- #1 HIGH (contradictie): US-006 spunea "reconstruieste prestatii ca itemi goi `{cod_prestatie}`",
ceea ce ar fi sters `cod_op_service`/`denumire` -> rupea D7 si US-009. **Remediat**: US-006 pastreaza
itemii existenti, seteaza doar `cod_prestatie`; pereche operatie<->cod definita; `idPrezentare` se
adauga in `payload.py`, nu in itemul intern.
- #2 MEDIUM: `sent_today`/`sent_month` nu aveau sursa de timp (nu exista `sent_at`). **Remediat**:
US-003 foloseste `status='sent' AND date(updated_at)` cu justificare (randul `sent` nu mai e scris
pana la purge la +90z) -> fara migrare.
- #3 MEDIUM: US-006 subestima rewrite-ul handler-elor (logica single-`prestatii[0]`). **Remediat**:
Fisierele US-006 numesc liniile exacte de rescris.
- #5/#6 LOW: suprafata JS reala (US-007) + tinta de click "De corectat" (US-003). **Remediat** (note adaugate).
- #4 LOW (scope): US-009/US-010 sunt adiacente FUNCTIONALE (din SELECTIVE EXPANSION), dincolo de
obiectivul pur de propagare design. **Acceptat constient** (alegerea userului); ramane optiunea de
a le scoate intr-un PRD separat daca propagarea design e ce e urgent.
**VERDICT:** APROBAT cu modificari incorporate (+remedieri spec-review). Scope: 10 stories / 6 valuri.
Fara CODEX/cross-model in aceasta rulare (review uni-model). Gata de executie dupa confirmarea userului.
NO UNRESOLVED DECISIONS