<!-- /autoplan restore point: /home/claude/.gstack/projects/romfast-rar-autopass/main-autoplan-restore-20260629-150326.md -->
# 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
