# PRD 5.19 — Bifa "Auto": transmitere automata sau manuala din coada > Status: DRAFT (asteapta aprobare). Sursa de contract: `docs/api-rar-contract.md`. > Limba: romana, fara emoji. Stil: aditiv, nedistructiv pe backend-ul de trimitere. ## 1. Introducere Astazi transmiterea catre RAR e controlata de un singur comutator **global** (`AUTOPASS_WORKER_SEND_ENABLED`, env): cand e pornit, worker-ul ia ORICE rand `queued` al unui cont `active` si il trimite imediat. Nu exista un control **per-cont** care sa permita unui service sa-si tina prezentarile in coada pentru verificare umana inainte de a pleca la RAR. Cazul concret care motiveaza feature-ul: utilizatorul testeaza canalul API din ROAAUTO (Visual FoxPro) direct in **productie** (`autopass.romfast.ro`), pe contul lui de test. Vrea ca prezentarile sa **apara in coada si sa astepte**, nu sa plece automat la RAR, pana cand le verifica si apasa explicit "Trimite". Reper vizual: bifa **"Auto"** din dashboard-ul gomag-vending (`image.png`). ## 2. Obiective ### Obiectiv principal - Un comutator **"Auto" per-cont**, persistat pe contul service-ului: cand e bifat, prezentarile pleaca automat la RAR (comportament actual); cand e debifat, randurile **asteapta vizibil in coada** pana cand un operator le trimite manual. ### Obiective secundare - Trimitere manuala **per rand** ("Trimite") si in **bloc** ("Trimite toate (N)", analogul "Start Sync" din gomag). - La activarea Auto (OFF -> ON), randurile deja tinute sunt **eliberate automat** spre transmitere. - Vizibilitate: randurile tinute apar in coada cu o stare umana clara ("In asteptare (manual)"), separate de cele in curs de trimitere. ### Metrici de succes - Cu Auto OFF, un `POST /v1/prezentari` valid creeaza un rand care **NU** e trimis de worker (ramane vizibil in coada) pana la actiune umana. - Cu Auto ON, acelasi rand pleaca la RAR fara interventie (zero regresie fata de azi). - Bifa supravietuieste restartului (persistata in `accounts`), per-cont (un cont OFF nu afecteaza alt cont). ## 3. Design (decizii luate cu utilizatorul) | # | Decizie | Alegere | |---|---------|---------| | D1 | Default bifa "Auto" pe conturi (inclusiv noi) | **OFF** (manual) — sigur, nimic nu pleaca fara confirmare | | D2 | La OFF -> ON, randurile deja tinute | **Eliberate automat** spre transmitere | | D3 | Plasare in UI | **Bara de status** (langa contoare, ca in mockup gomag) | | D4 | Trimitere manuala | **Per rand + buton "Trimite toate (N)"** | | D5 | Persistenta | Bifa salvata **pe contul service-ului** (`accounts`) | ### Mecanica aleasa: flag `held` pe submission (NU stare noua) Randurile tinute raman in starea `queued` (sunt logic in coada, doar puse pe pauza), marcate cu o coloana booleana noua `submissions.held`. Motiv: evitam atingerea CHECK-ului de stari si a masinii de stari (`queued/sending/sent/needs_mapping/ needs_data/error`), a pill-urilor, filtrelor si contoarelor — schimbare strict **aditiva**. Eticheta umana "In asteptare (manual)" se deriva din `status='queued' AND held=1` in stratul de afisaj (`labels.py`). - **Comutatorul de cont** (`accounts.auto_send_enabled`) guverneaza DOAR: (a) valoarea implicita a lui `held` la ingestie; (b) eliberarea in bloc la OFF -> ON. - **Worker-ul** (`claim_one`) ia doar `status='queued' AND held=0`. Nu mai stie de comutatorul de cont — ramane simplu si robust. - **Trimiterea manuala** (per rand sau bulk) = `held: 1 -> 0`; worker-ul preia randul la urmatorul poll. Functioneaza chiar daca contul e pe Auto OFF (override uman per rand). Comutatorul global `AUTOPASS_WORKER_SEND_ENABLED` ramane **kill-switch master** (productia il porneste). Feature-ul nou se aseaza DEASUPRA lui: held tine randul indiferent de env. ## 4. User Stories ### US-001: Schema — comutator cont + flag held **Ca** dezvoltator **Vreau** coloanele de persistenta pentru bifa Auto si pentru randurile tinute **Pentru ca** starea sa supravietuiasca restartului si sa fie per-cont. **Acceptance Criteria:** - [ ] `accounts.auto_send_enabled INTEGER NOT NULL DEFAULT 0 CHECK (auto_send_enabled IN (0,1))` adaugat in `app/schema.sql` + migrare defensiva in `app/db.py::_migrate` (ALTER idempotent, ca la `email`/`tier`). - [ ] `submissions.held INTEGER NOT NULL DEFAULT 0 CHECK (held IN (0,1))` adaugat + migrare defensiva. Index partial `idx_submissions_held ON submissions(held) WHERE held=1`. - [ ] **Index in `_migrate`, nu doar `schema.sql` (Eng MEDIUM):** `CREATE TABLE IF NOT EXISTS` nu se declanseaza pe DB existent -> indexul partial trebuie creat si in `_migrate` (ca `idx_submissions_batch` la `db.py:155`), altfel un DB prod upgradat capata coloana (ALTER) dar NU si indexul. - [ ] Contul implicit id=1 (dev) ramane pe default (0) — fara tratament special. - [ ] Helperi in `app/accounts.py`: `get_auto_send(conn, account_id) -> bool` si `set_auto_send(conn, account_id, enabled: bool)` (idempotent, scoped pe cont). - [ ] `python3 -m pytest -q` ramane verde (migrare aditiva, fara regresie pe schema). ### US-002: Ingestie respecta comutatorul de cont **Ca** operator de service cu Auto OFF **Vreau** ca prezentarile noi sa intre in coada tinute (held=1) **Pentru ca** sa le verific inainte sa plece la RAR. **Acceptance Criteria:** - [ ] La INSERT-ul `status='queued'` pe canalul API (`app/api/v1/router.py`, ~l.282), `held` = `0 daca accounts.auto_send_enabled=1 altfel 1` (snapshot la ingestie). - [ ] Acelasi snapshot la commit-ul de import (`app/api/v1/import_router.py`, ~l.1193). - [ ] La reresolve (un `needs_mapping` rezolvat trece pe `queued`, `app/mapping.py` ~l.895), `held` se seteaza tot din comutatorul contului. - [ ] `held` NU intra in `payload_json`, NU in `build_key`/idempotenta, NU in payload-ul RAR — e pur control de coada (ca `reviewed` la import). - [ ] **DRY + acoperire COMPLETA (review CEO + Eng Finding A — HIGH):** calculul `held` e UN SINGUR helper `held_for_account(conn, account_id) -> int`, chokepoint pt. TOATE situri `SET status='queued'`, nu doar 3. Codebase-ul are ~8 scriitori de `queued`: `router.py:282` (enqueue), `import_router.py:1190` (commit), `mapping.py:895` (reresolve), **`router.py:237` (reactivare error->queued la re-POST — BUG real de bypass: randul pastra `held` VECHI -> se auto-trimite desi Auto OFF)**, si rutele web de operator `routes.py` mapeaza-inline / corecteaza / repune / bulk-fix. - [ ] **Politica rute operator:** pentru tranzitiile declansate de operator in panoul de detaliu (corecteaza/repune/mapeaza/bulk-fix), `held=0` (actiunea operatorului = intentie explicita de trimitere) — DAR e o DECIZIE documentata, nu o omisiune, si respecta UX-ul de confirmare cand contul e OFF. Canalele de ingestie (API/import/reresolve/reactivare) = `held_for_account`. - [ ] `requeue_with_backoff` (worker `:154`) NU atinge `held` (tranzitie interna worker). - [ ] **Echo pe dedup (Eng MEDIUM):** ramura de dedup (`router.py:264`, re-POST pe rand existent) intoarce si ea `held` (azi ar da un "queued" curat fals — vezi US-009). - [ ] Test: cont Auto OFF -> `POST /v1/prezentari` valid -> rand `queued, held=1`; cont Auto ON -> `queued, held=0`. - [ ] Test reresolve: cont Auto OFF, submission `needs_mapping` -> mapare salvata -> rand devine `queued, held=1` (nu pleaca automat). ### US-003: Worker nu trimite randurile tinute **Ca** sistem **Vreau** ca worker-ul sa sara peste randurile held=1 **Pentru ca** transmiterea sa astepte decizia umana. **Acceptance Criteria:** - [ ] `claim_one` (`app/worker/__main__.py`) adauga `AND s.held = 0` la `WHERE`-ul de claim. - [ ] Test: rand `queued, held=1` cu cont `active` si send pornit -> `claim_one` intoarce `None` (nu il ia); acelasi rand cu `held=0` -> e luat (`sending`). - [ ] Recuperarea orfanilor / reconcilierea NU sunt afectate (held se aplica doar la claim din `queued`; un rand deja `sending` ramane gestionat normal). ### US-004: Bifa "Auto" in bara de status (toggle + persistenta + auto-release) **Ca** operator **Vreau** o bifa "Auto" in bara de status, salvata pe cont **Pentru ca** sa pornesc/opresc transmiterea automata dintr-un click. **Acceptance Criteria:** - [ ] Control checkbox HTMX cu eticheta vizibila **"Trimite automat la RAR"** (decizie user; NU "Auto" — eviti coliziunea cu "Trimitere automata" worker din `labels.py`) + helptext ("Debifat: prezentarile asteapta confirmarea ta"), in clusterul de header langa `.rar-chip` SAU pe rand propriu in bara de status (vezi D1). Reflecta `accounts.auto_send_enabled` al contului din sesiune. - [ ] `POST /auto-send` (ruta web, sub `require_login` + scope cont + CSRF) comuta bifa si o **persista** pe cont; raspuns OOB care re-randeaza bara de status. - [ ] La trecerea OFF -> ON: toate randurile `queued AND held=1` ale contului devin `held=0` (eliberare in bloc), scoped strict pe contul curent. Eliberarea e o SINGURA instructiune SQL atomica (`UPDATE ... WHERE account_id=? AND status='queued' AND held=1`), NU un loop (review CEO: atomicitate + evita contention cu worker-ul). - [ ] **Garda de confirmare (review CEO F4):** daca exista N>0 randuri tinute la activarea Auto, comutatorul cere o confirmare explicita cu numarul si destinatia ("Activarea Auto trimite imediat N prezentari catre RAR PRODUCTIE — FINALIZATA e ireversibila"). Fara confirmare, randurile tinute NU pleaca. Motiv: pe contul de test, un OFF->ON necugetat ar arunca toate prezentarile de proba in RAR real. - [ ] La trecerea ON -> OFF: randurile deja `queued held=0` NU sunt retrase (doar ingestiile NOI vor fi tinute); randurile in `sending`/`sent` neatinse. - [ ] Verify in browser: comuti bifa, se salveaza, ramane dupa refresh; cu OFF un rand nou apare tinut; comutand pe ON randurile tinute pleaca. ### US-005: Trimitere manuala — per rand + "Trimite toate (N)" **Ca** operator cu Auto OFF **Vreau** sa trimit un rand tinut sau toate odata **Pentru ca** sa eliberez selectiv sau in bloc spre RAR. **Acceptance Criteria:** - [ ] Buton "Trimite" pe fiecare rand `queued held=1` in lista de trimiteri/coada (`_submissions.html` / `_coada.html`), scoped + CSRF. - [ ] `POST /trimitere/{id}/trimite-acum`: 404-before-leak pe id strain; seteaza `held=0` DOAR daca randul e `queued held=1` (no-op sigur altfel); OOB refresh. - [ ] Buton bulk "Trimite toate (N)" (N = nr. randuri tinute ale contului) -> `POST /trimite-toate`: elibereaza toate `queued AND held=1` ale contului (held=0), cu confirmare tipata (count + "catre RAR PRODUCTIE", review CEO F5). Update atomic scoped pe cont (NU poate elibera randuri ale altui cont). - [ ] `POST /trimitere/{id}/trimite-acum` UPDATE include `AND status='queued'` ca un rand deja `sending` (luat de worker intre afisaj si click) sa fie no-op sigur (edge race). - [ ] Eliberarea seteaza doar `held=0`; worker-ul preia randul la urmatorul poll (trimitere asincrona, ca azi). Necesita worker pornit + send master ON + cont activ. - [ ] Butonul "Trimite toate (0)" e ascuns cand nu exista randuri tinute. - [ ] Test: rand tinut -> `trimite-acum` -> `held=0`; apoi `claim_one` il ia. ### US-006: Afisaj stare "In asteptare (manual)" **Ca** operator **Vreau** sa disting randurile tinute de cele in curs de trimitere **Pentru ca** sa stiu ce asteapta decizia mea. **Acceptance Criteria:** - [ ] `app/web/labels.py`: pentru `status='queued' AND held=1` -> eticheta umana "In asteptare (manual)" + clasa CSS de avertizare (ca `needs_*`); `held=0` ramane "In asteptare" (queued normal). - [ ] Bara de status arata un contor separat "In asteptare (manual): N" cand N > 0 (derivat din `queued AND held=1`); contorul `queued` total ramane corect. - [ ] Lista de trimiteri marcheaza randurile tinute (badge/pill), butonul "Trimite" apare doar pe ele. - [ ] Verify in browser: un rand tinut afiseaza eticheta corecta si butonul; dupa trimitere trece la "In curs de trimitere" -> "Trimisa". ### US-007: Vizibilitate coada tinuta imbatranita (mitigare OBLIGATORIE pt. default OFF) **Ca** operator / admin **Vreau** un semnal vizibil cand prezentari raman tinute prea mult **Pentru ca** default OFF (decizie user, pana devine stabil) lasa altfel prezentari nedeclarate tacit — exact esecul silentios pe care L.142/2023 il face risc legal. > Conditie: user a ales DELIBERAT default OFF "pana devine stabil" peste avertismentul de > conformitate (review CEO F1/F3). Aceasta US e atenuarea agreata si e BLOCANTA, nu optionala. **Acceptance Criteria:** - [ ] Bara de status: cand exista randuri `queued AND held=1` mai vechi de `N` zile (config `AUTOPASS_HELD_WARN_DAYS`, default 7), afiseaza un banner de avertizare ("M prezentari tinute de >N zile — declarare obligatorie L.142") cu deep-link la lista filtrata pe tinute. - [ ] `/metrics` expune un gauge `autopass_held_submissions` (total randuri tinute) si `autopass_held_oldest_age_seconds` (varsta celui mai vechi rand tinut), scoped global (observabilitate ops, review CEO F3). - [ ] Bannerul + gauge sunt derivate (zero stare noua); contorul varstei foloseste `created_at` al randului. - [ ] Test: rand tinut cu `created_at` vechi -> bannerul apare; gauge raporteaza varsta. ### US-008: Retentie randuri tinute (inchide gaura GDPR/L.142, review CEO F6) **Ca** sistem **Vreau** ca randurile tinute la nesfarsit sa aiba o politica de expirare **Pentru ca** un `queued held=1` nu e nici `sent` nici blocat -> azi NU primeste `purge_after` -> PII criptat (si `rar_creds_enc` efemer pe canalul API) ar sta vesnic. **Acceptance Criteria:** - [ ] Worker-ul expira randurile `queued AND held=1` mai vechi de `held_retention_days` (config, default 90, aliniat T16): le trece la `error` cu mesaj `TINUT_EXPIRAT` (terminal) + **seteaza `purge_after` DIRECT la momentul expirarii** (NU lasa `mark()` sa aplice `blocked_retention_days`=30). Eng MEDIUM: altfel viata reala = 90 (held) + 30 (error) = 120 zile, nu 90. Fie purge_after explicit la tranzitie, fie documenteaza 120. - [ ] La eliberarea manuala/auto a unui rand tinut, daca `rar_creds_enc` (canal API) e prea vechi, worker-ul cade pe `accounts.rar_creds_enc` (fallback re-login) ca azi — verificat ca creds efemere expirate nu blocheaza trimiterea. - [ ] Test: rand tinut vechi -> ciclul de purjare al worker-ului il expira + seteaza `purge_after`; PII devine purjabil. ### US-009: Fixturi teste + jurnal audit (review CEO F7 + observabilitate) **Ca** dezvoltator **Vreau** ca suita existenta sa nu se blocheze pe default OFF si ca actiunile sa fie auditate **Pentru ca** default OFF + `claim_one ... AND held=0` face ca lantul `POST -> claim -> sent` din testele existente (+ `test_live_rar`) sa stagneze tacit daca nu setam Auto ON. **Acceptance Criteria:** - [ ] `conftest`/factory de cont seteaza `auto_send_enabled=1` (sau `held=0`) pe conturile folosite de testele care exercita lantul de trimitere; `test_live_rar` seteaza explicit Auto ON. `pytest -q` ramane verde. - [ ] **Subtilitate id=1 (Eng HIGH/test):** contul implicit id=1 e creat de `schema.sql` (`INSERT OR IGNORE`), NU de `create_account` -> un fix care patcheaza doar factory-ul NU acopera contul folosit de majoritatea testelor (`test_import_e2e`, `test_creds_delivery`, `test_live_rar` ar stagna). Conftest face explicit `UPDATE accounts SET auto_send_enabled=1 WHERE id=1` (autouse). E un fix de STARE DB, nu env var (coloana e per-rand in `accounts`). - [ ] Audit `app_events`: comutarea Auto (`auto_send_schimbat` cu valoarea + cont) si eliberarile manuale/bulk (`held_eliberat` cu count) sunt jurnalizate (redactat, scoped). - [ ] Echo onest pe canalul API (aliniat invariant 5.7): raspunsul `POST /v1/prezentari` pentru un rand tinut indica starea reala (`held=true` / nota umana "tinut pentru verificare"), nu un fals "queued" curat. Dev-ul ROAAUTO vede ca randul NU a plecat. - [ ] Test: eveniment audit scris la toggle + la eliberare; raspuns API reflecta `held`. ### US-010: Onestitate + observabilitate pe canalul API (review DX Faza 3.5) **Ca** dezvoltator ROAAUTO/VFP care integreaza prin `POST /v1/prezentari` **Vreau** sa vad clar ca un rand e tinut si NU a plecat la RAR **Pentru ca** azi un rand tinut intoarce byte-identic cu unul gata de auto-send (`status:queued, erori:[]`) -> reintroduce exact bug-ul de succes-fals 5.7. **Acceptance Criteria:** - [ ] **Camp `held: bool = False` pe `SubmissionResult`** (`models.py`) + plumbing din `held_for_account` in `_rezultat_enqueue(..., held=...)` SI pe ramura de dedup (`router.py:264`). Cand `held and status=='queued'`, `motiv` devine NON-null (DX CRITICAL): mesaj uman "In asteptare — tinut pt verificare; NU trimis la RAR (Auto OFF)". - [ ] **`held` in proiectiile GET** (`_PREZENTARE_FIELDS` `router.py:398` + lista `cols` `router.py:369`): un dev care face `GET /v1/prezentari/{id}` vede `held=true`, nu un `queued` etern fara semnal (DX HIGH). - [ ] **Reutilizeaza vocabularul existent `AUTO_SEND_OPRIT`** (`errors.py:92`) pt. mesajul held — NU inventa al treilea vocabular "auto_send" (DX + R6). Mesaj 3-niveluri (problema/cauza/fix) pe `rar_error`/`motiv`. - [ ] **Documentatie hub `/integrare`** (`integrare_examples.py`/`_integrare.html`): tabel "De ce nu ajunge la RAR?" (held / needs_mapping / needs_data) + nota explicita "conturi noi pornesc cu Auto OFF, randurile asteapta eliberare manuala" + cum verifici/comuti (DX HIGH — altfel primul POST da 200/queued, dev-ul crede ca merge, nimic nu ajunge). - [ ] (Optional, paritate API) endpoint de eliberare API simetric cu `/repune` (`router.py:458`): `POST /v1/prezentari/{id}/trimite-acum`, scoped pe cont, 404-before, no-op daca nu `queued AND held=1` — ca integratorul API sa nu fie fortat in browser. - [ ] Test: held -> `held=true` + `motiv` non-null pe enqueue, dedup si GET (regresie ca `test_queued_fara_erori_nemapate`). ## 5. Cerinte functionale 1. [REQ-001] Comutatorul "Auto" e per-cont, persistat in `accounts.auto_send_enabled`, default 0 (OFF) inclusiv pentru conturi noi. 2. [REQ-002] Cu Auto OFF, orice ingestie care ar produce `queued` produce `queued held=1`. 3. [REQ-003] Worker-ul nu trimite niciodata un rand `held=1`. 4. [REQ-004] OFF -> ON elibereaza in bloc randurile tinute ale contului (atomic, scoped), DAR cu confirmare tipata cand N>0 (count + "RAR PRODUCTIE"); ON -> OFF nu retrage randuri deja eliberate. 5. [REQ-005] Operatorul poate elibera un rand tinut individual sau toate odata (bulk cu confirmare). 6. [REQ-006] `held` nu influenteaza payload-ul RAR, idempotenta sau validarea — pur coada. 7. [REQ-007] Toate rutele noi sunt scoped pe contul din sesiune, sub `require_login`, cu CSRF si 404-before-leak pe id strain. **`account_id` se deriva INTOTDEAUNA din sesiune, NICIODATA dintr-un camp de formular** (Eng security): altfel un operator pe contul A ar elibera in bloc randurile contului B postand `account_id=B`. Per-rand prin `_get_submission_scoped` (404 inainte de UPDATE). 8. [REQ-008] Randurile tinute imbatranite sunt VIZIBILE (banner + `/metrics`) si au politica de retentie/expirare (nu raman PII vesnic). Comutarea + eliberarile sunt auditate in `app_events`. ## 6. Non-Goals (ce NU facem) - **Fara interval/programare de sync** (dropdown "1 min" + buton "Start Sync" din gomag): worker-ul autopass e continuu, nu pe interval. "Trimite toate" e analogul lui "Start Sync". - **Fara stare noua de submission** (`held`/`tinut`): folosim flag boolean pe `queued`. - **Fara comutator per-operatie sau per-canal**: granularitatea e per-cont (decizie D5). (Nota: coloanele `auto_send` ramase pe `operations_mapping`/`operation_text_rules` sunt neutralizate din 5.11 si NU se reactiveaza aici.) - **Fara modificarea kill-switch-ului global** `AUTOPASS_WORKER_SEND_ENABLED`. - **Fara retragerea randurilor deja in `sending`/`sent`** (FINALIZATA e terminal la RAR). - **`held` NU e sandbox de testare** (avertisment de onestitate — tema cross-faza CEO F2 + DX4): eliberarea unui rand tinut declara REAL la RAR (FINALIZATA ireversibila). "Tinut" doar AMANA o trimitere reala. Ca sa testezi fara consecinte cu functia asta: tii randul si il STERGI (nu-l eliberezi). **Decizie user (poarta finala): 5.19 = doar tinut operational**; fara documentare `/valideaza` ca unealta de testare si fara rutare per-cont la RAR test (`rar_env`). Acestea raman posibile follow-up-uri (TODOS), neangajate in 5.19. ## 7. Consideratii tehnice ### Stack / fisiere atinse - Schema: `app/schema.sql` + `app/db.py::_migrate` (2 coloane aditive + 1 index). - Backend cont: `app/accounts.py` (get/set toggle). - Ingestie: `app/api/v1/router.py`, `app/api/v1/import_router.py`, `app/mapping.py` (reresolve) — set `held` din comutator. - Worker: `app/worker/__main__.py::claim_one` (+`AND s.held=0`). - Web: `app/web/routes.py` (rute `/auto-send`, `/trimite-toate`, `/trimitere/{id}/trimite-acum`), `app/web/labels.py`, template-uri `_status.html` / `_submissions.html` / `_coada.html`. ### Patterns de urmat - Migrare defensiva aditiva (model `accounts.email` / `accounts.tier` din 5.12/5.17). - Rute web scoped + CSRF + OOB HTMX (model `submissions_admin.py` / butoanele de lifecycle 5.6). - Strat de afisaj pur in `labels.py` (model 5.4) — fara logica de stare in template. ### Riscuri tehnice - **R1 (default OFF schimba comportamentul):** azi nu exista hold; cu default 0, conturile ar tine totul. Acceptabil — productia e pre-lansare, fara conturi legacy active (cf. 5.17), iar utilizatorul vrea explicit OFF pe contul de test. Documentat ca decizie constienta (D1). - **R2 (reresolve scapa snapshot-ul):** daca uitam `held` pe calea de reresolve (`mapping.py`), un rand deblocat din `needs_mapping` ar pleca automat desi contul e OFF. Acoperit explicit de US-002 AC. - **R3 (idempotenta):** `held` NU intra in cheie -> un re-`POST` al aceluiasi continut loveste randul existent (dedup), nu creeaza dublura. Confirmat de invariantul `build_key`. - **R4 (hazard de rollback — review CEO + Eng, HIGH operational):** daca se da revert pe cod DUPA ce randuri au `held=1`, worker-ul pierde filtrul `AND held=0` -> ar trimite TOATE randurile tinute la RAR (FINALIZATA ireversibila). Atenuare OBLIGATORIE: livreaza ODATA cu feature-ul un helper `tools/` care carantineaza randurile tinute (`UPDATE submissions SET status='error', rar_error='ROLLBACK_QUARANTINE' WHERE held=1`) + pas de runbook scris in §9 (copy-paste, nu improvizat sub presiune). - **R7 (eroziune creds efemere — Eng low-med):** la orice login reusit worker-ul NULL-eaza TOATE `submissions.rar_creds_enc` ale contului (`worker:382`), nu doar randul trimis. Un cont hibrid web+API cu keepalive-login poate sterge creds-urile efemere ale unui rand tinut API -> la eliberare se cade pe `accounts.rar_creds_enc` (fallback). Acoperit de US-008, dar triggerul e login-frate, nu varsta creds — de formulat corect. - **R5 (contention SQLite la bulk release):** `UPDATE` masiv pe "Trimite toate" concureaza cu `BEGIN IMMEDIATE` al worker-ului -> posibil `database is locked`. Update-ul atomic (o instructiune) + retry/backoff scurt; sau chunking daca N e mare. - **R6 (naming):** apare al TREILEA `auto_send` (cont `auto_send_enabled` vs `operations_mapping.auto_send` vs `operation_text_rules.auto_send`). Comentariu clar in `schema.sql` care le distinge, ca un viitor dezvoltator sa nu le confunde. ### Rafinari UI (review design Faza 2 — OBLIGATORII la implementare) - **D1 (container real):** RAR dot e in `base.html` (header `.rar-chip`), NU in `_status.html`. US-004 AC corectat: comutatorul Auto sta in clusterul de header langa `.rar-chip` (vizibilitate maxima, langa semnalul RAR real) SAU pe un rand propriu etichetat in bara de status — NU "langa dot" in `_status.html` (dot-ul nu e acolo). - **D2 (toggle non-optimist):** checkbox HTMX flip-uie vizual indiferent de raspuns. Necesita `hx-indicator` + revert-on-failure (la esec POST `/auto-send` -> bifa revine + toast eroare). Fara fals-sigur tacit pe un comutator de transmitere guvernamentala. - **D3 (poller nu inghite toggle-ul):** `#status-bar` are `hx-trigger="every 15s"` + `hx-swap="outerHTML"` -> ar inlocui comutatorul la fiecare 15s (pierdere focus tastatura + flicker). Exclude comutatorul din swap-ul periodic (container separat sau `hx-preserve`). - **D4 (modal de confirmare real):** confirmarea tipata (count + "RAR PRODUCTIE") NU se poate face cu `hx-confirm` (doar OK/Cancel nativ). Necesita un component modal (count, destinatie, type-to-confirm) — adaugat in lista de fisiere. Per-rand "Trimite" primeste si el o confirmare (1 linie + microcopy de ireversibilitate), nu doar bulk-ul. - **D5 (camp derivat, nu in template):** `held` NU e stare noua -> pill-ul existent ar randa "In coada" identic pt held si non-held. Calcul UN camp de afisaj derivat in `routes.py` (regula "display layer pur"), nu in template. Culoare `--warn` (amber), NU clasa `needs_*` (rosu/eroare) — held e asteptare benigna, nu eroare. - **D6 (mobil 390px):** per-rand actiune = afordanta dedicata pe `.trimitere-slim` cu `event.stopPropagation()` (randul e el insusi `role=button`), NU buton-copil nestat. Al 6-lea contor "In asteptare (manual)" se pliaza in celula "In coada" pe bara compacta (nu adauga a 6-a celula la 10px). Pill scurt ("Manual"/"Tinut") cu fraza completa in `title`. - **D7 (ordonare bannere):** `_status.html` poate avea deja 3 bannere (cont inactiv / trial / RAR jos) + al 4-lea (US-007 held). Regula de prioritate un-singur-banner ca sa nu impinga contoarele sub fold pe mobil. ### Dependente - Trimiterea manuala produce efect doar cu worker pornit + send master ON + cont `active` (mediul de productie real). In dev (send OFF) randul eliberat ramane `queued held=0`. ## 8. Open Questions - [ ] Trimiterea manuala se face asincron (flip `held=0`, worker preia la poll). Acceptam latenta de un poll (cateva secunde) sau vrem feedback "in curs" imediat in UI? (Propunere: asincron + OOB refresh, fara sincron — consistent cu arhitectura.) - [ ] Pe mobil, butonul "Trimite" per rand + "Trimite toate" incap in layout-ul compact (5.13)? (Propunere: "Trimite toate" in bara sticky, "Trimite" iconita pe card.) ## 9. Plan de verificare - Regresie `python3 -m pytest -q` verde (baseline curent ~1392) + teste noi per story. - E2E browser (Playwright, logat): comutare bifa persistenta dupa refresh; rand nou tinut cu Auto OFF; eliberare per rand si bulk; tranzitie OFF -> ON elibereaza in bloc. - Optional live RAR (`AUTOPASS_LIVE_RAR=1`): cont OFF -> rand tinut -> "Trimite" -> `sent idPrezentare=...` confirmat in finalizate. ## 10. Decizii /autoplan — audit trail Pipeline: CEO -> Design -> Eng -> DX, voce unica (Codex indisponibil pana 2026-07-18, plafon utilizare). Deciziile intermediare auto-decise pe 6 principii; portile umane = premise + taste. ### Poarta de premise (decizia ta) - **Scop:** AMBELE — control operational permanent + ajutor de testare. - **Default Auto:** OFF, pastrat "pana devine stabil" (ales constient peste avertismentul de conformitate L.142). Inverseaza recomandarea CEO F1 (default ON). Acceptat ca decizie de domeniu; declanseaza atenuari OBLIGATORII (US-007/008/009). ### Decizii auto (6 principii) | # | Faza | Decizie | Clasif. | Principiu | Motiv | |---|------|---------|---------|-----------|-------| | 1 | CEO | Approach A (held boolean) ca baza, nu stare noua (B) sau enum mod (C) | Mecanica | P5+P3 | aditiv, reuse pattern `reviewed`; B atinge masina de stari pazita | | 2 | CEO | US-007 vizibilitate coada imbatranita OBLIGATORIE | Mecanica | P1+observ | atenuarea agreata pt default OFF; inchide esecul silentios F3 | | 3 | CEO | US-008 retentie randuri tinute | Mecanica | P1 | F6: held nu primeste `purge_after` -> PII vesnic (GDPR/L.142) | | 4 | CEO | US-009 fixturi teste Auto ON + audit + echo API held | Mecanica | P1 | F7: default OFF stagneaza testele; invariant 5.7 raspuns onest | | 5 | CEO | Garda de confirmare OFF->ON + bulk (count + RAR PRODUCTIE) | Mecanica | P1 | F4/F5: flush ireversibil de randuri test in RAR real | | 6 | CEO | `held_for_account()` helper unic (DRY) | Mecanica | P4 | calcul held inline de 3x = sit uitat trimite automat | | 7 | CEO | Enum mod cont (live/hold/test) -> TODOS | Mecanica | P3 | scope dincolo de cerere; dream-state, nu blocant | ### Decizii de taste / provocari -> poarta finala (Faza 4) - **T-EXP1 (reframe testare, CEO F2 + DX4) -> REZOLVAT: user a ales "doar tinut".** Nici `rar_env`, nici documentarea `/valideaza` ca unealta de testare in 5.19. Ambele -> TODOS (posibil follow-up). Pastrat doar avertismentul de onestitate ca eliberarea declara real. - **T-LABEL (eticheta toggle, Design HIGH) -> REZOLVAT: user a ales REDENUMIREA.** Eticheta vizibila = **"Trimite automat la RAR"** (nu "Auto"), ca sa nu se ciocneasca cu "Trimitere automata" (worker viu) din `labels.py`. Conceptul/coloana raman `auto_send_enabled`. ### Faze Design/Eng/DX (audit) | Faza | Decizie cheie | Clasif. | Motiv | |------|---------------|---------|-------| | Design | D1-D7 rafinari UI (non-optimist, poller, modal, mobil, camp derivat, --warn, bannere) | Mecanica | structural, P5 explicit | | Eng | held_for_account la TOATE ~8 situri queued (bug reactivare router:237) | Mecanica | P5; bypass real Auto OFF | | Eng | conftest UPDATE id=1; index in _migrate; purge_after direct; account_id din sesiune | Mecanica | corectitudine/securitate | | DX | held pe SubmissionResult+GET; reuse AUTO_SEND_OPRIT; hub docs | Mecanica | P1; invariant 5.7 | ### Sumar completare review ``` +====================================================================+ | /autoplan — MEGA PLAN REVIEW — COMPLETION SUMMARY | +====================================================================+ | Mod | SELECTIVE EXPANSION | | Voci | Claude subagent (CEO/Design/Eng/DX); | | | Codex INDISPONIBIL (plafon -> 2026-07-18) | | Poarta premise | scop=AMBELE; default OFF (user, time-boxed) | | CEO | 7 findings, 2 critice -> atenuate | | Design | 5->8/10; 13 findings, 3 critice -> D1-D7 | | Eng | 7 issues; 1 BUG real (reactivare bypass) | | DX | 5->8/10; onestitate API -> US-010 | | Stories | 6 -> 10 (US-007/008/009/010 adaugate) | | Taskuri | 26 (14 P1, 9 P2, 3 P3), agregate pe faze | | Tema cross-faza | hold != sandbox testare (CEO F2 + DX4) | | Taste rezolvate | T-EXP1=doar tinut; T-LABEL=redenumire | | Deferate (TODOS) | enum mod cont; rar_env; doc /valideaza | | Test plan | scris pe disc (~/.gstack/.../test-plan) | | Artefacte taskuri | 4 JSONL pe faza | | Decizii nerezolvate | 0 | +====================================================================+ ``` ## GSTACK REVIEW REPORT | Review | Trigger | Why | Runs | Status | Findings | |--------|---------|-----|------|--------|----------| | CEO Review | `/plan-ceo-review` | Scope & strategy | 1 | issues_open->resolved | 5 propuneri, 4 acceptate, 2 deferate; 2 gap critice atenuate | | Eng Review | `/plan-eng-review` | Architecture & tests (required) | 1 | issues_open->resolved | 7 issues (1 bug bypass reactivare), 0 gap critice ramase | | Design Review | `/plan-design-review` | UI/UX gaps | 1 | issues_open->resolved | 5->8/10, 13 findings (3 critice) -> D1-D7 | | DX Review | `/plan-devex-review` | Developer experience gaps | 1 | issues_open->resolved | 5->8/10, onestitate API (US-010) | - **CROSS-MODEL:** N/A — Codex indisponibil (plafon utilizare pana 2026-07-18); voce unica Claude subagent pe toate fazele. - **VERDICT:** CEO + DESIGN + ENG + DX CLEARED (voce unica) — PRD revizuit, gata de implementare. Toate deciziile portilor inchise cu user. NO UNRESOLVED DECISIONS