Files
rar-autopass/docs/prd/prd-5.15-propagare-design-dashboard-editare.md
Claude Agent 3fc53534e2 feat(5.15+5.14): CLOSE — fix-uri code-review + embeddings functional
5.15 (propagare design + dashboard editare) si 5.14 (mapare LLM distilata)
inchise dupa /code-review high. 8 buguri reparate TDD:

- HIGH modal nu se deschidea pe randul slim (base.html: trimitere-slim)
- HIGH /repune trunchia prestatii (declaratie incompleta la RAR) -> iterare
  peste existing, codes pozitional
- HIGH embeddings incarca model ~230MB degeaba pe corpus gol -> poarta has_corpus()
- HIGH picker chips gol pe re-render eroare -> conn/account_id pe toate ramurile
- MED obs re-derivat dupa stergere explicita -> _merge_override pastreaza obs=''
- MED mapare salvata fara denumire poluă GOLD -> _record_gold_validation guard
- MED typo nome_prestatie -> nume_prestatie in select /repune
- MED bucketare timp +3h gresita iarna -> SQLite localtime + TZ=Europe/Bucharest

Embeddings WIRE-uit functional (PRD #15, decizie user): ensure_embeddings_corpus
construieste corpus din nomenclator, gated pe AUTOPASS_EMBEDDINGS_ENABLED (default
off). Marime model corectata ~50MB->~230MB (estimare PRD gresita).

Cleanup: hoist load_* din bucla bulk-fix; import re la top.
Regresie: 1256 passed, 1 deselected (live), 0 failed.

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

612 lines
48 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**: inchis (2026-06-28; CLOSE dupa `/code-review high` -> 8 buguri reparate TDD; regresie 1256 passed, 1 deselected live; E2E browser real ramane OPEN — mediu sandbox fara Playwright)
> 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`.
> Mockup-uri piese fara design (REFERINTA VIZUALA OBLIGATORIE): `docs/mockups/prd-5.15-mockups.html`
> — strip sanatate D6 (stari rosu/verde), picker prestatii E4 (op<->cod), reveal odometru initial.
> Acopera exact piesele pe care mockup-urile landing nu le aratau si corecteaza contradictiile
> mockup<->PRD (VIN unic, contor all-time, culori prin tokeni).
> 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).
Decizii din /plan-eng-review (2026-06-28, model claude/opus; outside-voice = Claude subagent,
Codex a atins usage-limit). Fiecare confirmata cu userul:
- **E1** (ARCH, /repune nu mai sterge operatia): `/repune` face azi `p0.pop("cod_op_service")`
la `routes.py:1326` — sterge operatia cand se seteaza un cod direct, rupand D7 si US-009.
US-006 ELIMINA acel `pop` si pastreaza `cod_op_service`; test de regresie obligatoriu
(op_service supravietuieste unui /repune cu cod). (Rafineaza US-006.)
- **E2** (DRY teme, fisier fierbinte): config-ul de teme e duplicat in ~7 locuri in base.html
(anti-FOUC `VALID` la :22 + cinci literali paraleli `CYCLE`/`VALID`/`ICONS`/`LABELS`/`NEXT`
la :758-765). US-001 CONSOLIDEAZA intr-o singura structura sursa-de-adevar (`THEMES`
ordonata) din care se DERIVA ciclul/etichetele/iconitele + setul anti-FOUC. Adaugarea unei
teme = o intrare. (Rafineaza US-001.)
- **E3** (obs concat idempotent): la import, copierea denumirii operatiei in `obs` se face
DERIVE-ON-EMPTY (doar cand `obs` e gol) ca sa fie idempotenta la re-import/re-editare. Test
dedicat anti-dublu-concat ("Schimb ulei; Schimb ulei"). (Rafineaza US-005.)
- **E4** (binding operatie<->cod in chips — HIGH): chip-urile NU sunt o lista plata de coduri.
Cand exista operatii (`cod_op_service`), UI-ul randeaza UN picker PE operatie (eticheta op +
chip-ul ei de cod), pastrand perechea per-item pe care modelul o are deja; lista plata de
coduri libere DOAR pentru cazul fara operatie (corectie pura). Astfel US-009 citeste perechea
direct, iar deduparea e PER-ITEM (nu "dupa cod" — doua operatii distincte pot mapa legitim la
acelasi cod RAR). (Rafineaza US-006 AC2 + US-007 AC3 + US-009.)
- **E5** (serializare Val 3 pe routes.py): US-005 si US-006 rescriu ACEEASI functie
`post_corecteaza` (`routes.py:1120-1262`). Regula "un singur autor pe fisier fierbinte" se
EXTINDE la routes.py in Val 3: US-005 INAINTE de US-006 (secvential, nu paralel). (Rafineaza §6.)
- **E6** (US-007 HTMX server-driven PRIMARY): inversam abordarea — chips add/remove via `hx-post`
care re-randeaza partial-ul chips+form; reveal-ul conditional `odometruInitial` rezulta GRATIS
din re-randarea server; navigare tastatura = `<select>`/`<datalist>` nativ. JS custom DOAR ca
progressive enhancement (snappiness), nu calea principala. Elimina path-ul dublu JS/no-JS.
(Rafineaza US-007.)
- **E7** (contoare in timp local RO): `azi`/`luna asta` se bucketeaza in timp local RO (UTC+2/+3),
nu UTC — `updated_at` e `datetime('now')` UTC, deci `date(updated_at)` pur ar numara gresit
trimiterile dintre miezul noptii local si ~03:00. Folosim offset RO (ex. `date(updated_at,'+3 hours')`
cu aceeasi baza `now`) + test la granita de miez de noapte local. (Rafineaza US-003.)
- **E8** (interleave fix authz GET-listari — securitate): CLAUDE.md noteaza scurgere cross-cont
deschisa ("GET-urile de listare sunt globale + neprotejate"). Userul a ales sa INTERLEAVE
remedierea in 5.15 -> story noua **US-011** (account-scope pe GET-urile de listare + teste),
nu queue dupa polish-ul de teme.
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**:
- [x] 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`.
- [x] Selectorul ciclic parcurge TOATE: light -> dark -> petrol -> grafit -> cobalt -> cupru ->
hartie -> Auto, afiseaza eticheta temei curente, persistenta `localStorage` (D2).
- [x] **DRY (E2)**: config-ul de teme traieste intr-o SINGURA structura sursa-de-adevar
(`THEMES` ordonata, cu `{id,label,icon}`) din care se DERIVA `CYCLE`/`NEXT`/`ICONS`/`LABELS`
(azi 5 literali paraleli la base.html:758-765) SI setul anti-FOUC `VALID` (azi separat la
base.html:22). Adaugarea unei teme noi = o singura intrare; test ca derivatele acopera
toate temele (prinde o intrare ICONS/LABELS lipsa, nu doar token CSS lipsa).
- [x] "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).
- [x] 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).
- [x] Contrast AA pentru text principal in toate temele (light + hartie sunt cele luminoase).
- [x] `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**:
- [x] `.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).
- [x] `.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.
- [x] 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).
- [x] `.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).
- [x] **Doar tokeni, fara hex hardcodat (criteriu din mockup)**: toate culorile componentelor noi
(contor, lista slim, chips, strip, picker) folosesc EXCLUSIV variabile CSS
(`var(--errt)`/`var(--okt)`/`var(--accent)`/`var(--card2)`/`var(--line2)` etc.), NU hex literal
si NU inline-styles copiate ca-atare din `landing.html`. Cifra "De corectat" rosie = token
(`var(--errt)`), nu `#E05D5D` hardcodat, ca sa ramana AA pe temele luminoase (hartie/light).
Referinta: `docs/mockups/prd-5.15-mockups.html`.
- [x] 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**:
- [x] **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.
**Layout exact (din mockup)**: strip full-width sus; glifa (✗ rosu / ✓ verde) + text bold la
stanga, "Ultima autentificare RAR: ..." mono muted la dreapta. Copy: rosu "Blocat: worker oprit
— declaratiile NU pleaca" (sau "... RAR inaccesibil"), verde "Declaratiile curg normal".
Referinta: `docs/mockups/prd-5.15-mockups.html`.
- [x] 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).
- [x] **Stari goale + ierarhie contor (criteriu din mockup)**: cifra principala a contorului "Trimise"
e **all-time** (cifra mare bold), iar "luna asta"/"azi" sunt o sub-linie mono secundara
(`luna {n} · azi {n}`) — NU "luna asta" ca cifra principala (corecteaza framing-ul din mockup-ul
landing). Contorul "De corectat" la 0 se afiseaza **muted, nu rosu** (rosu doar cand exista
blocate — pastreaza pattern-ul `_status.html:47`). Referinta: `docs/mockups/prd-5.15-mockups.html`.
- [x] 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.
**Timezone (E7)**: `updated_at` e `datetime('now')` = UTC; bucketarea `azi`/`luna asta`
se face in TIMP LOCAL RO (ex. `date(updated_at,'+3 hours')`, aceeasi baza `now`), altfel
trimiterile dintre miezul noptii local si ~03:00 cad pe ziua precedenta si "luna asta" e
gresita in primele ore ale zilei de 1. Test la granita de miez de noapte local.
**Caveat reconcile (E6 outside-voice)**: pe reconciliere (raspuns pierdut) worker-ul
marcheaza `sent` cu `updated_at` = momentul reconcilierii, nu al inserarii RAR — pentru
randurile reconciliate (rare) `updated_at` poate diferi de momentul real al trimiterii.
- [x] 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.
- [x] Scoped pe cont; poll-ul existent (`/_fragments/status`) randeaza noul antet fara a pierde tab-ul.
- [x] 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**:
- [x] 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).
- [x] Filtre (data/vehicul/stare — `_coada.html`), paginarea numerotata si bulk-delete pe randuri
blocate (checkbox doar pe `gestionabil`) raman FUNCTIONALE.
- [x] Click pe rand deschide `/_fragments/trimitere/{id}` in modal (neschimbat).
- [x] Slim layout consistent desktop si <=1024px (cardurile responsive existente nu regreseaza).
- [x] Pill-urile de stare folosesc maparea din `labels.py` (zero etichete noi). Eticheta "Eroare VIN"
din mockup-ul landing e DOAR ilustrativa — se foloseste `stare_scurt` existent (ex. "De corectat").
- **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**:
- [x] `obs` traieste in `payload_json` (camp `obs` din contractul RAR); fara coloana noua / migrare (D5).
- [x] `obs` adaugat in `EDIT_FIELDS`; `corecteaza` si `editeaza` (preview) accepta si persista `obs`.
- [x] `obs` optional (text liber, fara validare de continut, doar trim); apare in `payload_view`.
- [x] `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.)
- [x] **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.
- [x] **Idempotent (E3)**: copierea operatiei in `obs` e DERIVE-ON-EMPTY (doar cand `obs` e gol)
ca re-importul/re-editarea sa NU dubleze textul ("Schimb ulei; Schimb ulei"). Test dedicat
anti-dublu-concat.
- [x] **Cuplaj preview-import**: `obs` se adauga in `EDIT_FIELDS` (`import_router.py:261`); `_merge_override`
il propaga (obs e free-text, cade pe ramura ne-canonicalizata — fara strip "0", doar trim).
- **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**:
- [x] 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.
**E1 (CRITIC)**: `/repune` face azi `p0.pop("cod_op_service", None)` la `routes.py:1326`
ELIMINA acel `pop`: cand se seteaza un cod direct, `cod_op_service`/`denumire` RAMAN pe item
(altfel rupe D7 si US-009). **Test de regresie obligatoriu** (IRON RULE): op_service
supravietuieste unui /repune cu cod.
- [x] **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).
- [x] 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).
- [x] Lista goala de coduri -> ramane `needs_mapping` (nu se trimite fara cod).
- [x] **Coduri duplicate** -> dedupare **PER-ITEM, nu "dupa cod"** (E4): doua operatii distincte
pot mapa legitim la acelasi cod RAR; deduparea naiva dupa cod ar sterge o operatie reala si
ar distruge contextul op->cod cerut de US-009. Dedup = acelasi (op, cod) de 2x, nu acelasi cod.
- [x] Recalcul idempotenta dupa editare (mecanism existent), cu prinderea coliziunii ca azi.
- [x] 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**:
- [x] 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").
- [x] Observatii = textarea liber, legat de `obs` (US-005).
- [x] Prestatii = chips multi-select. **Binding op<->cod (E4)**: cand exista operatii
(`cod_op_service`), UN picker PE operatie (eticheta op + chip-ul ei de cod), pastrand
perechea per-item; lista plata de coduri libere DOAR pentru cazul fara operatie (corectie
pura). Fiecare cod ca chip cu `×`; lista se trimite ca `cod_prestatie` multiplu (US-006).
- [x] Acelasi `_form_editare.html` slujeste ambele modale (detaliu `/corecteaza` si preview
`/editeaza`), fara duplicare; degradare fara JS rezonabila (chips ca lista, picker = select).
- [x] **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.
- [x] **Editare keyboard-first** (D10d): in picker, Enter adauga chip-ul selectat; sageti
navigheaza optiunile; Esc inchide modalul; focus-ul revine logic dupa adaugare/stergere.
- [x] Stilizare fidela mockup-ului pe toate temele; tinte 44px pe mobil; a11y (label-uri, aria,
anunt de chip adaugat/sters pentru screen-reader).
- [x] **HTMX server-driven PRIMARY (E6)**: chips add/remove via `hx-post` care re-randeaza
partial-ul chips+form; reveal-ul conditional `odometruInitial` rezulta GRATIS din re-randarea
server (server computeaza din lista de chips, fara ramura JS); navigare tastatura =
`<select>`/`<datalist>` nativ. JS custom DOAR ca progressive enhancement (snappiness), nu
calea principala. Elimina path-ul dublu JS/no-JS pe care formularea anterioara il cerea.
- [x] **Referinta vizuala (criteriu din mockup)**: `docs/mockups/prd-5.15-mockups.html` defineste
aspectul-tinta — VIN unic (FARA al doilea camp "Confirma VIN" din mockup-ul landing); Observatii
ca textarea slim; picker PE operatie cu DOUA stari vizuale: (a) operatie mapata = chip cod cu `×`
+ "+ alt cod" + link "salveaza regula op->cod" (US-009); (b) operatie ne-mapata = picker galben
"alege cod RAR" cu eticheta "lipsa cod". OdometruInitial: ascuns implicit (doar hint discret
"se cere doar pentru R-ODO/I-ODO") si DEZVALUIT cu bordura-stanga galbena + label "necesar pentru
R-ODO" cand lista de chips contine R-ODO/I-ODO.
- **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**:
- [x] `pytest -q -m "not live"` verde (fara regresii fata de baseline).
- [x] **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**:
- [x] 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.
- [x] Re-rezolvarea deblocheaza si alte submission-uri `needs_mapping` cu aceeasi operatie (pe `batch_id`).
- [x] 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**:
- [x] 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`).
- [x] Scoped pe cont (404-before-409 ca la bulk-delete); doar randuri blocate eligibile.
- [x] 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.
### US-011: Securitate — account-scope pe GET-urile de listare (interleave, E8)
**Ca** operator **vreau** ca listarile sa-mi arate DOAR trimiterile contului meu **pentru ca**
azi GET-urile de listare sunt globale + neprotejate (scurgere VIN/PII cross-cont, notata in CLAUDE.md).
- **Depinde de**: — (backend pur, independent de UI; ruleaza in paralel cu valurile de design)
- **Fisiere**: `app/web/routes.py` (GET-urile de listare trimiteri), `app/api/v1/router.py`
(GET-urile API de listare daca sunt globale), `app/auth.py` (refolosire scope existent),
`tests/test_web_scope.py`, `tests/test_api_scope.py` (~5 fisiere)
- **Test intai (RED)**: `test_get_listare_scoped_cont` — un cont NU vede randuri ale altui cont;
`test_get_listare_neautentificat_401`; `test_get_detaliu_scoped` (404-before-leak pe id strain).
- **Acceptance criteria**:
- [x] GET-urile de listare (trimiteri + orice listare globala) devin account-scoped, refolosind
mecanismul de scope existent (ca POST-urile + bulk-delete: 404-before-409 pe id strain).
- [x] Un cont nu poate enumera/citi VIN/PII al altui cont prin listare sau detaliu.
- [x] Enforcement aliniat cu `AUTOPASS_REQUIRE_API_KEY` (dev vs prod), fara a rupe contul id=1
implicit in dev.
- [x] Actualizeaza nota din CLAUDE.md ("GET-urile de listare ... de remediat") cand e inchis.
- **Verificare E2E**: doua conturi cu trimiteri; contul A nu vede niciun rand al contului B in
listare, filtre, paginare sau detaliu.
### US-012: Analytics device-mix (validare premisa mobil, in-PR)
**Ca** owner **vreau** sa stiu raportul desktop/mobil al operatorilor **pentru ca** sa decid daca
rafinarile mobil (390px) viitoare merita efortul (premisa nevalidata din TODOS 5.13/CEO-F1).
- **Depinde de**: — (instrumentare backend, independenta de UI)
- **Fisiere**: `app/web/routes.py` (sau middleware existent), `app/schema.sql` SAU `app_events`
(reuse tabela de evenimente existenta — fara coloana noua daca `app_events` poarta semnalul),
`tests/test_device_mix.py` (~3 fisiere)
- **Test intai (RED)**: `test_device_mix_inregistrat`, `test_device_mix_fara_pii`.
- **Acceptance criteria**:
- [x] La acces dashboard, clasifica grosier viewport/UA in desktop/mobil si inregistreaza in
`app_events` (semnal agregat, FARA PII suplimentar). Reuse tabela existenta — fara migrare
daca `app_events` poarta semnalul.
- [x] Un mod simplu de citire a raportului (query/admin), suficient pentru a decide investitia mobil.
- [x] Zero PII nou; aliniat retentiei `app_events` existente.
- **Verificare E2E**: acces dashboard de pe doua viewport-uri -> doua evenimente clasificate corect.
## 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 0: [US-011] authz GET-listari (backend pur; ruleaza in paralel cu orice val) ||
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 APOI prestatii — SECVENTIAL (E5): ambele rescriu
ACEEASI functie post_corecteaza (routes.py:1120-1262), autor unic
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)
— disjuncte la nivel de template (_form_editare vs _submissions)
Val 6: [US-008] regresie + E2E final (dupa toate)
```
> **Regula autor-unic extinsa (E5)**: pe langa base.html, `routes.py` are autor unic in Val 3:
> US-005 INAINTE de US-006 (ambele in `post_corecteaza`). US-009/US-010 in Val 5 sunt disjuncte
> la nivel de template; adauga rute noi separate in routes.py (regiuni diferite, mergeabile).
> 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
Verificator independent (context curat, subagent Sonnet) — 2026-06-28. **VERDICT: PASS** (12/12 stories),
cu 1 FAIL documentar remediat de lead + 1 OPEN limitat de mediu.
- **Suita completa**: `python3 -m pytest -q -m "not live"`**1230 passed, 1 deselected, 0 failed** (118s).
Baseline initial 992 → +238 teste, zero regresii.
- **AC per story (US-001..US-012)**: toate PASS cu dovezi (fisier:linie + test care le acopera).
Puncte verificate explicit: 7+1 teme cu `--card2`/`--line2` in toate (US-001, DRY `THEMES`);
componente slim doar cu tokeni, zero hex (US-002, ancorat pe `SENTINEL-COMPONENTE-SLIM`);
strip sanatate D6 + 3 contoare + `sent_today`/`sent_month` bucketate timp local RO `+3 hours` (US-003, E7);
lista slim cu filtre/paginare/bulk pastrate (US-004); `obs` editabil + EXCLUS din cheia idempotenta
(`idempotency.py:98`) + concat derive-on-empty anti-dublu (US-005, D8/E3); prestatii multi-cod via
`getlist` + **E1 IRON RULE** (`cod_op_service` supravietuieste `/repune` — test dedicat) + dedup per-item
(US-006, E4); form slim VIN unic + picker chips pe operatie + reveal odo server-driven + select vechi
redundant ELIMINAT (US-007/cleanup B); test tema parametrizat 5 tokeni x 7 teme ancorat pe selectori
`[data-theme]` (US-008, anti false-green); salvare mapare din chip reuse `save_mapping`+`reresolve_account`
(US-009); bulk-fix sumar "N reusite/M esuate" scoped (US-010); account-scope GET-listari 404-before-leak
(US-011); device-mix fara PII reuse `app_events` (US-012).
- **Fidelitate mockup** (`docs/mockups/prd-5.15-mockups.html`, cod-level): D6 strip, contoare D4,
picker E4 cu 2 stari (mapata=chip+×+salveaza / nemapata=select galben "lipsa cod"), reveal odo
border-left warn — toate conforme; toate culorile prin `var(--token)`, fara hex.
- **Regresia de aur**: testele `POST /v1/prezentari` + worker + import→commit raman verzi in suita;
E1 confirmat cu test. Live RAR real (`FINALIZATA`) = opt-in, indisponibil fara creds in sandbox (documentat).
**FAIL 1 (remediat de lead)**: nota CLAUDE.md "GET-urile de listare globale + neprotejate (de remediat)"
nu fusese actualizata (teammates instruiti sa NU atinga CLAUDE.md). **Remediat**: `CLAUDE.md:70` actualizat
sa reflecte scope-ul implementat de US-011.
**OPEN (mediu)**: E2E Playwright pe 390/820/1280 (grafit/hartie/petrol) — browserul MCP a returnat
"already in use" in sandbox (ca la livrabilele anterioare). Serverul porneste OK (`/healthz` ok),
ACs acoperite functional de pytest (`test_web_responsive.py`). Recomandat: rulat de operator cu browser real.
---
## 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.
### /plan-eng-review — 2026-06-28 (model claude/opus; outside-voice = Claude subagent, Codex usage-limit)
| Review | Trigger | Why | Runs | Status | Findings |
|--------|---------|-----|------|--------|----------|
| CEO Review | `/plan-ceo-review` | Scope & strategy | 1 | issues_open->remediat | 10 stories, 4 ext acceptate, spec-review remediat |
| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 1 | issues_open->remediat | 7 issues (2 HIGH), 1 regresie IRON-RULE, +2 stories noi |
| Outside Voice | Claude subagent | Independent 2nd opinion | 1 | issues_found | 10 findings; 2 HIGH absorbite, restul foldate |
**Step 0 scope:** acceptat ca-atare (10 stories). Gate de complexitate = breadth, nu depth (zero clase/servicii noi). User a confirmat pastrarea US-009/US-010.
**Constatari eng-review (toate confirmate cu userul si foldate in AC):**
- **E1 (ARCH, HIGH, conf 9/10)** `routes.py:1326` `/repune` face `p0.pop("cod_op_service")` — sterge operatia, rupe D7+US-009. US-006: elimina pop + test regresie (IRON RULE).
- **E2 (Code-quality, conf 9/10)** config teme duplicat ~7 locuri pe base.html (anti-FOUC + 5 literali). US-001: o singura structura `THEMES`, restul derivat.
- **E3 (Test, conf 7/10)** obs concat la import poate dubla textul la re-import. US-005: derive-on-empty + test anti-dublu.
- **E7 (Perf/Correctness, conf 8/10 — outside-voice)** `date(updated_at)` UTC numara gresit `azi`/`luna` peste granita local RO. US-003: bucketare timp local + test granita.
**Outside-voice (Claude subagent) — material absorbit:**
- **OV/E4 (HIGH, conf 9/10)** chip-uri lista plata fara binding op<->cod -> rupe US-009; dedup-dupa-cod sterge operatie legala. US-006/007: picker PE operatie cand exista op; flat doar fara op; dedup per-item.
- **OV/E5 (HIGH, conf 9/10)** Val 3 conflict same-function: US-005+US-006 rescriu `post_corecteaza`. §6: serializare US-005 -> US-006 pe routes.py.
- **OV/E6 (MED, conf 8/10)** US-007 supraestimeaza JS custom intr-un app HTMX. US-007: hx-post server-driven primary; reveal odo gratis; select/datalist nativ.
- **OV/E8 (MED securitate, conf 7/10)** GET-uri de listare globale neprotejate (scurgere VIN/PII cross-cont, CLAUDE.md). User a ales INTERLEAVE -> **US-011** (account-scope + teste).
- **OV minore foldate:** reconcile drift pe `updated_at` (caveat US-003); cost poll non-sargabil (notat, non-blocant); cuplaj `EDIT_FIELDS` pentru obs preview (US-005 AC).
**TODO (decizie user):** premisa mobil nevalidata -> user a ales BUILD-IN-PR -> **US-012** (analytics device-mix, fara PII, reuse `app_events`).
**Scope actualizat:** 10 -> **12 stories** (+US-011 authz, +US-012 analytics). Fara migrare de schema. Outside-voice a confirmat "no migration" TRUE.
**Failure modes — gap critic:** niciun gap critic ramas silent. Cel mai aproape: E1 (regresie tacuta op_service pe /repune) — acum acoperit de test obligatoriu. E7 (off-by-a-day tacut) — acum cu test de granita.
**VERDICT:** CEO + ENG CLEARED — gata de executie. 12 stories. Outside-voice absorbit (2 HIGH foldate). Fara migrare de schema.
NO UNRESOLVED DECISIONS