# PRD 5.20 — Medii RAR per cont (Testare / Productie): activare, credentiale, selectie per trimitere **Stare**: aprobat > Proces complet: `docs/ROADMAP.md` §5. Contract RAR (sursa de adevar): `docs/api-rar-contract.md`. > Stare: `draft -> aprobat -> in-executie -> verify-pass -> inchis`. ## 1. Obiectiv Trateaza **Testare** si **Productie** ca doua medii RAR configurabile **per cont**. Fiecare mediu are, independent: o **bifa de activare** si un **set propriu de credentiale**. Un mediu e *disponibil* pentru trimitere doar daca e activat SI are credentiale. Din disponibilitate decurge tot UX-ul: cand un singur mediu e disponibil totul merge acolo (fara selector); cand ambele sunt disponibile, apare selector la import + toggle in statusbar + alegere in API. Trimiterile arata mereu un **badge** cu mediul tinta. Scop: clientul declara real pe Productie, iar cine are si cont de test RAR isi poate testa integrarea pe Testare — fara redeploy si fara variabila globala de mediu. **Premisa verificata (2026-06-29, doua seturi reale)**: test si prod sunt sisteme RAR **complet separate**; un set de credentiale se autentifica pe **exact unul** (creds dev: test 200 / prod 401; creds client real: test 401 / prod 200). Deci 2 seturi de creds per cont; un cont prod-only NU poate trimite la test fara cont de test emis de RAR. Detaliu memorat: vezi memoria de proiect "rar-test-prod-creds-separate". ## 2. Non-Goals (anti scope-creep) - NU eliminam `AUTOPASS_RAR_ENV` global: ramane **ancora de migrare** + fallback pentru actiuni de sistem fara cont (ex. keepalive login). Per-submission are precedenta cand exista. - NU configuram base_url-uri din UI (raman in `config.py`); NU adaugam un al treilea mediu. - NU gating pe plan/tier pentru Productie (decizie user: liber). „Guard-ul" e: Productie e tinta doar daca e activata + are creds, plus o confirmare unica la activarea Productie (constientizare L.142), NU per trimitere. - NU schimbam masina de stari, backoff-ul, sau payload-ul `postPrezentare`. - NU migram automat credentiale de prod ale clientilor — ei le introduc; migrarea doar muta creds-ul existent in slotul mediului sub care contul opera efectiv. ## 3. Cerinte transversale (reguli de derivare) - **REQ-DISP**: `medii_disponibile(cont)` = mediile din {test, prod} cu `enabled=1` SI creds prezente. Sursa unica de adevar pentru vizibilitatea selector/toggle si pentru validarea unei tinte cerute. - **REQ-VIZ**: selector la import + toggle in statusbar apar DOAR cand `len(medii_disponibile) >= 2`. La 1 mediu, tinta e implicita (acel mediu), fara selector. La 0, trimiterea e blocata cu mesaj „configureaza credentiale RAR". - **REQ-BADGE**: orice trimitere afiseaza badge Test/Productie (chiar si la 1 mediu — claritate ca declari real). - **REQ-DEFAULT**: `rar_env_default(cont)` e mereu unul din mediile disponibile; cont client nou = `prod`. Daca default-ul nu mai e disponibil (mediu dezactivat), cade pe singurul disponibil; daca 0 disponibile -> nicio tinta. - **REQ-CONF**: trimiterea pe Productie nu cere confirmare per-rand; constientizarea vine din badge + o confirmare UNICA la activarea mediului Productie in configurare. ## 4. Stories atomice > Backend + UI pentru acelasi comportament = stories separate. `Fisiere` + `Depinde de` complete. ### US-001: Schema — medii per cont (activare + creds) + env pe submission **Ca** sistem **vreau** sa stochez per cont activarea si credentialele fiecarui mediu, default-ul, si env-ul tinta pe fiecare submission **pentru ca** test si prod sunt sisteme separate cu credentiale separate. - **Depinde de**: — - **Fisiere**: `app/schema.sql`, `app/db.py` (migrare idempotenta), `tests/test_schema_migrate.py` - **Test intai (RED)**: `tests/test_schema_migrate.py` — `test_coloane_medii_pe_cont`, `test_default_client_prod_on_test_off`, `test_migrare_creds_in_slotul_env_global`, `test_submissions_rar_env` - **Acceptance criteria**: - [ ] `accounts`: `rar_test_enabled INTEGER NOT NULL DEFAULT 0`, `rar_prod_enabled INTEGER NOT NULL DEFAULT 1` (ambele CHECK IN (0,1)); `rar_creds_test_enc TEXT`, `rar_creds_prod_enc TEXT`; `rar_env_default TEXT NOT NULL DEFAULT 'prod' CHECK (rar_env_default IN ('test','prod'))` - [ ] `submissions.rar_env TEXT NOT NULL DEFAULT 'test' CHECK (rar_env IN ('test','prod'))` - [ ] **Migrare existenti (NU presupune env-ul)**: `rar_creds_enc` -> slotul `AUTOPASS_RAR_ENV` global de la migrare; seteaza `enabled=1` DOAR pe mediul cu creds; `rar_env_default` = acel mediu. Conturi fara creds: raman pe default-urile coloanei (prod on / test off). Coloana veche RAMANE acum (dropul e in US-013, dupa ce toate citirile trec pe per-env) - [ ] **(AUTO-FIX G — CRITIC, amendament AC) Backfill `submissions.rar_env` EXISTENT din `AUTOPASS_RAR_ENV` global**, NU lasa pe `DEFAULT 'test'`. Un rand prod pre-migrare etichetat 'test' -> US-006 reconciliaza contra endpoint TEST -> no-match -> re-send prod = DUPLICAT REAL IREVERSIBIL. `DEFAULT 'test'` ramane doar plasa pentru randuri net-noi (fiecare INSERT din US-004/005/009 seteaza `rar_env` explicit) - [ ] **(AUTO-FIX E4/3) Recompute `idempotency_key` pentru randurile existente** la forma env-aware (`build_key(account_id, canon, rar_env)` cu `rar_env`-ul backfill-at), ca lookup-urile de dedup (API + import) sa nu rateze randuri legacy -> altfel re-POST = duplicat - [ ] `test_submissions_rar_env` asserteaza ca un rand PRE-migrare ajunge cu env-ul global (NU 'test') si reconciliaza contra endpointului corect - [ ] migrare idempotenta pe DB existent, fara pierdere de date - [ ] `python3 -m pytest tests/test_schema_migrate.py -q` PASS - **Verificare E2E**: DB pre-migrare cu `AUTOPASS_RAR_ENV=test` -> creds aterizeaza in `rar_creds_test_enc`, `rar_test_enabled=1`, `rar_env_default='test'`. ### US-002: Logica de disponibilitate si default efectiv **Ca** sistem **vreau** un helper unic care intoarce mediile disponibile si default-ul efectiv al unui cont **pentru ca** vizibilitatea UI, API-ul si worker-ul sa decida identic (REQ-DISP/REQ-DEFAULT). - **Depinde de**: US-001 - **Fisiere**: `app/rar_env.py` (nou) sau `app/mapping.py`, `tests/test_rar_env_disponibil.py` - **Test intai (RED)**: `tests/test_rar_env_disponibil.py` — `test_doar_prod_cu_creds`, `test_ambele`, `test_zero_cand_lipsesc_creds`, `test_default_cade_pe_singurul_disponibil`, `test_enabled_fara_creds_nu_e_disponibil` - **Acceptance criteria**: - [ ] `medii_disponibile(account) -> list[str]` (subset din ['test','prod']) = enabled AND creds prezente - [ ] `rar_env_efectiv(account) -> 'test'|'prod'|None` aplica REQ-DEFAULT - [ ] `python3 -m pytest tests/test_rar_env_disponibil.py -q` PASS - **Verificare E2E**: — ### US-003: Idempotenta include rar_env **Ca** sistem **vreau** ca `build_key` sa incorporeze `rar_env` **pentru ca** aceeasi prezentare la test si apoi la prod sunt doua trimiteri reale distincte, nu un duplicat. - **Depinde de**: — - **Fisiere**: `app/idempotency.py`, `tests/test_idempotency.py` - **Test intai (RED)**: `tests/test_idempotency.py` — `test_key_difera_intre_test_si_prod`, `test_key_stabil_pe_env` - **Acceptance criteria**: - [ ] `build_key(account_id, canon, rar_env)` -> chei diferite test vs prod pe acelasi continut; stabil pe re-apel - [ ] toate apelurile (`router.py`, `import_router.py`) trec env-ul rezolvat - [ ] `python3 -m pytest tests/test_idempotency.py -q` PASS - **Verificare E2E**: — ### US-004: Rezolvare tinta la ingestie (cerere > default cont) + respinge tinta indisponibila **Ca** sistem **vreau** sa decid env-ul unui submission si sa resping tintele indisponibile **pentru ca** o tinta fara mediu activ/creds nu trebuie sa intre in coada. - **Depinde de**: US-002 - **Fisiere**: `app/validation.py`, `app/mapping.py`, `tests/test_rar_env_resolve.py` - **Test intai (RED)**: `tests/test_rar_env_resolve.py` — `test_cerere_castiga`, `test_fallback_default_cont`, `test_tinta_indisponibila_respinsa`, `test_valoare_invalida` - **Acceptance criteria**: - [ ] precedenta: valoare ceruta (daca e in `medii_disponibile`) > `rar_env_efectiv(cont)` - [ ] tinta ceruta dar indisponibila -> eroare clara („mediul X nu e activat / fara credentiale"), fara enqueue - [ ] valoare invalida (≠ test/prod) -> eroare de validare, fara fallback silentios - [ ] `python3 -m pytest tests/test_rar_env_resolve.py -q` PASS - **Verificare E2E**: — ### US-005: API — camp `rar_target` pe POST /v1/prezentari si /valideaza **Ca** integrator ROAAUTO **vreau** sa pot preciza `rar_target`, cu default = default-ul contului meu **pentru ca** sa aleg unde declar fara sa stiu env-ul global. - **Depinde de**: US-003, US-004 - **Fisiere**: `app/api/v1/router.py`, `app/models.py`, `tests/test_api_rar_target.py` - **Test intai (RED)**: `tests/test_api_rar_target.py` — `test_default_din_cont_cand_lipseste`, `test_target_explicit`, `test_target_indisponibil_respins`, `test_get_ecou_rar_env`, `test_valoare_invalida_422` - **Acceptance criteria**: - [ ] camp optional `rar_target: "test"|"prod"` pe `POST /v1/prezentari` si `/valideaza` - [ ] absent -> `rar_env_efectiv(cont)` (pt client prod-only = `prod`) - [ ] tinta indisponibila -> raspuns clar, fara enqueue; `SubmissionResult` + GET ecou-iesc `rar_env` - [ ] valoare invalida -> 422 fara echo de input (handler global pastrat) - [ ] `python3 -m pytest tests/test_api_rar_target.py -q` PASS - **Verificare E2E**: `POST /v1/prezentari` fara `rar_target` pe un cont prod-only -> submission env=prod. ### US-006: Worker — sesiuni si trimitere per (cont, env) **Ca** worker **vreau** login/JWT separat per `(account_id, rar_env)`, cu base_url + creds corecte per submission **pentru ca** test si prod sunt sisteme RAR diferite. - **Depinde de**: US-001 - **Fisiere**: `app/worker/__main__.py` (`AccountSessions`), `app/rar_client.py` (base_url per env), `app/reconcile.py`, `tests/test_worker_rar_env.py` - **Test intai (RED)**: `tests/test_worker_rar_env.py` — `test_sesiune_separata_per_env`, `test_base_url_dupa_submission`, `test_creds_din_slotul_env`, `test_reconcile_pe_env_corect` - **Acceptance criteria**: - [ ] cheia cache sesiune = `(account_id, rar_env)`; JWT/keepalive/last_rar_login_ok per env - [ ] `RarClient` primeste env/base_url explicit (nu doar `settings.rar_base_url`) - [ ] creds alese: submission efemere -> `accounts.rar_creds_{env}_enc`; lipsa -> blocaj clar (nu trimite) - [ ] reconcilierea cauta in `finalizate` pe endpoint-ul `submission.rar_env` - [ ] purjarea atinge DOAR `submissions.rar_creds_enc`, NU `accounts.rar_creds_{env}_enc` - [ ] `python3 -m pytest tests/test_worker_rar_env.py -q` PASS - **Verificare E2E**: doua submission-uri (test + prod, creds prezente) -> doua login-uri distincte in jurnal. ### US-007: Validare login pe env-ul ales (signup / preview / test integrare) **Ca** sistem **vreau** ca validarea credentialelor sa loveasca mediul caruia ii apartin **pentru ca** o parola prod nu se valideaza contra RAR test si invers (confirmat: 401 incrucisat). - **Depinde de**: US-002 - **Fisiere**: `app/web/routes.py`, `app/rar_client.py`, `app/web/templates/_integrare.html`, `tests/test_validare_env.py` - **Test intai (RED)**: `tests/test_validare_env.py` — `test_valideaza_pe_env_creds`, `test_mesaj_distinge_env` - **Acceptance criteria**: - [ ] validarea (signup, „testeaza integrarea", preview) foloseste env-ul setului de creds verificat - [ ] mesaj distinct „creds invalide pe TESTARE" vs „pe PRODUCTIE" - [ ] `python3 -m pytest tests/test_validare_env.py -q` PASS - **Verificare E2E**: in UI „testeaza integrarea" cu creds prod -> login pe endpoint prod. ### US-008: Configurare cont — doua medii (bifa activare + creds), default, confirmare prod **Ca** titular de cont **vreau** sa activez fiecare mediu, sa-i introduc credentialele si sa aleg default-ul **pentru ca** vreau sa controlez unde se poate trimite si unde merge implicit. - **Depinde de**: US-001, US-007 - **Fisiere**: `app/web/routes.py`, `app/web/templates/_cont.html`, `app/crypto.py` (refolosit), `tests/test_cont_medii.py` - **Test intai (RED)**: `tests/test_cont_medii.py` — `test_activeaza_si_salveaza_creds_per_env`, `test_default_doar_dintre_disponibile`, `test_activare_prod_cere_confirmare`, `test_creds_criptate_fara_echo` - **Acceptance criteria**: - [ ] doua sectiuni „Testare" si „Productie": fiecare cu bifa Activeaza + campuri email/parola; default client = Productie bifat, Testare nebifat - [ ] la salvare, creds-ul fiecarui mediu activat e validat prin login pe acel env (US-007); invalid -> nu se marcheaza disponibil - [ ] selectorul de default ofera DOAR mediile disponibile; nu poti seta default un mediu indisponibil - [ ] activarea mediului Productie cere o confirmare unica „Inteleg ca trimiterile pe Productie sunt declaratii reale (L.142)" - [ ] creds criptate Fernet in `rar_creds_{env}_enc`, niciodata reflectate inapoi in pagina - [ ] `python3 -m pytest tests/test_cont_medii.py -q` PASS - **Verificare E2E**: activez Testare + creds valide si Productie + creds invalide -> doar Testare devine disponibil. ### US-009: Import web — selector mediu conditionat de disponibilitate **Ca** operator **vreau** sa aleg mediul la import doar cand am ≥2 disponibile, pre-bifat pe default **pentru ca** la un singur mediu alegerea e inutila. - **Depinde de**: US-002, US-004 - **Fisiere**: `app/import_router.py`, `app/import_parse.py`, `app/web/templates/_upload.html`, `_preview_import.html`, `tests/test_import_rar_env.py` - **Test intai (RED)**: `tests/test_import_rar_env.py` — `test_selector_ascuns_la_un_mediu`, `test_selector_prezent_si_prebifat_la_doua`, `test_commit_seteaza_env_pe_submissions` - **Acceptance criteria**: - [ ] selector Test/Prod apare DOAR daca `len(medii_disponibile) >= 2`; initial = `rar_env_efectiv` - [ ] la 1 mediu: fara selector, toate randurile primesc acel mediu - [ ] la commit, toate submission-urile lotului primesc `rar_env` ales - [ ] `python3 -m pytest tests/test_import_rar_env.py -q` PASS - **Verificare E2E**: cont prod-only -> import fara selector, submissions env=prod; cont cu ambele -> selector pre-bifat. ### US-010: Badge mediu in liste, preview, jurnal, audit + ecou API **Ca** utilizator **vreau** sa vad pe fiecare trimitere mediul tinta **pentru ca** sa nu confund testul cu realul. - **Depinde de**: US-001 - **Fisiere**: `app/web/templates/_submissions.html`, `_coada.html`, `_trimitere_detaliu.html`, `_preview_rand.html`, `_jurnal.html`, `app/web/routes.py` (audit export), `app/api/v1/router.py` (GET), `tests/test_badge_rar_env.py` - **Test intai (RED)**: `tests/test_badge_rar_env.py` — `test_badge_in_lista`, `test_audit_contine_rar_env`, `test_get_ecou_rar_env` - **Acceptance criteria**: - [ ] badge vizibil (Test vs Productie, culori distincte) in lista, preview rand, detaliu, jurnal - [ ] `rar_env` in audit export si in `GET /v1/prezentari(/{id})` - [ ] `python3 -m pytest tests/test_badge_rar_env.py -q` PASS - **Verificare E2E**: rand prod -> badge „Productie"; export audit contine coloana. ### US-011: Statusbar — indicator mediu + toggle conditionat **Ca** operator **vreau** sa vad in statusbar mediul default si sa-l pot schimba cand am ≥2 medii **pentru ca** sa stiu mereu unde trimit si sa comut rapid. - **Depinde de**: US-002, US-008 - **Fisiere**: `app/web/templates/_status.html`, `base.html`, `app/web/routes.py` (ruta toggle account-scoped), `tests/test_statusbar_env.py` - **Test intai (RED)**: `tests/test_statusbar_env.py` — `test_afiseaza_env_default`, `test_toggle_doar_la_doua_medii`, `test_toggle_schimba_default` - **Acceptance criteria**: - [ ] statusbar afiseaza mediul default al contului logat (Test/Productie), distinct vizual - [ ] toggle apare DOAR la `len(medii_disponibile) >= 2`; comutarea schimba `rar_env_default` (HTMX, fara reload) - [ ] la 1 mediu: doar eticheta statica - [ ] `python3 -m pytest tests/test_statusbar_env.py -q` PASS - **Verificare E2E**: cont cu ambele -> click statusbar schimba default; cont prod-only -> eticheta fixa „Productie". ### US-012: Audit + e2e pe medii **Ca** lead **vreau** evenimente de audit la activare mediu / schimbare default / blocaj tinta, plus teste e2e **pentru ca** orice atingere a mediului Productie trebuie trasabila. - **Depinde de**: US-005, US-006, US-009, US-011 - **Fisiere**: `app/audit.py`/`log_event`, `tests/test_e2e_rar_env.py` - **Test intai (RED)**: `tests/test_e2e_rar_env.py` — `test_lant_import_pana_la_queued`, `test_activare_prod_logata`, `test_tinta_indisponibila_blocata_si_logata` - **Acceptance criteria**: - [ ] audit la: activare/dezactivare mediu, schimbare `rar_env_default`, blocaj tinta indisponibila - [ ] e2e (TestClient + SQLite temporar) acopera import->queued cu env corect, ambele cai - [ ] `python3 -m pytest tests/test_e2e_rar_env.py -q` PASS - **Verificare E2E**: jurnal arata „mediu Productie activat" + „default schimbat" cu cont + timestamp. ### US-013: Retragerea `accounts.rar_creds_enc` (toate citirile -> per-env, apoi DROP) **Ca** sistem **vreau** ca toate cele ~40 de locuri care citesc `accounts.rar_creds_enc` sa treaca pe coloanele per-mediu si apoi sa sterg coloana veche **pentru ca** modelul per-env sa fie sursa unica, fara schema dubla. - **Depinde de**: US-005, US-006, US-008 (consumatorii principali deja pe per-env) - **Fisiere**: `app/worker/__main__.py` (fallback + bucla keepalive „toate conturile cu creds"), `app/web/routes.py` (indicatorii `are_creds`), `app/api/v1/integrare_router.py` (`are_creds_rar`), `app/api/v1/router.py` (`POST /v1/conturi/rar-creds` devine env-aware), `app/accounts.py` (purge la stergere cont), `app/db.py` (DROP cu garda), `app/models.py`, `tests/test_retragere_creds_enc.py` - **Test intai (RED)**: `tests/test_retragere_creds_enc.py` — `test_niciun_read_pe_coloana_veche`, `test_conturi_rar_creds_env_aware`, `test_are_creds_pe_per_env`, `test_drop_cu_garda_blocat_daca_lipsa_copiere` - **Acceptance criteria**: - [ ] worker fallback + keepalive citesc `rar_creds_{env}_enc` (per env), nu coloana veche - [ ] `are_creds` (web) + `are_creds_rar` (integrare) devin per-mediu („are creds pe Testare/Productie") - [ ] `POST /v1/conturi/rar-creds` primeste mediul (`rar_target`/`env`) si scrie in slotul corect — **schimbare de contract API**, documentata in `docs/api-rar-contract.md` - [ ] purjarea la stergere cont (`accounts.py`) sterge ambele sloturi per-env - [ ] **DROP cu garda**: migrarea verifica intai ca fiecare `rar_creds_enc` non-null a aterizat intr-un slot per-env (assert), apoi `ALTER TABLE accounts DROP COLUMN rar_creds_enc` (SQLite 3.45 OK); verificare esuata -> NU dropa, ridica eroare (fail-safe) - [ ] **(AUTO-FIX 6a — CRITIC) Elimina ATOMIC blocul `ADD COLUMN rar_creds_enc` din `db.py:77-78`** in aceeasi migrare cu DROP-ul. Altfel urmatorul boot vede coloana absenta si o re-ADD goala -> ping-pong perpetuu, garda se rupe. Garda e one-way: dropeaza doar cand sloturile per-env sunt populate SI coloana inca exista - [ ] **(AUTO-FIX 6b — HIGH) DROP-ul nu crapa boot-ul**: `init_db/_migrate` ruleaza la fiecare pornire a ambelor procese; un `DROP COLUMN` care arunca (SQLite < 3.35 / assert garda esuat) propaga -> API + worker crash-loop. Prinde + degradeaza (log + lasa coloana pe loc), NU arunca. Asserteaza `sqlite_version() >= 3.35` (verifica SQLite din imaginea Docker, nu doar dev box) si sare drop-ul gracios sub acel prag - [ ] **(AUTO-FIX 6c — HIGH) Re-ruleaza backfill old->new IMEDIAT inainte de assert**: creds setate via `POST /v1/conturi/rar-creds` intre deploy-ul US-001 si US-013 aterizeaza doar in coloana veche; copiaza-le in slotul per-env (ancora globala) inainte de garda, altfel garda blocheaza drop-ul la nesfarsit - [ ] **(AUTO-FIX 6d) Verificare prin `PRAGMA table_info(accounts)`** ca `rar_creds_enc` lipseste, NU doar prin grep (ambele coloane — `accounts` si `submissions` — au acelasi nume; purjarea worker-ului ramane pe `submissions.rar_creds_enc`) - [ ] `grep -rn "rar_creds_enc" app/` nu mai gaseste citiri pe `accounts` (doar `submissions.rar_creds_enc` ramane) - [ ] `python3 -m pytest tests/test_retragere_creds_enc.py -q` PASS - **Verificare E2E**: dupa migrare, `PRAGMA table_info(accounts)` nu mai contine `rar_creds_enc`; fluxul de cont (salvare creds, worker trimite) functioneaza pe per-env. ## 5. Riscuri - **Trimitere reala accidentala** (FINALIZATA terminal, L.142): atenuat prin badge omniprezent + Productie disponibil doar dupa activare explicita + creds + confirmare unica la activare. NU exista anulare la RAR. - **Default invalid dupa dezactivare mediu**: REQ-DEFAULT recalculeaza; teste US-002 acopera caderea pe disponibil. - **Migrare ambigua** (CONFIRMAT): `rar_creds_enc` poate fi test SAU prod; migrarea aterizeaza in slotul `AUTOPASS_RAR_ENV` global + activeaza doar acel mediu. De validat pe DB-ul real inainte de deploy. - **Client prod-only nu poate testa**: corect by design; UI explica explicit (nu „creds invalide"), nu ofera Testare fara creds test. - **Idempotenta**: schimbarea cheii (US-003) cere ca TOATE apelurile sa treaca env-ul; grep dupa `build_key` + teste. - **Retragere `rar_creds_enc` (US-013)**: ~40 citiri + endpoint API `POST /v1/conturi/rar-creds` (contract). Blast radius mare, dar single-release e mai curat decat schema dubla. DROP cu garda (assert copiere) = fara pierdere de date; produsul e in TESTE (putine conturi reale). Recuperarea via coloana veche dispare dupa DROP — acceptat. ## 6. Intrebari deschise — REZOLVATE (user 2026-06-29) - [x] **Default API** = default-ul contului (NU „test" hardcodat), fiindca clientii sunt prod-only. CONFIRMAT. - [x] **Activare implicita cont nou** = Productie on / Testare off; contul operator setat manual pe Testare. CONFIRMAT. - [x] **Confirmare Productie** = o data, la activarea mediului in configurare (nu per trimitere). CONFIRMAT. - [x] **`rar_creds_enc` vechi** = se STERGE in acest PRD (US-013), nu in 5.2x. DROP cu garda (assert copiere), toate citirile mutate pe per-env, endpoint `POST /v1/conturi/rar-creds` devine env-aware. CONFIRMAT. ## 7. Valuri de executie (graful de dependente) ``` Val 1: [US-001] [US-003] ← schema + idempotenta (fisiere distincte) → paralel Val 2: [US-002] ← deblocat de US-001 Val 3: [US-004] [US-006] [US-007] ← rezolvare ingestie / worker / validare → paralel Val 4: [US-005] [US-008] [US-009] [US-010] ← API / config cont / import / badge → paralel Val 5: [US-011] ← statusbar (depinde de US-008) Val 6: [US-012] [US-013] ← audit + e2e; retragere rar_creds_enc + DROP (depind de tot) ``` --- ## Raport VERIFY > Completat de subagentul verificator (context curat) in faza VERIFY — vezi ROADMAP §5.6. > PASS/FAIL per criteriu, cu dovezi. Lipseste pana la VERIFY. --- ## /autoplan Review (2026-06-29, commit 7371c37) Voci: Claude (primar) + Claude subagent (independent). Codex indisponibil (usage limit, revine 18 iul) -> mod `[subagent-only]`. Poarta premisa: user a ales **"Build full per-account multi-env (as planned)"** — premisa de baza (sisteme separate) verificata live; nevoia de dashboard unic justifica per-cont peste 2 deployment-uri pinned. ### Auto-fixuri (corectitudine/siguranta — incorporate in stories) | # | Story | Gap (gasit de) | Fix incorporat | Principiu | |---|-------|----------------|----------------|-----------| | G | US-001 | **CRITIC** (subagent): migrarea backfill-eaza creds dar NU `submissions.rar_env` existent; randuri prod pre-migrare cad pe DEFAULT 'test' -> US-006 reconciliaza contra endpoint TEST -> no-match -> **re-send prod = duplicat real ireversibil** | Migrarea backfill-eaza `submissions.rar_env` din `AUTOPASS_RAR_ENV` global (DEFAULT 'test' doar pentru randuri net-noi). Test: rand prod pre-migrare reconciliaza contra endpoint prod | P1 completeness + siguranta | | L | **US-005/US-013** (NU US-006 — eng finding 5: write-back e in `router.py`, pe care US-006 nu-l atinge) | HIGH (ambele voci, `router.py:250`): write-back creds efemere API -> `accounts.rar_creds_enc` durabil nu e rutat pe slotul `submission.rar_env` | Write-back tinteste `accounts.rar_creds_{submission.rar_env}_enc` + test. **Plus**: nu auto-propaga creds API NEVALIDATE in slotul durabil per-env (ar putea clobber-i un slot login-validat); propaga doar dupa login reusit | P1 | | K | US-013 | HIGH (subagent): `POST /v1/conturi/rar-creds` e contract extern; env-aware in-place = breaking | Endpoint **aditiv**: param `env` optional, default = default cont; apelanti vechi neatinsi. (Independent de decizia DROP) | P5 explicit + back-compat | | M2 | US-013 | MEDIUM (Claude): `_keepalive_target` alege un cont fara notiune de env dupa per-env | Keepalive foloseste ancora globala `AUTOPASS_RAR_ENV` + un cont cu creds in slotul acelui env | P5 | | M3 | US-003 | MEDIUM (Claude): `_already_sent_lookup` (import_router.py:369) are dual-lookup legacy; adaugarea env in cheie cere extinderea lui, nu doar a parametrului | US-003 extinde dual-lookup (cheie noua env-aware + fallback legacy) | P1 | | D | US-001 | HIGH (subagent): corectitudinea migrarii e "de validat manual"; trebuie poarta testata | Script de audit pre-migrare (raporteaza slot-ul atribuit) + assert DROP-cu-garda existent ca poarta, nu nota manuala | P1 | | M | US-012 | MEDIUM (subagent): niciun test live dual-env; riscul dominant (rutare gresita env) e exact ce SQLite nu prinde | Test live opt-in dual-env (extinde `test_live_rar`): 1 rand test + 1 prod -> 2 login-uri, 2 endpoint-uri, badge corecte, reconciliere pe env corect | P1 | | backup | US-013 | MEDIUM (Claude): "recovery via coloana veche dispare dupa DROP — acceptat" | Inainte de DROP, dump coloana veche criptata intr-un backup timestamped (recuperare supravietuieste DROP) | P2 boil-lake | ### Decizii user la poarta finala (REZOLVATE 2026-06-29) — APROBAT - **A (DROP US-013) -> PASTREAZA single-release.** User: "aplicatia e doar in teste, nu folosita de clienti" -> blast radius mic, rollback-ul conteaza mai putin. Decizia §6 ramane. **Garzile 6a/6b/6c sunt obligatorii in AC US-013** (eliminare atomica bloc ADD, catch+degrade fara boot-crash, re-backfill interim) + backup criptat inainte de DROP. NU se amana. - **J/H1 (interlock prod) -> doar butonul de commit colorat (F8), FARA modal.** REQ-CONF ramane. Lantul: bifa activare (o data) + badge "fierbinte" + buton "Declară la PRODUCȚIE (real)". Fara confirmare per-commit (evita oboseala de click; clientii prod-only oricum n-au selector). - **H (fallback default) -> doar toast zgomotos (F5), FARA re-confirmare.** REQ-DEFAULT auto-fallback ramane; toast-ul "Mediul implicit a trecut pe X" face flip-ul vizibil. Fara gate suplimentar. ### Taste (recomandari acceptate — fara override) - **T1**: token dedicat `--prod` (brick) pentru badge-ul Productie. **T2**: `rar_env` ca nume unic input+output (scoate `rar_target`/`env`). ### Taste decisions (auto-decise cu recomandare — override la poarta) - **T1 — token culoare Productie**: rosu (`--err`) se ciocneste cu erorile, amber (`--warn`) cu badge-ul legacy. Recomandat: token dedicat `--prod` (brick inchis) SAU `--accent` plin. (design F2) - **T2 — nume camp request**: recomandat `rar_env` peste tot (un singur nume input+output), scoate `rar_target`/`env`. (DX F1) ### Teme cross-fază (semnal de incredere ridicat — aparut independent in 2+ faze) - **Siguranta declaratiei reale ireversibile** — TOATE 4 fazele (CEO G/H1/J, Design F1/F8/F10, Eng 1b/3/G, DX F2/F3/F4). Semnalul dominant: badge + interlock + discoverability + rutare env corecta converg pe "nu declara real din greseala". - **Flip silentios al mediului default** — CEO-H, Design-F5, DX-F3 (3 faze). Fa flip-ul zgomotos + nu auto-promova prod silentios. - **Risc DROP US-013** — CEO-A, Eng 6a/6b/6c (2 faze). Intareste amanarea DROP-ului. - **Ambiguitate spec/nume care musca implementer-ul** — Design-F14, Eng-4a, DX-F1/F7. Auto-fixurile TREBUIE sa intre in AC + contract inainte de implementare. ### NOT in scope (confirmat) Eliminarea ancorei globale `AUTOPASS_RAR_ENV`; base_url din UI; al treilea mediu; gating plan/tier pe prod; schimbari masina-stari/backoff/payload; auto-migrare creds prod client. (PRD §2) ### Ce exista deja (leverage) `crypto.py` Fernet (creds per-env), `AccountSessions` (re-key (cont,env)), `RarClient` (primeste settings; +param env), `config.rar_base_url_test/prod` (deja prezent), `build_key` (+param), `account_scope_clause`. Fara infra noua. ### Auto-fixuri DESIGN (structurale — incorporate in stories) Voci: Claude (primar) + Claude subagent. Scorecard: 1 CRITIC, 7 HIGH, 5 MEDIUM, toate CONFIRMED. | # | Story | Gap | Fix incorporat | Sev | |---|-------|-----|----------------|-----| | F1 | US-010 | **CRITIC**: "culori distincte" e singura spec a singurului guard vizual contra riscului dominant | Badge **normativ**: Productie = fill plin, saturat, text alb, iconita + cuvant complet UPPERCASE cu diacritice ("PRODUCȚIE"); Testare = outline/tint linistit (muted/accent), receding. Asimetria de greutate ESTE designul | CRITIC | | F2 | US-010 | HIGH: rosu (`--err`) rezervat erorilor, amber (`--warn`) ocupat de `.badge-env` legacy + needs_* | Token dedicat `--prod` (ex. brick `#B4452F`) SAU `--accent` plin pentru Productie; hex/token scris in AC, nu improvizat per template. (taste: hexul exact -> poarta) | HIGH | | F3/F12 | US-010 | HIGH: "Test/Testare/prod/PRODUCTIE" folosite interschimbabil; bypass `labels.py` | `labels.py` adaugat in Fisiere: `ETICHETE_ENV` + `eticheta_env(env)->(text,css)` (oglindeste `eticheta_scurta`). Productie UPPERCASE+diacritice, Testare title-case; clase `.badge-prod/.badge-test` definite o data in base.html langa `.sugg-sursa` | HIGH | | F11 | US-011 | HIGH: `.badge-env` EXISTENT in header arata `AUTOPASS_RAR_ENV` global -> dupa 5.20 e semantic gresit; doua indicatoare env cu surse diferite in acelasi viewport | US-011 retrage/repurpune header `.badge-env` (preferat: scos pentru user logat, inlocuit de indicatorul account-scoped din statusbar). NU coexista doua surse de adevar | HIGH | | F4 | US-009 | HIGH: starea 0-medii e numita dar nedesignata; blocaj la commit (dupa munca) = calea minima | Blocaj la UPLOAD (nu commit): banner `--warn` (refoloseste pattern "Cont in asteptare", `_status.html:8`) + CTA link `?tab=cont`, inainte de drop-zone | HIGH | | F5 | US-011 | HIGH: schimbarea silentioasa a default-ului (mediu dezactivat) nu are UI -> target real/test comuta fara ca userul sa stie | Toast explicit (componenta `#toast` exista) la schimbarea `rar_env_default` ca efect al disponibilitatii: "Mediul implicit a trecut pe X". Leaga de CEO-H | HIGH | | F8 | US-009 | HIGH: o bifa la activare apoi nimic = sub-avertizare; modalul per-trimitere a fost respins (REQ-CONF) | Butonul de commit POARTA greutatea cand target=Productie: "Declară la PRODUCȚIE (real)" + culoarea Productie (FARA modal, FARA click extra -> nu incalca REQ-CONF). Copy bifa activare: adauga ireversibilitatea ("declarații oficiale, finale și fără anulare") | HIGH | | F6/F7 | US-008/US-011 | MEDIUM: stari loading/error pt toggle HTMX + validare creds la RAR nespecificate; stare per-sectiune (activat-fara-creds-valide) | toggle: `hx-indicator` + disabled in zbor, pe esec NU schimba default + eroare; US-008 validare creds arata `htmx-indicator` ("se verifica la RAR…") + esec in `.banner` cu copy per-env (US-007); fiecare sectiune arata 3 stari: dezactivat / activat-fara-creds / disponibil | MEDIUM | | F9/F10 | US-009 | MEDIUM/HIGH: selectorul absent la 1 mediu = env invizibil la import; default pre-bifat prod la prima trimitere | Mereu randeaza un indicator env la import (eticheta statica la 1 mediu, toggle la >=2, ACEEASI pozitie). Prod pre-bifat e sigur DOAR daca F8+F9 livreaza impreuna — legate explicit in AC | HIGH | | F13 | US-010 | MEDIUM: sa nu forkeze un badge structural nou | Refoloseste idiomul `.sugg-sursa` (10px, weight 700, tint+border) pt Testare; Productie = aceeasi geometrie dar fill plin+alb+icon (spargerea e semnalul) | MEDIUM | ### Auto-fixuri ENG (corectitudine/deploy — incorporate in stories) Voci: Claude (primar) + Claude subagent (verificat contra codului real). **Meta (eng 4a): toate auto-fixurile de mai jos sunt NORMATIVE si trebuie sa intre in AC-ul story-urilor inainte de implementare — un implementer care urmeaza AC-ul literal, fara ele, livreaza bug-urile critice.** G + 6a deja imbinate in AC US-001/US-013. | # | Story | Gap (vs cod real) | Fix | Sev | |---|-------|-------------------|-----|-----| | E1/1a | US-006 | `get_token` purjeaza `submissions.rar_creds_enc WHERE account_id=?` -> dupa re-key, login TEST sterge creds efemere ale submission-urilor PROD ale contului -> prod blocat | `WHERE account_id=? AND rar_env=?` + test `test_purge_creds_doar_pe_env` | HIGH | | 1b/E6 | US-006 | `recover_orphans` filtreaza doar pe `account_id`; iterat per sesiune (cont,env) reconciliaza orfanii prod contra endpoint TEST -> no-match -> re-POST prod = DUPLICAT real | +`rar_env` in WHERE; apelat per (cont,env) din `active()`; test orfan env A nereconciliat contra env B | HIGH/CRITIC | | 3/E4 | US-003 | API channel (`router.py:223`) NU are dual-lookup; re-POST al unui rand pre-5.20 cu cheie env-aware rateaza randul legacy -> duplicat. Import dual-lookup ignora env-ul randului matchuit | Recompute-keys la migrare (US-001, vezi acolo) acopera ambele canale uniform; daca pastrezi dual-lookup, exista si in `router.py` SI gate pe `matched_row.rar_env==target_env` | HIGH | | 1c/E8 | US-006 | `claim_one` nu selecteaza `s.rar_env` -> worker nu poate alege cheia sesiune/base_url/slot | AC explicit: claim selecteaza + propaga `rar_env` in dict-ul `claimed` | MEDIUM | | 1d | US-006/US-001 | `worker_heartbeat` e un singur rand global (`WHERE id=1`); US-006 cere `last_rar_login_ok` PER env dar US-001 nu adauga schema per-env -> neimplementabil ca scris | Decizie: pastreaza heartbeat global (JWT/sesiune per env e suficient), scoate "per env" din AC US-006; SAU adauga coloana in US-001. Recomandat: global | MEDIUM | | 1e | US-006 (doc) | `_refresh_nomenclator` upsert intr-un `nomenclator_rar` env-less la fiecare login; login test suprascrie cu coduri test, prod cu prod -> un cod valid pe prod poate fi respins la ingestie daca ultimul refresh a fost test | Documenteaza presupunerea (nomenclator identic intre medii — aceleasi 18 coduri) SAU scope per-env (out of scope acum). Minim: nota explicita | MEDIUM | | 5 | US-005/US-013 | write-back creds API nevalidate -> slot durabil (vezi L de mai sus) | re-asignat la US-005/US-013; propaga doar dupa login reusit | MEDIUM/HIGH | | 6a..6d | US-013 | ping-pong re-ADD / boot-crash / interim-creds / grep ambiguu | imbinate in AC US-013 (vezi acolo) | CRITIC/HIGH | ### ENG DUAL VOICES — CONSENSUS TABLE ``` Dimension Claude Subagent Consensus ────────────────────────────── ──────── ───────── ──────────────────── 1. Architecture sound? da/cond da/cond CONFIRMED (cond. fixuri) 2. Test coverage sufficient? lacune +API b/c CONFIRMED lacune 3. Performance risks? low low CONFIRMED low 4. Security (creds routing)? L/5 5+unvalid CONFIRMED 5. Error paths (boot)? E1/E9 6a/6b CRIT CONFIRMED (boot-crash) 6. Deployment risk (DROP)? migrare CRIT/HIGH CONFIRMED ELEVAT -> intareste challenge A ``` Codex: indisponibil (N/A). Mesaj-cheie: caile de duplicat ireversibil (1b, 3) si boot-crash/ping-pong (6a, 6b) musca in productie; intaresc recomandarea de a amana DROP-ul (challenge A). ### Diagrama teste (codepath -> acoperire) | Codepath nou | Story test | Stare | |---|---|---| | `medii_disponibile`/`rar_env_efectiv` | US-002 | acoperit | | resolve target (cerere>default), respinge indisponibil | US-004 | acoperit | | idempotency env-aware + **recompute legacy** | US-003/US-001 | GAP recompute -> adaugat | | **migrare backfill `submissions.rar_env`** | US-001 | GAP (G) -> adaugat in AC | | worker sesiune (cont,env) + base_url per env | US-006 | acoperit | | **purge creds scoped pe env** | US-006 | GAP (E1) -> adaugat | | **recover_orphans per env** | US-006 | GAP (1b) -> adaugat | | **write-back slot routing** | US-005/013 | GAP (L/5) -> adaugat | | reconcile endpoint per env (inline + **orfani**) | US-006 | inline acoperit; orfani GAP -> adaugat | | **keepalive env (ancora globala)** | US-013 | GAP (M2) -> adaugat | | DROP garda: assert + **idempotent re-run** + **fail-loud/no-crash** | US-013 | partial -> intarit (6a/6b/6c) | | **API-channel idempotency back-compat** | US-003 | GAP (3) -> adaugat | | badge/labels env | US-010 | acoperit | | API `rar_target` default/explicit/invalid/indisponibil | US-005 | acoperit | | config 2 sectiuni + confirmare prod | US-008 | acoperit | | statusbar toggle viz + **retragere header `.badge-env`** | US-011 | toggle acoperit; header GAP (F11) -> adaugat | | **live dual-env smoke** | US-012 | GAP (M) -> adaugat opt-in | ### Auto-fixuri DX (contract API extern — incorporate in stories) Voci: Claude (primar) + Claude subagent (perspectiva integrator VFP/ROAAUTO). Riscul ireversibilitatii ridica stacheta pe claritate nume / eroare / discoverability pre-trimitere. | # | Story | Gap | Fix | Sev | |---|-------|-----|-----|-----| | F1 | US-005/US-013 | Trei nume pt un concept: input `rar_target`, echo/DB `rar_env`, rar-creds `env` (US-013 AC scrie literal "rar_target/env") | **Un singur cheie: `rar_env`** pe input + output + rar-creds (englez snake, consistent cu coloana si `on_unmapped_error`). Scoate `rar_target`/`env`. (taste usor -> poarta) | HIGH | | F2 | US-004 | Eroarea "mediu indisponibil" e proza, fara `cod`/envelope 6-chei/status; `errors.py` nu e in Fisiere | `RAR_MEDIU_INDISPONIBIL` in `errors.CATALOG` (problema/cauza cu lista disponibile/fix "activeaza in Cont"); adauga `errors.py` la Fisiere US-004; distinge literal-invalid (422 pydantic) de valid-dar-indisponibil (cod dedicat); acopera si cazul 0-medii | HIGH | | F3 | US-004/contract | Flip runtime test->prod prin canal web: operator comuta disponibilitatea -> apelant API fara `rar_env` trece silentios pe prod (real). Migrarea previne flip la DEPLOY, nu la RUNTIME | Mitigat de F4+F5 (probe pre-trimitere); documenteaza reasignarea ca comportament cunoscut; leaga de CEO-H | HIGH | | F4 | US-010 (sau story noua) | Niciun GET nu expune `medii_disponibile`/`rar_env_default` -> integratorul afla env-ul doar din eroare sau dupa o trimitere reala | `GET /v1/conturi/medii` account-scoped: `{medii_disponibile, rar_env_default, test:{enabled,has_creds}, prod:{...}}` (refoloseste helper US-002, <1 fisier) | HIGH | | F5 | US-005 | `ValidareResult` (dry-run) NU ecou-ieste `rar_env`; dry-run e canalul sigur de a confirma unde ar ateriza o trimitere reala | adauga `rar_env: str` la `ValidareResult` + `/valideaza`; `models.py` | MEDIUM | | F6 | US-004/US-005 | Respingere whole-request vs per-rand inconsistenta cu `on_unmapped_error` (per-rand, 200) | Decide + documenteaza; recomandat: corp parsabil imbogatit cu `cod` (prietenos VFP), noteaza asimetria intentionat | MEDIUM | | F7 | US-005/US-010/US-004/US-013 | Contractul (sursa adevar) actualizat doar pt rar-creds; lipsesc field-ul nou, echo-ul, cod-ul nou. **`/v1/conturi/rar-creds` NU e documentat deloc azi** -> US-013 e documentare de la zero, nu amendament | AC explicit "update `api-rar-contract.md`" pe fiecare; US-013 documenteaza endpoint-ul intreg (req/resp, param env, slot default) | HIGH | | F8 | US-013 (doc) | `env` optional default = slot default cont: integrator cu creds TEST pe cont nou (default prod) le scrie silentios in slot prod -> US-007 le respinge "invalide pe PRODUCTIE" desi sunt valide (test) | pastreaza aditiv; documenteaza ca omiterea `env` tinteste slotul default; mesaj validare sugereaza nepotrivire env ("creds valide pentru alt mediu?") | MEDIUM | ### DX DUAL VOICES — CONSENSUS TABLE ``` Dimension Claude Subagent Consensus ─────────────────────────────── ─────── ───────── ────────────── 1. Getting started (aditiv)? low fr low fr CONFIRMED low 2. Naming guessable? D1 incon F1 3-nume CONFIRMED -> rar_env 3. Error messages actionable? D2 gap F2 gap CONFIRMED gap 4. Docs findable & complete? D4 gap F7 gap+ CONFIRMED gap 5. Back-compat safe? D3 resid F3 runtime CONFIRMED (1 rezidual) 6. Discoverability pre-send? D5 gap F4 gap CONFIRMED gap ``` Codex: indisponibil (N/A). DX scor initial: ~6/10 (model API solid + aditiv, dar nume inconsistent + eroare neimbogatita + zero discoverability + contract neactualizat). Tinta dupa fixuri: ~9/10. ### Jurnal integrator (condensat) | Etapa | Azi (plan brut) | Dupa fixuri DX | |---|---|---| | Afla env-urile contului | doar din eroare / dupa trimitere reala | `GET /v1/conturi/medii` | | Trimite | `rar_target` (nume #1) | `rar_env` (un nume) | | Confirma tinta fara trimitere reala | imposibil (valideaza nu ecou-ieste) | `/valideaza` ecou-ieste `rar_env` | | Eroare tinta indisponibila | proza, fara cod | `cod: RAR_MEDIU_INDISPONIBIL` + fix | | Citeste rezultatul | `rar_env` (nume #2) | `rar_env` (acelasi) | | Doc | contract fara field/endpoint | contract complet |