docs(plan): adauga plan-treapta2.md (planul Treapta 2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
474
docs/plans/plan-treapta2.md
Normal file
474
docs/plans/plan-treapta2.md
Normal file
@@ -0,0 +1,474 @@
|
||||
# Plan Treapta 2 — Import xlsx/csv + mapare coloane (canal non-ROA)
|
||||
|
||||
> **Plan executabil, post-review CEO (SELECTIVE EXPANSION).** Continuă `plan.md` (Treapta 1 = LIVE).
|
||||
> Acoperă funcționalitățile de integrare care AZI nu există: upload fișier, mapare coloane,
|
||||
> spectru de integrare. Motor identic cu Treapta 1 (mapare op→cod + coadă + worker + monitorizare).
|
||||
> Ultima actualizare: 2026-06-16. Review: `/plan-ceo-review` + voce externă (subagent independent).
|
||||
|
||||
## 1. Problema (de ce acum)
|
||||
|
||||
Treapta 1 servește clienții ROA prin ROAAUTO (API JSON). Dar obligația legală (L.142/2023)
|
||||
apasă pe **mii de service-uri non-ROA** care AZI introduc manual în interfața web AUTOPASS,
|
||||
prezentare cu prezentare (2-4 min × 60-100/lună = 3-6 ore/lună de tastare). Nu au ROAAUTO și nu
|
||||
vor scrie cod. Au deja datele în Excel/export din propriul soft.
|
||||
|
||||
**Rezultat țintă:** un service non-ROA, fără instalare, încarcă un fișier (xlsx/csv), mapează
|
||||
coloanele o singură dată (reținut), vede preview cu rândurile cu probleme flag-uite, confirmă
|
||||
explicit, apasă „Trimite la RAR" și prezentările apar `FINALIZATA`. A doua oară: drop fișier → trimite.
|
||||
|
||||
## 2. Ce există deja (reuse, NU se rescrie)
|
||||
|
||||
| Sub-problemă | Reuse din Treapta 1 | Atenție la review |
|
||||
|---|---|---|
|
||||
| Validare conținut (VIN/dată/odometru/nrInm) | `app/validation.py` | OK, se compune cu batch |
|
||||
| Mapare operație→codPrestatie + fuzzy | `app/mapping.py`, `operations_mapping` | ⚠️ `reresolve_account` e account-GLOBAL (vezi Risc R1) |
|
||||
| Coadă + idempotency | `submissions`, `app/idempotency.py` | ⚠️ cheia exclude obs/op-denumire (vezi 3.4) |
|
||||
| Worker login RAR + postPrezentare + retry | `app/worker`, `app/rar_client.py` | ⚠️ purjează creds (vezi 3.6, decizie D4) |
|
||||
| Reconciliere anti-duplicat | `app/reconcile.py` | OK |
|
||||
| Monitorizare + audit CSV | `/v1/prezentari`, `/v1/audit/export`, dashboard | OK |
|
||||
| Auth API-key per cont | `app/auth.py` | OK |
|
||||
| Criptare PII (Fernet) | `app/crypto.py` | refolosit pt. `import_rows` |
|
||||
|
||||
Nou = **doar stratul de INTRARE** (parsare + mapare coloane + preview) + **un fix de model creds pe web**.
|
||||
|
||||
## 3. Funcționalități noi (scope confirmat la review)
|
||||
|
||||
### 3.1 Upload fișier — `POST /v1/import`
|
||||
- `multipart/form-data`: `.xlsx`, `.xls`, `.csv`. Encoding: UTF-8 + **fallback `cp1250`/`latin2` (RO)** + BOM.
|
||||
- Parsare: `openpyxl` (xlsx) / `csv` stdlib. Limită (ex. 5 MB / ~5000 rânduri) → semnal explicit, nu trunchiere tăcută.
|
||||
- **openpyxl `read_only=True` streaming (Eng#6):** `load_workbook(read_only=True, data_only=True)`; verifică
|
||||
`max_row`/dimensiune ÎNAINTE de parsare → `FileTooLarge` fără parse parțial. Memorie marginală, rămâne sincron
|
||||
(acceptabil la volumul țintă, R2). `wb.close()` la final.
|
||||
- **CSV delimiter sniff (Eng#3):** export Excel RO folosește `;` (virgula = separator zecimal). `csv.Sniffer`
|
||||
pe `{; , \t}` sau probă explicită; alege delimiter-ul care dă >1 coloană consistent. 1 coloană → `HeaderError`
|
||||
clar, NU mapare oarbă.
|
||||
- **Coercion Excel (R3):** odometru numeric → `123456.0`; VIN/nr cu zerouri tăiate; date ca `datetime`. Normalizarea
|
||||
e centralizată în `idempotency.canonicalize_row` (vezi 3.4bis); coercion nerecuperabilă → starea `needs_review` (3.4).
|
||||
- **Dată dezambiguizată (Voce#2):** celulă `datetime` nativă din openpyxl → folosită DIRECT (neambiguă). Celulă
|
||||
STRING → aplică `date_fmt` mapat, DAR dacă `zi<=12` (deci și MM.DD ar fi valid) → forțează `needs_review`,
|
||||
nu trimite orb. Acoperă fișierul mixt datetime/string (cazul real RO).
|
||||
- **Robustețe export RO real (Voce#6/#7):** dacă workbook-ul are >1 sheet non-gol → cere alegerea sheet-ului
|
||||
(nu presupune `active`); rezolvă celulele header **îmbinate** (un-merge logic → nume reale sau flag, nu nume goale);
|
||||
taie rândurile trailing unde coloanele-cheie (VIN+dată) sunt goale (footer `TOTAL`/`Întocmit de:`); rând fără VIN
|
||||
= **skip structural**, nu `needs_data` fantomă.
|
||||
- Detectează header (primul rând non-gol), întoarce `{import_id, columns, sample_rows}`. NU trimite nimic la RAR.
|
||||
- Stochează în `import_batches` / `import_rows` (PII **criptat** cu `app/crypto.py`, `purge_after` ca `submissions`).
|
||||
|
||||
### 3.2 Mapare coloane (NOUĂ — stratul care lipsește azi)
|
||||
- **Schemă fișier → câmpuri canonice**: `vin`, `nr_inmatriculare`, `data_prestatie`, `odometru_final`,
|
||||
`odometru_initial?`, `operatie` (denumire/cod), `obs?`.
|
||||
- **Reținută per cont** (`column_mappings`), cu **semnătură de coloane**. Map once, reuse forever.
|
||||
- **Detectie drift (acceptat D3):** maparea reținută se aplică DOAR dacă semnătura coloanelor se potrivește
|
||||
exact. Coloane mutate/redenumite → NU aplica orb, cere re-confirmare. Previne maparea tăcută greșită la upload 2.
|
||||
- Auto-sugestie fuzzy pe nume coloană („VIN"/„Serie sasiu"→vin; „KM"→odometru_final).
|
||||
- **DRY (Eng#4):** refolosește `mapping.normalize_for_match` (NFKD+lowercase+strip) + `fuzz.token_sort_ratio`
|
||||
(rapidfuzz) — ACELAȘI primitiv ca editorul de operații. Map `{camp_canonic: [sinonime]}`, zero dependință nouă.
|
||||
- Format dată configurabil per mapare (`DD.MM.YYYY` RO vs ISO) → normalizat la `YYYY-MM-DD` (vezi dezambiguizarea în 3.1).
|
||||
|
||||
### 3.3 Mapare operații (reuse Treapta 1)
|
||||
- Eticheta operației din fișier → `codPrestatie` prin `operations_mapping` + fuzzy.
|
||||
- **Gate auto_send pe coduri noi (acceptat D3):** o operație nou-mapată sau cod neobișnuit NU se trimite
|
||||
automat → review manual o dată (un `FINALIZATA` eronat e permanent).
|
||||
|
||||
### 3.4 Preview + commit (gate HARD)
|
||||
- `GET /v1/import/{id}/preview`: fiecare rând cu stare derivată (rulează `validation.py` + `resolve_prestatii`
|
||||
FĂRĂ enqueue). **Cinci stări:**
|
||||
- `ok` — gata de trimis.
|
||||
- `needs_mapping` — operație fără cod.
|
||||
- `needs_data` — validare RAR eșuată / odometru lipsă.
|
||||
- `needs_review` (acceptat D6, R3) — coercion Excel suspectat (VIN numeric, odometru float). **Blochează
|
||||
auto-send chiar dacă validarea trece** — VIN stricat = `FINALIZATA` permanent greșit.
|
||||
- `already_sent` (acceptat D5) — cheia idempotency există deja. Preview arată „deja trimis pe `<data>` ca
|
||||
`idPrezentare X`". **Niciodată dedup tăcut într-un commit în masă** — decizie explicită per-rând.
|
||||
- **Lookup batch, nu N+1 (Eng#5):** calculează toate cheile, apoi `SELECT idempotency_key FROM submissions
|
||||
WHERE account_id=? AND idempotency_key IN (chunk)` (chunk-uri ~900 param SQLite). O(1) interogări, nu 5000.
|
||||
`load_mapping` o singură dată ca POST.
|
||||
- `duplicate_in_file` (Voce#3, NOU) — coliziune INTRA-batch. Grupare pe cheie în fișierul parsat:
|
||||
`|grup|>1` identice → „rândul 12 și 88 identice"; același `vin+dată+odometru` cu operație diferită →
|
||||
„rândul 12 și 41 diferă doar prin operație, confirmă". `already_sent` verifică doar batch-uri anterioare;
|
||||
aceasta prinde coliziunile din ACELAȘI fișier (altfel `UNIQUE` global le înghite/erează mid-batch).
|
||||
- **Gate HARD de confirmare (acceptat D3 + Voce#1):** rezumat dry-run (X gata, Y date lipsă, Z nemapate, W deja
|
||||
trimise) + confirmare explicită (tastezi numărul de prezentări). **Plus atestare pe VALORI, nu doar pe total:**
|
||||
preview-ul arată per-rând valorile FINALE rezolvate (VIN, dată ca `YYYY-MM-DD` cum o vede RAR, km); rândurile
|
||||
`needs_review` trebuie bifate explicit „verificat" ca să intre în N. `N` dovedește numărul; bifa dovedește
|
||||
conținutul. Oprește atât count-error cât și content-error (VIN coercionat / dată swap în rândul individual).
|
||||
- Commit = enqueue în `submissions` DOAR rândurile `ok` confirmate → worker → monitorizare standard.
|
||||
- **Log atestare (Voce#9):** la commit scrie `import_attestations` (batch_id, account_id, confirmed_by, ts,
|
||||
`rows_hash`=sha256 peste valorile rezolvate confirmate, n_confirmed). Apare în `/v1/audit/export`. UI sub bară:
|
||||
„Confirmând, TU ești declarantul acestor N prezentări la RAR (ireversibil)". Apărare legală + trasabilitate
|
||||
(L.142/2023 — operatorul e declarantul de rol).
|
||||
|
||||
### 3.4bis Cheie idempotency canonică partajată (Eng#2)
|
||||
- Coercion-ul Excel (`123456.0`) calculat ÎNAINTE de cheie poate da cheie diferită de POST live (`123456`) →
|
||||
`already_sent` ratează → al doilea `FINALIZATA`. Fix: extrage normalizarea canonică (odometru strip `.0`,
|
||||
VIN upper/strip, dată `YYYY-MM-DD`) într-un helper public `idempotency.canonicalize_row(raw) -> dict` +
|
||||
`build_key(account_id, canon)`. **Parser-ul de import ȘI `POST /v1/prezentari` apelează ACELAȘI helper** înainte
|
||||
de cheie ȘI de validare. O sursă de adevăr; cele două canale nu pot diverge.
|
||||
- **REGRESIE CRITICĂ:** cheia produsă de refactor trebuie să fie IDENTICĂ cu cea de azi pentru input-uri
|
||||
existente, altfel rândurile deja trimise capătă cheie nouă → re-trimise. Test de regresie obligatoriu.
|
||||
|
||||
### 3.5 Spectru de integrare (același backend)
|
||||
1. **API** (există) — ROAAUTO / soft propriu.
|
||||
2. **Upload manual în browser** (3.1-3.4) — service fără cod. **Acesta e scope-ul acestei trepte.**
|
||||
3. ~~Drop fișier (SFTP/email-to-import)~~ — **CUT din această treaptă** (vezi NOT in scope, decizie D6).
|
||||
|
||||
### 3.6 Acces web + creds RAR (model creds CORECTAT — decizie D4 + Eng#1/Voce#5)
|
||||
- Login web (email + parolă cont) → folosește/emite API-key existent.
|
||||
- Creds RAR introduse în UI, **criptate-at-rest per-cont** pe **coloana `accounts.rar_creds_enc`** (ALTER aditiv,
|
||||
exact ca migrarea existentă a aceleiași coloane pe `submissions`; NU tabel nou — Eng#1). O singură sursă per cont.
|
||||
- **Worker re-login (fallback):** `claim_one` rămâne; la login worker-ul face `creds = submission.creds_enc
|
||||
OR SELECT rar_creds_enc FROM accounts WHERE id=?`. Submission web fără creds → ia din `accounts` → login OK.
|
||||
- **De ce abatere de la zero-storage Treptei 1:** canalul web **nu are re-pusher** (ROAAUTO re-trimitea
|
||||
la creds lipsă; aici nu există). Worker-ul trebuie să poată re-login oricând, altfel o serie încărcată
|
||||
zile mai târziu, după un restart worker, rămâne blocată permanent → declarație legală netrimisă, tăcut.
|
||||
- **Gate purjare `worker:271` (Voce#5, P1):** purjarea existentă `UPDATE submissions SET rar_creds_enc=NULL
|
||||
WHERE account_id=?` e ACCOUNT-scoped → la primul login web ar șterge creds de pe TOATE submission-urile
|
||||
contului, inclusiv cele API-channel efemere. Conturile CU `accounts.rar_creds_enc` durabil: purjarea devine
|
||||
inofensivă (worker re-citește din `accounts`). Conturile FĂRĂ durabil (API-channel pur Treapta 1): purjarea
|
||||
rămâne neschimbată. **Test = coadă MIXTĂ API+web** (după login web, submission-urile API tot se trimit), nu doar web.
|
||||
- Compensare risc: creds tot criptate (Fernet), tot redactate din loguri; doar **persistate**, nu efemere.
|
||||
Blast-radius mai mare la scurgere cheie Fernet (creds durabile vs. doar in-flight) — acceptat conștient (D4).
|
||||
|
||||
## 4. Date noi (SQLite)
|
||||
- `column_mappings` (account_id, signature_coloane, json mapare, format_data, created_at).
|
||||
- `import_batches` (id, account_id, filename, status, total/ok/needs_*/already_sent/duplicate_in_file, created_at, purge_after).
|
||||
- `import_rows` (batch_id, row_index, raw_json **criptat**, resolved_status, error). Purjate cu batch-ul.
|
||||
- `accounts.rar_creds_enc` — **coloană durabilă per-cont** (ALTER aditiv, NU tabel nou) pentru canalul web (D4, Eng#1).
|
||||
- `import_attestations` (batch_id, account_id, confirmed_by, ts, rows_hash, n_confirmed) — log atestare legală (Voce#9).
|
||||
- `submissions += batch_id, row_index` (T7, P1) — scope pentru `reresolve_account` + trasabilitate export rânduri eșuate.
|
||||
**Închide R1** (bulk-send tăcut cross-batch). T7 e predecesor HARD al U3 (vezi Roadmap).
|
||||
|
||||
## 5. NOT in scope (amânat / tăiat, cu motiv)
|
||||
- **Drop-fișier SFTP / email-to-import** (era U7) — TĂIAT. Trei mecanisme de intrare înainte ca un singur
|
||||
service non-ROA să fi încărcat manual un fișier. Validează întâi upload-ul manual, apoi decide. (D6)
|
||||
- **Contor volum + prag freemium** (era U6) — DEFER. Metrici de preț înainte să existe useri; a contoriza o
|
||||
obligație legală e delicat. Contorul e trivial de adăugat post-validare. (D6)
|
||||
- **Wedge auto-drop (SFTP/email-to-import)** — DEFER, confirmat de user la eng review: „verific manual upload-ul
|
||||
întâi". Manual upload e wedge-ul; auto-drop se re-evaluează post-validare (tensiune Voce#8 vs CEO D6, rezolvată
|
||||
în favoarea manual).
|
||||
- **Mapare AI / conector MCP** — Treapta 3.
|
||||
- **Editare/anulare prezentări trimise** — `FINALIZATA` terminal, neschimbat.
|
||||
- **Billing complet (Stripe etc.)** — după validarea pragului.
|
||||
|
||||
## 6. Mașina de stări (rând de import → submission)
|
||||
|
||||
```
|
||||
┌─────────── POST /v1/import (parsare, staging, NU trimite) ──────────┐
|
||||
▼ │
|
||||
import_row.resolved_status: │
|
||||
ok ─────────────┐ │
|
||||
needs_mapping │ (preview: rezolva fara enqueue) │
|
||||
needs_data │ │
|
||||
needs_review ───┤ (coercion suspectat → blocheaza auto-send) │
|
||||
already_sent ───┘ (cheie idempotency exista → decizie per-rand) │
|
||||
│ │
|
||||
▼ GATE HARD confirmare (tastezi N prezentari) │
|
||||
commit: enqueue DOAR rinduri `ok` confirmate ──▶ submissions (queued) ──▶ worker (Treapta 1, neatins)
|
||||
login RAR → postPrezentare → FINALIZATA
|
||||
```
|
||||
|
||||
## 7. Error & Rescue Map (stratul nou)
|
||||
|
||||
```
|
||||
CODEPATH | CE POATE MERGE PROST | EXCEPTIE / STARE
|
||||
-------------------------------|-----------------------------------|----------------------
|
||||
POST /v1/import parse xlsx | fisier corupt / non-xlsx | BadZipFile/InvalidFile
|
||||
| encoding RO (cp1250) | UnicodeDecodeError
|
||||
| >5MB / >5000 randuri | FileTooLarge (custom)
|
||||
| header lipsa / coloane duplicate | HeaderError (custom)
|
||||
parse cell | VIN/odometru coercion Excel | → stare needs_review
|
||||
| data DD.MM.YYYY | → normalizare, altfel needs_data
|
||||
apply column_mapping | semnatura coloane != reținuta | → cere re-confirmare (drift)
|
||||
preview resolve | cheie idempotency exista | → stare already_sent
|
||||
commit | confirmare numar gresit | reject, nu enqueue
|
||||
| worker fara creds (restart) | → REZOLVAT D4 (creds durabile)
|
||||
|
||||
STARE / EXCEPTIE | RESCUED? | ACTIUNE | USER VEDE
|
||||
------------------------|----------|----------------------------------|---------------------------
|
||||
BadZipFile/InvalidFile | Y | 422, mesaj „fisier invalid" | „Fisier nerecunoscut (xlsx/csv)"
|
||||
UnicodeDecodeError | Y | retry cp1250/latin2, apoi 422 | „Encoding nesuportat"
|
||||
FileTooLarge | Y | 413, fara parsare partiala | „Max 5000 randuri / 5MB"
|
||||
HeaderError | Y | 422 + ce coloane s-au gasit | „Antet neclar, verifica fisierul"
|
||||
needs_review (coercion) | Y | blocheaza auto-send, cere uman | rind galben „verifica VIN/km"
|
||||
already_sent | Y | NU dedup tacut, decizie per-rind | „deja trimis pe <data> #X"
|
||||
drift semnatura coloane | Y | nu aplica orb, re-mapare | „coloanele difera, confirma maparea"
|
||||
worker fara creds | Y (D4) | re-login din creds durabile | nimic (transparent)
|
||||
```
|
||||
|
||||
## 8. Failure Modes Registry
|
||||
|
||||
```
|
||||
CODEPATH | FAILURE MODE | RESCUED? | TEST? | USER VEDE | LOGGED?
|
||||
--------------------------------|-------------------------------|----------|-------|------------------|--------
|
||||
upload parse | encoding/format RO | Y | Y | mesaj clar | DA
|
||||
cell coercion (VIN/odo Excel) | VIN stricat trece validarea | Y(D6) | Y | needs_review | DA
|
||||
column_mapping drift | mapare tacuta gresita upload2 | Y(D3) | Y | re-confirmare | DA
|
||||
commit in masa | trimite 100 randuri gresite | Y(D3) | Y | gate confirmare | DA
|
||||
re-export (idempotency) | duplicat / corectie inghitita | Y(D5) | Y | already_sent | DA
|
||||
worker restart, creds purjate | serie blocata permanent tacut | Y(D4) | Y | nimic (re-login) | DA
|
||||
mapare salvata → re-resolve | trimite tacut randuri cross- | Y(T7) | Y | gate confirmare | DA
|
||||
| batch / feed API live | | | (batch scoped) |
|
||||
data string zi<=12 (DD vs MM) | data gresita-dar-valida trece | Y(V#2) | Y | needs_review | DA
|
||||
duplicat in ACELASI fisier | UNIQUE global inghite/ereaza | Y(V#3) | Y | duplicate_in_file| DA
|
||||
multi-op same vin+data+odo | reconcile dropa rand netrimis | Y(V#4) | Y | confirma manual | DA
|
||||
creds durabile, login web | purjare account-scoped sterge | Y(V#5) | Y | nimic (fallback) | DA
|
||||
| creds API-channel efemere | | | |
|
||||
100 declaratii dintr-un N | raspundere fara atestare/rol | Y(V#9) | Y | UI declarant+log | DA
|
||||
export RO: sheet 2 / merged hdr | HeaderError pe fisier valid | Y(V#6) | Y | alege sheet/flag | DA
|
||||
export RO: footer TOTAL parsat | prestatie fantoma needs_data | Y(V#7) | Y | skip structural | DA
|
||||
```
|
||||
|
||||
**R1 ÎNCHIS (T7, P1):** `reresolve_account` (mapping.py:253) primește `batch_id` și se scope-ază la seria comitată;
|
||||
T7 e predecesor HARD al U3. Salvarea unei mapări nu mai poate auto-queue rânduri cross-batch / din feed API live.
|
||||
Niciun failure mode silent rămas neacoperit.
|
||||
|
||||
## 9. Riscuri / open questions
|
||||
- **R1 (ÎNCHIS la eng review):** mapare account-global → bulk-send tăcut cross-batch. Fix `batch_id` scoping
|
||||
promovat la P1-blocking (T7), predecesor HARD al U3. Nu mai e deschis.
|
||||
- **R2:** fișiere mari (mii rânduri) → upload sincron + `openpyxl read_only` streaming + cap hard înainte de
|
||||
parsare (Eng#6). Async amânat până apare nevoia reală.
|
||||
- **R4 (nou, blast-radius):** creds durabile-at-rest (D4) → la scurgerea cheii Fernet, toate parolele RAR sunt
|
||||
decriptabile (vs. doar in-flight azi). Acceptat conștient; mitigare = rotație cheie + redactare loguri (existent).
|
||||
- **R3:** coercion Excel nerecuperabilă la parsare → stare `needs_review` (acceptat D6).
|
||||
- Un cont = un agent RAR sau mai mulți (afectează maparea creds în UI) — open question moștenit din plan.md.
|
||||
- `b64Image` rămâne opțional, omis în upload v2.
|
||||
|
||||
## 10. Roadmap (reordonat — eng review: T7 înainte de U3)
|
||||
- [ ] **U1** — `import_batches`/`import_rows`/`column_mappings` + parser xlsx/csv (`POST /v1/import`), cu
|
||||
encoding RO + delimiter sniff + openpyxl read_only + dezambiguizare dată + robustețe sheet/merged/footer.
|
||||
PII criptat în staging. (T3 + Voce#2/#6/#7 + Eng#3/#6)
|
||||
- [ ] **U2** — creds RAR durabile per-cont pe web (`accounts.rar_creds_enc`, ALTER aditiv) + worker re-login
|
||||
fallback + gate purjare `worker:271` (fix D4 + Voce#5). **Mutat înainte** — dependență hard end-to-end.
|
||||
- [ ] **T7** — `batch_id`/`row_index` pe `submissions` + scope `reresolve_account` la seria comitată.
|
||||
**Predecesor HARD al U3** — închide R1 (bulk-send cross-batch) ÎNAINTE ca save-mapare să ajungă live.
|
||||
- [ ] **U3** — mapare coloane + reținere per cont + semnătură + detectie drift + auto-sugestie fuzzy (reuse
|
||||
`normalize_for_match`, Eng#4). Nu se livrează până T7 nu e merged.
|
||||
- [ ] **U4** — preview (6 stări: ok/needs_mapping/needs_data/needs_review/already_sent/duplicate_in_file) +
|
||||
lookup `already_sent` batch (Eng#5) + canonicalize partajat (Eng#2) + gate HARD confirmare cu atestare pe
|
||||
valori (Voce#1) + log atestare (Voce#9) + commit selectiv → coadă.
|
||||
- [ ] **U5** — UI web upload (Jinja2+HTMX în dashboard): drop → mapează → preview → confirmă → trimite.
|
||||
- [ ] **U6 (P2)** — export rânduri eșuate (CSV) pentru corecție + re-upload (acceptat D3; depinde de T7).
|
||||
- [ ] ~~contor freemium~~ — DEFER (D6). ~~drop-fișier SFTP~~ — CUT (D6, re-eval post-validare).
|
||||
|
||||
## 11. Diagrame
|
||||
|
||||
### Arhitectură (componente noi vs existente)
|
||||
```
|
||||
[Fisier xlsx/csv] GATEWAY (existent, neatins sub linie)
|
||||
│ upload ┌──────────────────────────────────────┐
|
||||
▼ │ app/validation.py app/mapping.py │
|
||||
POST /v1/import ──parse──▶ import_batches │ app/idempotency.py app/crypto.py │
|
||||
(NOU) (cp1250, coercion) │ │ app/reconcile.py │
|
||||
│ ▼ └──────────────┬───────────────────────┘
|
||||
│ import_rows (PII cript) │ commit (rinduri ok)
|
||||
▼ │ preview ▼
|
||||
column_mappings (NOU) ──semnatura──▶ │ resolve submissions+batch_id ──▶ WORKER (existent)
|
||||
(mapare retinuta+drift) │ (6 stari) queued (T7 scope) login RAR → postPrezentare
|
||||
▲ ▼ └─▶ FINALIZATA (permanent)
|
||||
accounts.rar_creds_enc (NOU, D4) ───creds durabile──▶ worker re-login fallback (fara re-pusher)
|
||||
```
|
||||
|
||||
### Data flow + shadow paths (upload → commit)
|
||||
```
|
||||
FISIER ──▶ PARSE ──▶ MAP COLOANE ──▶ RESOLVE ──▶ CONFIRM ──▶ ENQUEUE
|
||||
│ │ │ │ │ │
|
||||
▼ ▼ ▼ ▼ ▼ ▼
|
||||
[gol?] [coercion?] [drift?] [already_ [N gresit?] [dup key?
|
||||
[non-xlsx?][encoding?] [nemapat?] sent?] reject → already_sent]
|
||||
[>5MB?] [needs_ [auto_send [needs_ [creds? → D4]
|
||||
review] gate] data?]
|
||||
```
|
||||
|
||||
## 12. Implementation Tasks
|
||||
Sintetizate din findings. P1 blochează ship; P2 = aceeași treaptă; P3 = follow-up.
|
||||
|
||||
- [ ] **T1 (P1, human ~1zi / CC ~30min)** — schema — coloană `accounts.rar_creds_enc` durabilă + worker re-login fallback + **gate purjare `worker:271`**
|
||||
- Surfaced by: voce externă #1 (D4) + Voce#5 — `worker/__main__.py:271` purjază account-scoped, web n-are re-pusher
|
||||
- Files: `app/schema.sql` (ALTER aditiv), `app/db.py:_migrate`, `app/worker/__main__.py`, `app/crypto.py`
|
||||
- Verify: test — (a) serie web, worker restart, token expirat → re-login din `accounts` → trimite;
|
||||
(b) **coadă MIXTĂ** API(efemer)+web(durabil) → după login web, submission-urile API tot se trimit
|
||||
- [ ] **T2 (P1, human ~half zi / CC ~20min)** — preview — stare `already_sent` + lookup **batch IN(...)** (no N+1, no silent dedup)
|
||||
- Surfaced by: voce externă #2 (D5) + Eng#5 — `idempotency.py:23`; 5000 randuri = N+1 dacă per-rând
|
||||
- Files: `app/api/v1/` import preview, `app/idempotency.py`
|
||||
- Verify: test — (a) re-upload cu typo odometru corectat → already_sent, nu al doilea FINALIZATA; (b) 5000 randuri → ≤7 interogări
|
||||
- [ ] **T3 (P1, human ~half zi / CC ~20min)** — parse — coercion guard + stare `needs_review` (blochează auto-send) + **mesaj formule-None**
|
||||
- Surfaced by: voce externă #8 (D6, R3) + **Eng pass 2 Issue 3** — `openpyxl data_only=True` întoarce `None` pe celule cu formule necalculate (export soft RO ≠ Excel) → indistinct de gol → cad în `needs_data` cu mesaj generic confuz pe un fișier care arată plin
|
||||
- **Issue 3 (P2):** când o coloană obligatorie întoarce `None` pe o pondere mare de rânduri (euristică pe rata de None, fără `data_only=False`), emite mesaj țintit: „fișier cu formule fără valori salvate — deschide și re-salvează în Excel". Gate-ul `needs_data` prevenea deja trimiterea greșită; asta e doar claritate UX.
|
||||
- **Ordonare critică (Eng#2/§3.4bis):** `canonicalize_row` rulează ÎNAINTE de `validate_prezentare` — `_parse_int` (`validation.py:44`, `isdigit()`) respinge `"123456.0"`; coercion-ul trebuie să taie `.0` înainte ca validarea să-l vadă (altfel `needs_data` în loc de `needs_review`).
|
||||
- Files: parser import, preview resolve
|
||||
- Verify: test — (a) VIN `0123…` numeric din xlsx → needs_review, nu se trimite; (b) xlsx cu coloană de formule fără cache → mesaj specific, nu `needs_data` generic; (c) odometru `123456.0` → canonicalizat la `123456` înainte de validare
|
||||
- [ ] **T4 (P1, human ~half zi / CC ~20min)** — mapare — semnătură coloane + detectie drift
|
||||
- Surfaced by: review D3
|
||||
- Files: `column_mappings`, mapare coloane
|
||||
- Verify: test — upload 2 cu coloane mutate → cere re-confirmare, nu aplică orb
|
||||
- [ ] **T5 (P1, human ~half zi / CC ~25min)** — preview — gate HARD confirmare (tastezi N) + **atestare pe valori rezolvate** (Voce#1)
|
||||
- Surfaced by: review D3 + Voce#1 — N dovedește totalul, bifa dovedește conținutul (VIN/dată/km finale)
|
||||
- Files: UI preview, commit endpoint
|
||||
- Verify: test — (a) commit fără N corect → reject; (b) rând `needs_review` nebifate → exclus din N, nu se trimite
|
||||
- [ ] **T6 (P1, human ~half zi / CC ~15min)** — mapare — gate auto_send pe coduri nou-mapate (**NU e additiv — schimbă cod existent**)
|
||||
- Surfaced by: review D3 + plan.md P2 + **Eng pass 2 OV-1** — `auto_send` e SCRIS (`save_mapping`) și afișat (`_mapari.html:49`) dar CITIT de niciun codepath; `reresolve_account` și bucla POST resolve trec pe `queued` ignorând flag-ul → AZI codurile nou-mapate se auto-trimit deja (bug latent Treapta 1)
|
||||
- **OV-1 (P1):** T6 trebuie să MODIFICE `reresolve_account` ȘI resolve-ul POST/import să consulte `auto_send` (`auto_send=0` → stare ținută/`needs_review`), nu doar să adauge un gate nou.
|
||||
- Files: `app/mapping.py` (reresolve_account), `app/api/v1/router.py` (POST resolve), commit
|
||||
- Verify: test — (a) cod nou-mapat cu `auto_send=0` → nu auto-send, review manual; (b) **regresie:** mapare existentă cu `auto_send=1` tot se requeue ca azi
|
||||
- [ ] **T7 (P1, human ~1zi / CC ~30min)** — **R1 ÎNCHIS** — `batch_id`/`row_index` pe submissions + scope `reresolve_account` (**predecesor HARD al U3**)
|
||||
- Surfaced by: voce externă #3+#5 + Voce#10 — `mapping.py:253` account-global (PROMOVAT la P1-blocking la eng review)
|
||||
- Files: `app/schema.sql`, `app/db.py:_migrate`, `app/mapping.py`, commit
|
||||
- Verify: test — (a) salvare mapare în batch A NU trimite rânduri din batch B / feed API; (b) canal API (batch_id NULL) tot se re-rezolvă ca azi
|
||||
- [ ] **T8 (P2, human ~half zi / CC ~15min)** — export rânduri eșuate CSV (depinde de T7 pt. trasabilitate)
|
||||
- Surfaced by: review D3
|
||||
- Files: import export endpoint
|
||||
- Verify: descarci needs_data/needs_mapping ca CSV, corectezi, re-upload
|
||||
- [ ] **T9 (P1, human ~half zi / CC ~20min)** — idempotency — `canonicalize_row` + `build_key` partajat (parser + POST), DRY + **normalizare account_id**
|
||||
- Surfaced by: Eng#2 + **Eng pass 2 OV-2** — coercion înainte de cheie → divergență `already_sent`; `idempotency.py:23` hash-uiește `account_id` AS-PASSED (`None` pe canal API, `router.py:66`) dar rândurile se stochează sub `account_or_default`=1 → același rând logic capătă cheie diferită cross-canal → `already_sent` ratează → al doilea `FINALIZATA`
|
||||
- **OV-2 (P1):** `canonicalize_row`/`build_key` aplică `account_or_default` ÎNAINTE de hash (None și 1 colapsează la o cheie). Tensiune cu §3.4bis „cheie identică": rândurile vechi cheie-`None` trebuie reconciliate (recompute o-singură-dată SAU dual-lookup), documentat explicit.
|
||||
- Files: `app/idempotency.py`, parser import, `app/api/v1/router.py`
|
||||
- Verify: test — (a) **cross-canal:** cheie(API canal-None) == cheie(import canal-rezolvat) pt. același rând logic; (b) regresie: strategia de reconciliere a cheilor vechi acoperită de test (fără re-trimitere tăcută)
|
||||
- [ ] **T10 (P1, human ~half zi / CC ~20min)** — parse — dezambiguizare dată **la nivel de coloană** (datetime nativ direct; string ambiguu → needs_review)
|
||||
- Surfaced by: Voce#2 + **Eng pass 2 OV-8** — `validation.py:81` (`date.fromisoformat`) acceptă orice ISO valid în interval → un DD/MM swap valid-dar-greșit trece. `zi≤12` per-rând ratează coloana uniform MM.DD (rândurile `zi>12` par neambigue și trec ca `ok`)
|
||||
- **OV-8 (P3):** detectează formatul din ÎNTREAGA coloană — dacă ORICE rând are token poziția-1 `>12`, coloana e DD-first; aplică formatul la toate rândurile, nu doar flag per-rând `zi≤12`.
|
||||
- Files: parser import, preview resolve
|
||||
- Verify: test — (a) `03.04.2026` string ambiguu → needs_review; (b) celulă datetime nativă → folosită direct; (c) coloană uniform MM.DD cu rânduri `zi>12` → format detectat la nivel de coloană, nu trec orb ca `ok`
|
||||
- [ ] **T11 (P1, human ~half zi / CC ~20min)** — preview — detecție coliziuni intra-batch (DOAR la preview/commit, NU în worker)
|
||||
- Surfaced by: Voce#3+#4 + **Eng pass 2 OV-3** — `UNIQUE` global înghite dup intra-fișier; `reconcile.py` e op-blind PRIN DESIGN (recuperare răspuns pierdut, worker:184/217)
|
||||
- **OV-3 (P1):** detecția coliziunilor intra-fișier trăiește EXCLUSIV la preview/commit (`duplicate_in_file`). NU edita `reconcile.py` / `worker/__main__.py` — a face reconcile op-aware regresează T2 (recuperarea POST-ului pierdut pe timeout legitim). Intra-file dedup (preview-time) ≠ reconcile stare-RAR (worker-time): probleme diferite.
|
||||
- Files: preview resolve (NU reconcile.py, NU worker)
|
||||
- Verify: test — (a) 2 rânduri identice în fișier → `duplicate_in_file`; (b) batch cu vin+data+odo colidant → flag la preview, cere manual; (c) **regresie T2:** `match_finalizata` rămâne op-blind, recuperarea răspuns-pierdut neschimbată
|
||||
- [ ] **T12 (P2, human ~half zi / CC ~15min)** — commit — log `import_attestations` + UI „ești declarantul" + **commit per-rând ON CONFLICT (TOCTOU)**
|
||||
- Surfaced by: Voce#9 + **Eng pass 2 Issue 1** — `already_sent` la preview e un snapshot; gardianul real e indexul UNIQUE la INSERT, minute mai târziu. Un canal concurent (API live / al 2-lea import) poate insera cheia colidant în fereastra preview→commit → un INSERT multi-rând într-o tranzacție rollback-uiește TOT batch-ul (`router.py:100` e INSERT simplu, nu OR IGNORE) → utilizatorul a tastat N, confirmat, primește eroare opacă, iar `rows_hash`(N) nu mai corespunde cu ce s-a inserat.
|
||||
- **Issue 1 (P1):** commit inserează per-rând cu `INSERT … ON CONFLICT(idempotency_key) DO NOTHING`; rândurile care colidează se reclasifică `already_sent` în rezultatul commit-ului; `import_attestations.rows_hash` + `n_confirmed` acoperă DOAR rândurile efectiv puse în coadă (nu N inițial). Respectă principiul planului „niciodată dedup tăcut".
|
||||
- Files: `app/schema.sql`, commit endpoint, `app/api/v1/router.py` (audit export), UI preview
|
||||
- Verify: test — (a) commit → rând `import_attestations` cu rows_hash + n_confirmed; apare în `/v1/audit/export`; (b) **TOCTOU:** cheie inserată de canal concurent după preview → rând reclasificat `already_sent`, atestarea acoperă doar rândurile puse în coadă
|
||||
- [ ] **T13 (P2, human ~1zi / CC ~25min)** — parse — robustețe export RO (multi-sheet + merged header + trim footer)
|
||||
- Surfaced by: Voce#6+#7 — sheet 2 / celule îmbinate → HeaderError pe fișier valid; footer TOTAL → prestatie fantomă
|
||||
- Files: parser import
|
||||
- Verify: test — (a) workbook 2 sheets → cere alegerea; (b) header merged → nume reale; (c) footer fără VIN → skip, nu needs_data
|
||||
- [ ] **T14 (P2, human ~half zi / CC ~15min)** — perf — CSV delimiter sniff + openpyxl `read_only` streaming + cap înainte de parse
|
||||
- Surfaced by: Eng#3+#6 — `;` RO dă 1 coloană tăcut; DOM întreg = vârf memorie
|
||||
- Files: parser import
|
||||
- Verify: test — (a) CSV `;` → coloane corecte; 1 coloană → HeaderError; (b) >5000 rânduri → FileTooLarge fără parse parțial
|
||||
- [ ] **T15 (P2, human ~half zi / CC ~20min)** — test — **E2E integrare** import→commit→worker (RAR mock)
|
||||
- Surfaced by: Test review — mock-urile per-unit ascund cheia idempotency + re-login + batch scoping
|
||||
- Files: `tests/test_import_e2e.py`
|
||||
- Verify: upload fixture → mapează → preview → commit N → worker run_once(MockRar) → FINALIZATA; re-upload corectat → already_sent
|
||||
- [ ] **T16 (P1, human ~2h / CC ~20min)** — retenție — job purjare + `purge_after` SET la insert (ambele canale)
|
||||
- Surfaced by: **Eng pass 2 OV-5** — `purge_after` e exportat în audit dar SETAT de niciun INSERT și NICIUN job de purjare nu există (`grep purge_after` → doar SELECT). Planul presupunea paritate `submissions`/`import_rows` care nu există → PII criptat (Fernet) trăiește la nesfârșit. Decalaj GDPR/L.142.
|
||||
- Files: `app/worker/__main__.py` (tick purjare), commit/insert (`submissions` + `import_batches`/`import_rows`)
|
||||
- Verify: test — (a) insert → `purge_after` populat (sent+90z); (b) rând expirat → șters de tick-ul de purjare; (c) `import_rows` purjate cu batch-ul
|
||||
|
||||
### 12bis. Eng Review Pass 2 — sinteză (2026-06-16)
|
||||
A doua trecere `/plan-eng-review` pe planul deja CLEARED: 6 findings noi (Claude) + 5 din vocea externă (subagent — Codex la cuotă), TOATE acceptate cu opțiunea completă (Lake 11/11). Detalii foldate în taskuri:
|
||||
- **Issue 1 (P1, T12):** commit TOCTOU → per-rând `ON CONFLICT DO NOTHING`, atestare doar pe rândurile puse în coadă.
|
||||
- **Issue 2 (P2, T13/T14) = vocea externă OV-6 (consens cross-model):** `openpyxl read_only=True` nu vede celule îmbinate → parser în 2 treceri (read_only dim-check + body; normal-mode header+merged DUPĂ cap-check).
|
||||
- **Issue 3 (P2, T3):** `data_only=True` → `None` pe formule necalculate → mesaj specific (euristică rată-None).
|
||||
- **Issue 4 (P3, U1):** `openpyxl` lipsește din `requirements.txt` → adaugă PINNED (ex. `openpyxl==3.1.x`) explicit în U1.
|
||||
- **Issue 5 (P2, U1/U3):** teste explicite — (a) `import_rows.raw_json` criptat la rest (ciphertext pe disc, plaintext după decrypt); (b) fuzzy coloane refolosește `mapping.normalize_for_match` (fără normalizator duplicat).
|
||||
- **Issue 6 (P2, U1/U4):** scrieri bulk sub autocommit (`db.py:17` `isolation_level=None`) → `BEGIN IMMEDIATE`…`COMMIT` + `executemany` (model `claim_one`); 5000 fsync → 1.
|
||||
- **OV-1 (P1, T6):** `auto_send` coloană moartă (citită nicăieri) → T6 modifică `reresolve_account` + resolve POST, nu doar adaugă.
|
||||
- **OV-2 (P1, T9):** skew `account_id` la hash → normalizare `account_or_default` în `canonicalize_row` + test cross-canal.
|
||||
- **OV-3 (P1, T11):** intra-file dedup DOAR la preview/commit; NU atinge `reconcile.py`/worker (op-blind by design, T2).
|
||||
- **OV-5 (P2, T16):** job purjare + `purge_after` la insert (nou T16, mai sus).
|
||||
- **OV-8 (P3, T10):** dezambiguizare dată la nivel de coloană, nu per-rând `zi≤12`.
|
||||
- **NOTE U1:** parserul = 2 treceri (Issue 2); adaugă `openpyxl` pinned (Issue 4); test PII-at-rest (Issue 5a); scrieri staging în tranzacție explicită + `executemany` (Issue 6). **NOTE U3:** test reuse `normalize_for_match` (Issue 5b). **NOTE U4:** enqueue în tranzacție explicită (Issue 6).
|
||||
- **Constrângere asset offline (learning):** UI upload (U5) NU introduce assets din CDN — gateway rulează offline; refolosește htmx vendorizat local (`app/web/static/`).
|
||||
- **Ordine livrare actualizată:** U1 → U2/T1 → T7 → U3 → U4 → U5; **T16 (purjare) poate merge în paralel** (independent de T7).
|
||||
|
||||
## 13. Design spec UI (post `/plan-design-review`)
|
||||
|
||||
> Clasificare: **APP UI** (tool intern, data-dense). Extinde sistemul existent din
|
||||
> `app/web/templates/base.html` (`:root` tokens) — NU introduce limbaj nou. Refolosește:
|
||||
> `--ok/--warn/--err/--accent`, `.card`, pills `.s-*`, `.maprow`, `.tablewrap`, empty states.
|
||||
|
||||
### 13.1 Information architecture (Pass 1)
|
||||
```
|
||||
DASHBOARD (existent)
|
||||
├─ card UPLOAD (NOU, sus; primar/CTA cand coada e goala)
|
||||
│ „Incarca fisier (xlsx/csv)" → drop zone + buton
|
||||
├─ [dupa upload] → ecran/sectiune MAPARE
|
||||
│ 1. mapare COLOANE (.maprow: camp canonic ← dropdown coloane)
|
||||
│ 2. mapare OPERATII (editorul fuzzy existent)
|
||||
├─ [dupa mapare] → PREVIEW (tabel dominant, 5 stari)
|
||||
│ rezumat pills sus + filtru + bara confirmare jos
|
||||
└─ coada submissions (existent, neatins)
|
||||
```
|
||||
Ierarhie preview: 1) rezumat stari (ce e gata/cu probleme), 2) tabelul, 3) bara de trimitere.
|
||||
|
||||
### 13.2 Tabel stari interacțiune (Pass 2)
|
||||
```
|
||||
ECRAN | LOADING | EMPTY | ERROR | SUCCESS | PARTIAL
|
||||
---------------|--------------------|-----------------------|--------------------|--------------------|--------------------
|
||||
Upload | spinner „se incarca| drop zone + „trage | „fisier invalid | → trece la mapare | n/a
|
||||
| / se parseaza…" | fisierul aici" + CTA | (xlsx/csv)" rosu | |
|
||||
Mapare coloane | — | „nicio coloana | dropdown rosu pe | toate verzi → next | unele campuri
|
||||
| | detectata" | camp obligatoriu | | nemapate (galben)
|
||||
Preview | „se valideaza N | „0 randuri in fisier" | rand rosu + motiv | „N gata de trimis" | rezumat: X ok,
|
||||
| randuri…" | | per rand | verde | Y probleme
|
||||
Trimitere | bara progres N/M | n/a | rand → error in | „N trimise" flash | „M din N trimise,
|
||||
| | | coada | (.flash existent) | restul in coada"
|
||||
```
|
||||
Empty state upload = feature: warmth („Primul fisier? Trage-l aici.") + CTA + context („xlsx sau csv, max 5000 randuri").
|
||||
|
||||
### 13.3 User journey / arc emoțional (Pass 3)
|
||||
```
|
||||
PAS | USER FACE | USER SIMTE | UI SUSTINE
|
||||
----|--------------------|-------------------------|---------------------------------
|
||||
1 | incarca fisier | nesiguranta („trimite | mesaj clar „NU se trimite nimic
|
||||
| | acum la RAR?") | pana confirmi" sub drop zone
|
||||
2 | mapeaza coloane | efort prima data | auto-sugestie fuzzy pre-selectata;
|
||||
| | | a 2-a oara: „mapare retinuta aplicata"
|
||||
3 | vede preview | control / verificare | rezumat pills + problemele primele
|
||||
4 | confirma (tastezi N| frica de greseala | gate explicit; „needs_review" galben
|
||||
| | permanenta | blocheaza VIN suspect
|
||||
5 | vede „N trimise" | usurare / incredere | .flash verde + rand sent in coada
|
||||
```
|
||||
5s: „inteleg ca nu trimite nimic inca". 5min: „maparea s-a retinut". 5 luni: „drop + trimite, sub 1 min".
|
||||
|
||||
### 13.4 AI slop (Pass 4) — 8/10
|
||||
APP UI, refolosește sistemul calm existent. Fără card-mosaic decorativ, fără gradients, fără
|
||||
3-column grid, fără border-left colorat ornamental. Pills semantice = funcționale, nu decor. OK.
|
||||
|
||||
### 13.5 Design system (Pass 5)
|
||||
- Stări rând: refolosește `.s-queued/.s-sent/.s-error`; adaugă `.s-needs_review` (galben `--warn`),
|
||||
`.s-already_sent` (muted), **`.s-duplicate_in_file` (muted, D10)**. Pills numerice rezumat = aceleași culori.
|
||||
- **Semantica culorii (D10, post eng review):** amber `--warn` = „verifică valori" (DOAR needs_review);
|
||||
muted `--muted` = „informațional / decizie per-rând" (already_sent + duplicate_in_file); roșu `--err` = blocat;
|
||||
verde `--ok` = ok; albastru `--accent` = în coadă. `duplicate_in_file` diferențiat de already_sent prin TEXT
|
||||
explicit + referință încrucișată („dublă cu rândul 88"), nu doar culoare (daltonism — pill poartă cuvântul).
|
||||
- Mapare coloane = `.maprow` + `.mapcol.grow` + `select` (exact ca `_mapari.html`).
|
||||
- Drop zone: `.card` cu bordura `--line` dashed la hover; fără estetică nouă.
|
||||
- Bara confirmare: `.card` fix jos, buton `--accent` existent, `input[type=text]` pentru N.
|
||||
- **Checkbox atestare (D11):** rândurile `needs_review` au `.chk` existent **per-rând** („verificat") — trebuie
|
||||
bifate ca să intre în N (forțează privirea pe fiecare valoare rezolvată). `<label>` vizibil + focus tastatură.
|
||||
- **Banner declarant (D12):** variantă `.banner` cu `--warn` (avertisment, nu eroare roșie), plasat DIRECT
|
||||
deasupra input-ului N: „Confirmând, TU ești declarantul acestor N prezentări la RAR (ireversibil)". Inevitabil
|
||||
la momentul confirmării (Krug). Anunțat la screen-reader.
|
||||
|
||||
### 13.6 Responsive & a11y (Pass 6)
|
||||
- Mobil: `.maprow` deja se rupe (`flex-wrap`); tabel preview în `.tablewrap` (scroll în card, existent).
|
||||
Bara de confirmare devine sticky bottom, nu fixed-overlap.
|
||||
- Touch ≥44px (deja `.cardlink` min-height 36px → ridică la 44 pe butoanele de acțiune upload/confirm).
|
||||
- a11y: drop zone are și buton (nu doar drag — drag nu e accesibil la tastatură); dropdown-urile de
|
||||
mapare au `<label>` vizibil (nu placeholder-as-label); stările au și text, nu doar culoare
|
||||
(pill cu cuvânt, nu doar pastilă colorată — daltonism); contrast ≥4.5:1 (tokenii existenți trec).
|
||||
- Gate confirmare accesibil: input N cu label, eroare anunțată, focus pe el la deschidere.
|
||||
|
||||
### 13.7 Decizii de design (Pass 7, rezolvate)
|
||||
- Mapare coloane = listă `.maprow` cu dropdown + eșantion (D8-A). Nu wizard, nu dropdown-pe-antet.
|
||||
- Preview = rezumat pills + filtru pe stare + problemele primele + bară confirmare cu tastare N (D9-A).
|
||||
- **D10 (post eng review):** `.s-duplicate_in_file` = muted + text „dublă cu rândul N" (grupat cu already_sent;
|
||||
amberul rămâne doar „verifică valori"). Fără culoare nouă — disciplină de sistem.
|
||||
- **D11:** atestare = `.chk` per-rând pe `needs_review`, obligatoriu pentru includere în N (atestare pe valori).
|
||||
- **D12:** banner declarant = `.banner` `--warn` direct deasupra input-ului N (răspundere legală inevitabilă).
|
||||
## GSTACK REVIEW REPORT
|
||||
|
||||
| Review | Trigger | Why | Runs | Status | Findings |
|
||||
|--------|---------|-----|------|--------|----------|
|
||||
| CEO Review | `/plan-ceo-review` | Scope & strategy | 1 | issues_open | SELECTIVE EXPANSION: 6 propuneri, 5 acceptate, 2 deferate/taiate |
|
||||
| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 2 | clean | Pass 1: 12 findings foldate (R1 INCHIS, T7 P1). Pass 2: 11 findings noi (1 arh + 2 cod + 1 perf + 5 voce externa + 2 test), TOATE acceptate cu optiunea completa (Lake 11/11) |
|
||||
| Outside Voice | subagent Claude (Codex la cuota) | Independent 2nd opinion | 2 | issues_found | Pass 1: 10 findings. Pass 2: 5 noi (OV-1 auto_send mort, OV-2 skew account_id, OV-3 reconcile op-blind, OV-5 fara job purjare, OV-8 data col-level), TOATE absorbite |
|
||||
| Design Review | `/plan-design-review` | UI/UX gaps | 2 | clean | full 4→9 (9 decizii) + delta 5→9 pe 3 stari UI noi post-eng (D10/D11/D12) |
|
||||
|
||||
- **CROSS-MODEL:** Pass 2 — Issue 2 (Claude) == OV-6 (voce externa) au ajuns INDEPENDENT la aceeasi concluzie (`openpyxl read_only` nu vede merged cells → parser 2-treceri): consens, confidenta ridicata. Fara tensiune Claude-vs-voce-externa in pass 2. (Pass 1: vocea externa contrazisese design D9-A + CEO D6, rezolvate.)
|
||||
- **PASS 2 — corectitudine pre-existenta:** OV-1/OV-2/OV-3/OV-5 sunt locuri unde planul presupunea o schimbare aditiva dar codul EXISTENT contrazicea presupunerea: `auto_send` coloana moarta (citita nicaieri), skew `account_id` la hash (None vs rezolvat), `reconcile` op-blind by design (T11 nu trebuie sa-l atinga), `purge_after` setat de nimeni + zero job purjare (PII nelimitat). Toate foldate in T6/T9/T11/T16.
|
||||
- **VERDICT:** CEO + DESIGN + ENG (×2) CLEARED — gata de implementare. R1 INCHIS (T7 P1, predecesor HARD U3). Niciun critical gap silent ramas. Ordine livrare: U1 → U2/T1 → T7 → U3 → U4 → U5; T16 (purjare) in paralel.
|
||||
|
||||
NO UNRESOLVED DECISIONS
|
||||
Reference in New Issue
Block a user