Files
rar-autopass/docs/prd/prd-5.6-observabilitate-jurnal.md
Claude Agent c842e3352a feat(5.6): observabilitate + jurnal aplicatie + lifecycle trimiteri blocate
Implementeaza PRD 5.6 complet (14 stories, TDD). Doua axe:

Lifecycle trimiteri blocate (Val A):
- submissions_admin.py: sterge/repune scoped (404 cross-account inaintea lui 409 stare)
- reactivare dedup peste `error` cu CAS (WHERE id=? AND status='error'), creds noi in
  submissions + accounts.rar_creds_enc; worker invalideaza sesiunea RAR la creds proaspete
  (JWT 30h vechi nu mai trimite cu parola gresita); camp aditiv `reactivated:true`
- retentie randuri blocate 30z; purge_expired exclude queued/sending; purge_after curatat
  la reactivare/requeue
- API DELETE /v1/prezentari/{id} + /repune (200+JSON); UI butoane + bulk + banner actionabil

Observabilitate:
- app/observ.py log_event: dublu canal app_events (DB) + RotatingFileHandler per-proces,
  redactare creds/PII la scriere (redact_pii/vin_partial)
- request_id middleware + X-Request-ID pe toate raspunsurile
- handler global excepții -> 500 envelope 6-chei + request_id (traceback doar in jurnal)
- audit cerere API (api_prezentari/api_auth_esuat) + audit worker (rar_login/tranzitii)
- tab "Jurnal" filtrabil scoped (non-admin doar contul sau); retentie jurnal 90z
- rar_error expus in GET /v1/prezentari/{id} (recovery observabil)

pytest -q: 741 passed, 0 failed. Docs: PRD raport VERIFY, contract endpointuri noi, ROADMAP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 18:45:39 +00:00

39 KiB

PRD 5.6 — Observabilitate, jurnal aplicatie & lifecycle trimiteri blocate

Stare: aprobat + review /autoplan complet (4 decizii de gust rezolvate 2026-06-23; vezi Anexa /autoplan)

Proces complet: docs/ROADMAP.md §5. Contract RAR (sursa de adevar): docs/api-rar-contract.md. Catalog erori (sursa de adevar coduri): app/errors.py (PRD 5.4). Redactare creds: app/security.py.

0. Context — de ce acum

Un client ROAAUTO (Visual FoxPro, MSXML2.ServerXMLHTTP) a primit "Internal Server Error" la POST /v1/prezentari si nu a existat niciun mod de a vedea ce s-a intamplat fara a citi traceback-ul brut din access-log-ul uvicorn (.run/api.log).

Cauza concreta a fost o cheie Fernet invalida in .env (AUTOPASS_CREDS_KEY), care arunca ValueError abia la primul encrypt_creds — un 500 brut, fara mesaj util pentru client si fara inregistrare la nivel de aplicatie. Cauza a fost reparata ca hotfix (cheie valida in .env + validare fail-fast la startup — crypto.validate_creds_key, apelata in main.lifespan; confirmat live HTTP 200).

Acest incident a expus trei goluri structurale, care sunt obiectul acestui PRD:

  1. Excepțiile interne devin 500 brut, fara traducere in contractul de erori pe 3 niveluri (PRD 5.4) si fara log cu context (request_id, ruta, cont).
  2. Nu exista jurnal de aplicatie la nivel de eveniment — doar access-log uvicorn (linii HTTP) + print(...) ad-hoc in worker. Nu se poate raspunde la "ce s-a intamplat cu cererea X / contul Y" fara grep prin traceback-uri.
  3. Nu exista monitorizare a fluxului de afaceri dincolo de /healthz + /metrics: incercari de login RAR, ciclul de viata al trimiterilor, erori din catalog.

0bis. Context — lacune de lifecycle (descoperite la testarea live)

La testul live pe RAR test au iesit la iveala doua probleme de lifecycle, confirmate in cod:

  • Trimiterile blocate sunt permanente. purge_after se seteaza DOAR la status='sent' (mark()), iar purge_expired sterge DOAR randuri sent expirate. Randurile error/ needs_data/needs_mapping nu primesc niciodata purge_afternu se purjeaza niciodata. Login 401 (creds RAR gresite) → error direct, fara retry (by design, ca sa nu blocheze contul) → randul ramane la nesfarsit in dashboard, fara cale de stergere prin UI/API/CLI (corectia inline US-010 e doar pentru needs_*, nu error).
  • Un rand error blocheaza retrimiterea aceluiasi payload. Cheia de idempotenta e hash de CONTINUT (vin+nr+data+odometru+prestatii+cont) — parola NU intra in cheie. Daca un client trimite cu parola gresita (→ error), apoi corecteaza parola si retrimite acelasi payload, primeste deduped: true cu status: error si prezentarea nu se mai trimite niciodata. Randul eronat "fura" cheia.

Reproductibil acum: submission 15 (din clientul VFP cu parola placeholder) e blocat pe error RAR_CREDS_INVALIDE; testul reusit (submission 16 → idPrezentare 68818) a necesitat un odometru diferit tocmai ca sa ocoleasca aceasta capcana.

1. Obiectiv

Un sistem de observabilitate coerent: orice eveniment relevant (cerere API, login RAR, tranzitie de trimitere, eroare) este inregistrat structurat intr-un tabel app_events (vizibil intr-un tab "Jurnal" din dashboard, filtrabil pe cont/tip/data) si intr-un log text pentru depanare low-level. Orice excepție neasteptata produce un raspuns pe 3 niveluri (PRD 5.4) in loc de 500 brut. PII si credentialele sunt redactate peste tot.

In plus, inchidem doua lacune de lifecycle descoperite la testarea live (vezi §0bis): trimiterile blocate (mai ales error din creds RAR gresite) sunt azi permanente, nu se pot sterge/re-pune in coada din interfata, blocheaza retrimiterea aceluiasi payload prin dedup si nu se purjeaza niciodata. Le facem gestionabile (sterge / re-pune in coada), deblocam dedup-ul si le aducem sub retentie.

2. Non-Goals (anti scope-creep)

  • Fara dependinte/infrastructura noi de observabilitate (Sentry, ELK, Loki, OpenTelemetry, Prometheus push). Ramanem pe SQLite + fisier + dashboard HTMX existent.
  • Fara alerting (email/SMS/webhook la eroare). notify_signup ramane singura notificare; alertarea = follow-up daca apare nevoia din uz real.
  • Nu schimbam contractul de erori (PRD 5.4). Pe partea de observabilitate doar observam si traducem 500-urile ramase. Partea de lifecycle (US-009+) adauga DOAR doua tranzitii noi controlate — error → queued (re-pune in coada) si stergere de randuri ne-sent — fara a atinge logica de trimitere a worker-ului.
  • Nu stergem/atingem randuri sent prin noile actiuni de lifecycle: sunt dovada de trimitere la RAR (audit). Stergerea/re-punerea opereaza DOAR pe error/needs_data/ needs_mapping; sending (in zbor) e protejat de lease-ul worker-ului.
  • Nu anulam nimic la RARFINALIZATA ramane terminal acolo (fara API de anulare). Stergem doar randul LOCAL din coada gateway-ului, nu inregistrarea de la RAR.
  • Nu modificam /healthz si /metrics (raman; jurnalul e complementar, nu inlocuitor).
  • Fara UI de configurare a logarii (nivel/retentie se seteaza din env, nu din web).
  • Nu logam corpuri de payload integral (PII vehicul/proprietar) — doar metadate + identificatori (submission_id, cont, cod eroare). VIN/nr se logheaza doar redactat/partial.

3. Stories atomice

US-000 (hotfix 500) e DEJA LIVRAT in afara procesului normal (vezi §0): cheie Fernet valida in .env + crypto.validate_creds_key() apelata in main.lifespan, confirmata live (POST VFP → 200, queued). Listata aici doar pentru trasabilitate; nu se re-executa.

US-001: Handler global de excepții → eroare 3 niveluri + log

Ca integrator ROAAUTO vreau ca orice eroare interna sa-mi intoarca un raspuns structurat (nu "Internal Server Error" gol) pentru ca sa stiu daca e problema mea (date) sau a gateway-ului, si sa pot raporta cu un identificator.

  • Depinde de: — (US-003 imbogateste logul; handlerul poate loga si simplu intai)
  • Fisiere: app/main.py (@app.exception_handler(Exception)), app/errors.py (cod nou EROARE_INTERNA), tests/test_error_handler.py
  • Test intai (RED): tests/test_error_handler.pytest_exceptie_neasteptata_da_500_structurat, test_raspuns_contine_request_id_fara_traceback, test_creds_nu_apar_in_raspuns
  • Acceptance criteria:
    • O excepție neprinsa pe orice ruta → HTTP 500 cu body {cod: "EROARE_INTERNA", problema, fix, request_id} (3 niveluri, PRD 5.4).
    • Body-ul NU contine traceback, mesaj de excepție brut, sau credentiale (scrub).
    • Traceback-ul complet + request_id + ruta + account_id se scriu in log (fisier) prin scrub (app/security.py), niciodata in raspuns.
    • Handlerele existente (LoginRequired/AdminRequired/CSRF/RequestValidationError) raman neatinse; doar Exception generic e nou.
  • Verificare E2E: forteaza o excepție (ex. cheie Fernet invalida pe o ruta de test) → raspuns JSON 3 niveluri cu request_id; traceback doar in log.

US-002: request_id per cerere (corelare)

Ca operator vreau un identificator unic pe fiecare cerere pentru ca sa pot lega raspunsul clientului de randul din jurnal si de traceback.

  • Depinde de: —
  • Fisiere: app/web/middleware.py (sau middleware in main.py), tests/test_request_id.py
  • Test intai (RED): tests/test_request_id.pytest_raspuns_are_header_x_request_id, test_request_id_propagat_in_log
  • Acceptance criteria:
    • Fiecare raspuns are header X-Request-ID (generat daca clientul nu trimite unul).
    • request_id e disponibil in handlerul de erori (US-001) si in logger (US-003) pe durata cererii (contextvar, fara a polua semnaturi).
    • Format opac, fara PII (ex. secrets.token_hex(8)).
  • Verificare E2E: doua cereri → doua X-Request-ID distincte, regasite in jurnal.

US-003: Logger structurat central (app/observ.py)

Ca dezvoltator vreau un singur punct prin care se emit evenimente pentru ca formatul, redactarea si dublul canal (DB + fisier) sa fie consistente si imposibil de ocolit.

  • Depinde de: US-002 (request_id), schema app_events (US-004)
  • Fisiere: app/observ.py (modul nou: log_event(...)), app/schema.sql (tabela app_events), app/db.py (helper insert + read paginat), tests/test_observ.py
  • Test intai (RED): tests/test_observ.pytest_log_event_scrie_in_db_si_fisier, test_log_event_redacteaza_pii_si_creds, test_nivel_filtrat_din_env
  • Acceptance criteria:
    • log_event(tip, *, nivel, account_id=None, cod=None, mesaj=None, context=None) scrie un rand in app_events SI o linie in log text (acelasi continut redactat).
    • Toate valorile trec prin scrub (app/security.py) inainte de persistare — parole/token-uri/rar_credentials***REDACTED***; VIN logat doar partial.
    • Nivelul minim e configurabil din env (AUTOPASS_LOG_LEVEL, default INFO).
    • Eroarea la scrierea jurnalului NU propaga (best-effort, ca notify_signup): o cadere a logului nu doboara cererea/worker-ul.
    • app_events: id, ts, request_id, account_id, sursa(api|worker), tip, nivel, cod, mesaj, context_json, purge_after.
  • Verificare E2E: apel log_event din shell → rand in DB + linie in fisier, ambele redactate.

US-004: Audit cerere API per cont

Ca operator vreau sa vad fiecare cerere /v1/* (cine, ce, rezultat) pentru ca sa pot diagnostica integrari (ex. clientul VFP) fara acces la server.

  • Depinde de: US-003
  • Fisiere: middleware/dependinta in app/api/v1/ (hook pe rutele v1), app/api/v1/router.py (evenimente la enqueue), tests/test_audit_api.py
  • Test intai (RED): tests/test_audit_api.pytest_post_prezentari_logheaza_eveniment_cont, test_eveniment_contine_status_si_count_fara_pii, test_401_logat_ca_auth_esuat
  • Acceptance criteria:
    • POST /v1/prezentari emite eveniment api_prezentari cu: account_id, nr. prezentari, distributie status rezultat (queued/needs_data/needs_mapping/deduped).
    • Esecurile de auth (401 cheie invalida/lipsa in prod) emit api_auth_esuat cu IP + prefix cheie (nu cheia intreaga).
    • Niciun camp de payload PII integral (doar count + statusuri + coduri).
  • Verificare E2E: POST ca VFP (cheie valida + invalida) → ambele apar in jurnal cu cont/rezultat.

US-005: Audit login RAR + ciclu de viata trimiteri (worker)

Ca operator vreau sa vad incercarile de login RAR si tranzitiile trimiterilor pentru ca "nu exista incercari de login vizibile" a fost o plangere directa.

  • Depinde de: US-003
  • Fisiere: app/worker/__main__.py (inlocuieste print(...) cu log_event), app/rar_client.py (eveniment login ok/esuat), tests/test_worker_observ.py
  • Test intai (RED): tests/test_worker_observ.pytest_login_reusit_logat, test_login_401_logat_fara_parola, test_tranzitie_sent_si_error_logate
  • Acceptance criteria:
    • Login RAR (reusit/esuat) → eveniment rar_login cu account_id, rezultat, cod HTTP; fara email/parola in clar (scrub).
    • Tranzitiile sending→sent / →needs_data / →error / reconciliere → evenimente cu submission_id, account_id, cod eroare din catalog.
    • print(...) existente din worker migrate la log_event (sursa=worker), fara a pierde mesajele in stdout (logul text ramane).
  • Verificare E2E: o trimitere live pe RAR test (--send) → rar_login ok + sent cu idPrezentare in jurnal.

US-006: Tab "Jurnal" in dashboard (admin, filtrabil)

Ca admin vreau sa vad jurnalul in dashboard pentru ca sa diagnostichez fara SSH.

  • Depinde de: US-003 (date), US-004/US-005 (continut util)
  • Fisiere: app/web/routes.py (ruta /_fragments/jurnal + tab), app/web/admin_routes.py (gating admin daca e global), app/web/templates/_jurnal.html, tests/test_web_jurnal.py
  • Test intai (RED): tests/test_web_jurnal.pytest_jurnal_doar_admin, test_filtru_pe_tip_si_data, test_non_admin_vede_doar_evenimentele_contului_sau
  • Acceptance criteria:
    • Tab "Jurnal" cu lista paginata: ts, sursa, tip, nivel, cont, cod, mesaj (redactat).
    • Filtre: tip eveniment, nivel, interval data, (admin) cont. Scoped: un cont non-admin vede DOAR evenimentele proprii (regula NULL→cont 1, ca restul UI-ului).
    • Stil consistent cu tabelele PRD 5.5 (grila .tablewrap), AA light+dark.
    • Fara expunere de creds/PII (rendare din campuri deja redactate la scriere).
  • Verificare E2E: browser HTMX pe / → tab Jurnal, filtrare pe rar_login/api_prezentari, scoping verificat cu 2 conturi.

US-007: Redactare PII/parole in jurnal (gard de siguranta)

Ca responsabil GDPR vreau garantia ca jurnalul nu scurge date sensibile pentru ca PII vehicul/proprietar + creds RAR nu au voie in loguri (L.142/GDPR).

  • Depinde de: US-003
  • Fisiere: app/security.py (extinde scrub/SENSITIVE_KEYS daca e nevoie), tests/test_jurnal_redactare.py
  • Test intai (RED): tests/test_jurnal_redactare.pytest_parola_niciodata_in_app_events, test_vin_logat_partial, test_payload_integral_nu_se_logheaza
  • Acceptance criteria:
    • Niciun rand app_events (sau linie fisier) nu contine password/token/ email creds in clar — verificat prin scanare la nivel de test.
    • VIN/nr inmatriculare se logheaza doar partial (ex. ultimele 4) sau hash scurt.
    • Test "fuzz": evenimente cu chei sensibile in context → toate mascate.
  • Verificare E2E: provoaca eroare cu creds reale → cauta parola in app_events + fisier → 0 hits.

US-008: Retentie / purjare jurnal (GDPR)

Ca responsabil GDPR vreau ca jurnalul sa se auto-stearga dupa o perioada pentru ca retentia PII e limitata (acelasi mecanism ca submissions/import_batches).

  • Depinde de: US-003
  • Fisiere: app/worker/__main__.py (purge_expired extins pe app_events), app/schema.sql (purge_after), tests/test_jurnal_retentie.py
  • Test intai (RED): tests/test_jurnal_retentie.pytest_app_events_primesc_purge_after, test_purjare_sterge_evenimente_expirate
  • Acceptance criteria:
    • Fiecare rand app_events primeste purge_after = now + 90 zile (AUTOPASS_LOG_RETENTION_DAYS, default 90 — decizie §5).
    • Purjarea orara existenta (T16) sterge si app_events expirate.
    • Logul text foloseste RotatingFileHandler in aplicatie (rotatie pe dimensiune, N fisiere de backup) — decizie §5; nu depindem de deploy pentru rotatie.
  • Verificare E2E: insereaza eveniment cu purge_after in trecut → rulează purjarea → dispare.

US-009: Backend — sterge + re-pune in coada randuri ne-sent (helper)

Ca operator vreau sa pot sterge sau re-pune in coada o trimitere blocata pentru ca un rand error (creds gresite) ramane altfel permanent si nereparabil.

  • Depinde de: —
  • Fisiere: app/submissions_admin.py (modul nou: delete_submission, requeue_submission), tests/test_submissions_admin.py
  • Test intai (RED): tests/test_submissions_admin.pytest_sterge_rand_error_scoped, test_nu_sterge_sent_sau_sending, test_repune_error_devine_queued_reset_retry, test_repune_re_ruleaza_classify , test_scope_cross_account_404
  • Acceptance criteria:
    • delete_submission(conn, account_id, sid) sterge randul DOAR daca e error/needs_data/needs_mapping SI apartine contului; altfel ridica/Intoarce refuz (sent/sending → interzis, cross-account → inexistent).
    • requeue_submission(conn, account_id, sid) muta error → queued, reseteaza retry_count=0, next_attempt_at=NULL, sending_since=NULL, re-ruleaza classify pe payload (poate ajunge needs_data/needs_mapping daca continutul cere).
    • Niciuna nu atinge sent (audit) sau sending (lease worker).
    • Ambele emit eveniment in jurnal (US-003): submission_sters / submission_repus.
  • Verificare E2E: helper apelat din shell pe submission 15 → queued; pe un sent → refuz.

US-010: API v1 — sterge + re-pune in coada

Ca integrator ROAAUTO vreau endpointuri pentru a curata/relua trimiteri blocate pentru ca softul propriu sa gestioneze coada fara interventie manuala in DB.

  • Depinde de: US-009
  • Fisiere: app/api/v1/router.py (DELETE /v1/prezentari/{id}, POST /v1/prezentari/{id}/repune), tests/test_api_lifecycle.py
  • Test intai (RED): tests/test_api_lifecycle.pytest_delete_scoped_pe_cheie, test_delete_sent_403, test_repune_error_queued, test_repune_inexistent_404
  • Acceptance criteria:
    • DELETE /v1/prezentari/{id}200 + body JSON {ok, submission_id, status_anterior} (NU 204; clienti VFP string-parse) pe randuri ne-sent ale contului cheii.
    • Scope evaluat INAINTEA starii (decizie /autoplan #20): cross-account / inexistent → 404 (acelasi mesaj, B3 — nu confirmam existenta); own-account sent/sending409 (conflict de stare). Test test_delete_cross_account_sent_404.
    • POST /v1/prezentari/{id}/repune → randul devine queued (peste helper US-009).
    • Scoped strict pe contul cheii API (nu se poate atinge alt cont).
  • Verificare E2E: cu cheia contului 2, POST .../15/repune → 200; worker il re-trimite (creds corecte).

US-011: Web dashboard — butoane Sterge / Re-pune in coada

Ca operator in dashboard vreau butoane pe randurile blocate pentru ca sa le gestionez vizual, fara API/SQL.

  • Depinde de: US-009
  • Fisiere: app/web/routes.py (rute POST /trimitere/{id}/sterge, POST /trimitere/{id}/repune, POST /trimiteri/sterge-bulk cu CSRF + PRG), app/web/templates/_trimitere_detaliu.html + lista Trimiteri (selectie), tests/test_web_lifecycle.py
  • Test intai (RED): tests/test_web_lifecycle.pytest_buton_sterge_doar_pe_blocate, test_repune_din_ui_scoped_sesiune, test_csrf_enforce, test_bulk_sterge_doar_blocate_scoped
  • Acceptance criteria:
    • Pe detaliul unui rand error: buton "Re-pune in coada" + "Sterge" cu dialog de confirmare simpla (decizie §5).
    • Pe lista Trimiteri: selectie multipla + "Sterge selectate" (bulk), pe modelul panoului admin (PRD 5.5); actioneaza DOAR pe randuri blocate ale contului.
    • Randuri sent/sending: fara butoane si neselectabile pentru stergere (read-only).
    • Scoped pe sesiune (regula NULL→cont 1); CSRF enforce; PRG dupa actiune.
    • Stil consistent cu corectia inline existenta + panoul admin bulk (PRD 5.5), AA light+dark.
  • Verificare E2E: browser HTMX → pe submission 15 "Re-pune in coada" → dispare din "error", reapare ca trimis dupa worker; "Sterge" → dispare din lista.

US-012: Dedup nu mai e blocat de un rand error

Ca integrator vreau ca o retrimitere a aceluiasi payload (dupa ce am corectat parola) sa fie acceptata pentru ca azi un rand error cu aceeasi cheie o blocheaza tacit.

  • Depinde de: — (atinge app/api/v1/router.py enqueue + import_router)
  • Fisiere: app/api/v1/router.py (create_prezentari), app/api/v1/import_router.py (commit, daca aplica), tests/test_dedup_error.py
  • Test intai (RED): tests/test_dedup_error.pytest_resubmit_peste_error_reactiveaza, test_resubmit_actualizeaza_creds_pe_reactivare, test_resubmit_peste_sent_ramane_deduped, test_resubmit_peste_queued_ramane_deduped
  • Acceptance criteria:
    • La enqueue, daca randul existent cu aceeasi idempotency_key e error: se RE-ACTIVEAZA acelasi rand (re-ruleaza classify, actualizeaza rar_creds_enc cu creds-urile noi din cerere, reset retry_count/next_attempt_at, purge_after=NULL), si raspunsul poarta camp aditiv reactivated: true + starea noua (ex. queued); deduped ramane cu semantica actuala (decizie /autoplan #19, NU se repurpose-aza).
    • Reactivarea e un UPDATE compare-and-swap (WHERE id=? AND status='error'); daca rowcount==0 (alt POST/requeue a schimbat starea intre timp) -> raspuns dedup pe starea curenta. Worker-ul invalideaza sesiunea RAR cache-uita a contului cand randul claim-uit poarta rar_creds_enc != NULL (altfel JWT vechi 30h trimite cu parola gresita — vezi T1 anexa).
    • Creds noi se propaga si in accounts.rar_creds_enc (canal web durabil, decizie #17).
    • Pentru sent/queued/sending: comportament neschimbat → deduped: true (nu cream dubluri, nu deranjam in-flight/trimise).
    • needs_data/needs_mapping: raman deduped la resubmit (decizie §5) — corectia se face exclusiv prin UI (corectia inline existenta), nu prin re-trimiterea payload-ului.
    • Invariantul UNIQUE(idempotency_key) ramane (re-folosim randul, nu inseram al doilea).
  • Verificare E2E: POST cu parola gresita → error; re-POST acelasi payload cu parola corecta → queued (nu deduped); worker trimite → sent.

US-013: Retentie / purjare randuri ne-sent blocate

Ca responsabil GDPR vreau ca si trimiterile blocate sa se auto-stearga dupa o perioada pentru ca altfel PII-ul lor ramane permanent (azi doar sent se purjeaza).

  • Depinde de: —
  • Fisiere: app/worker/__main__.py (mark seteaza purge_after si pe stari blocate; purge_expired extins pe error/needs_*), tests/test_purge_blocate.py
  • Test intai (RED): tests/test_purge_blocate.pytest_error_primeste_purge_after, test_purjare_sterge_error_expirat, test_sent_si_blocate_retentii_separate_daca_difera
  • Acceptance criteria:
    • Randurile care intra in error/needs_data/needs_mapping primesc purge_after (AUTOPASS_BLOCKED_RETENTION_DAYS, default 30 zile — decizie §5, mai scurt decat 90z sent).
    • Purjarea orara (T16) sterge si randurile blocate expirate, nu doar sent.
    • O re-activare (US-012) / re-pune in coada (US-009) reseteaza/curata purge_after (randul redevine activ, nu mai e candidat la purjare imediat).
  • Verificare E2E: rand error cu purge_after in trecut → rulează purjarea → dispare.

Ca operator vreau ca avertismentul de trimiteri blocate sa-mi spuna CARE prezentare a esuat si sa ma duca la ea pentru ca azi arata doar un contor ("Eroare la trimitere (1)"), fara VIN/id/link — nu pot actiona, iar banner-ul nu se stinge niciodata cat timp exista error.

  • Depinde de: — (UI peste filtrul existent /_fragments/submissions?status=); se imbina natural cu US-011 (butoane sterge/re-pune in coada) si US-013 (purjare → banner se stinge)
  • Fisiere: app/web/templates/_status.html, app/web/routes.py (_render_status/ fragment status — expune si identificatorii randurilor blocate, nu doar contoare), tests/test_web_status_fragment.py
  • Test intai (RED): tests/test_web_status_fragment.pytest_categorie_blocata_linkeaza_la_trimiteri_filtrate, test_status_arata_identificator_rand_blocat, test_scoped_pe_cont
  • Acceptance criteria:
    • Fiecare categorie din "Necesita atentia ta" e link catre lista "Trimiteri" filtrata pe acea stare (deep-link ?tab=...&status=error etc.), scoped pe cont.
    • Sub fiecare categorie se afiseaza identificatorul randurilor blocate (VIN partial
      • nr inmatriculare + #id), cel putin pentru primele N, cu "...si inca M" daca sunt mai multe.
    • Banner-ul dispare cand nu mai exista randuri blocate (consecinta US-009/011/013: stergere / re-pune in coada / purjare → contor 0 → sectiunea nu se mai randeaza).
    • Nimic nou expus fara scope (regula NULL→cont 1); PII doar partial (ca jurnalul, US-007).
  • Verificare E2E: browser HTMX → "Eroare la trimitere (1)" arata #15 WVW…0001 / B123ABC si linkeaza in Trimiteri filtrat pe error; dupa re-pune in coada + sent, banner-ul dispare.

4. Riscuri

  • Scriere DB pe calea fiecarei cereri (US-004) poate adauga latenta/contentie pe SQLite (WAL). Mitigare: insert minimal, best-effort, nivel filtrat; eveniment per cerere agregat (1 rand), nu per camp. De masurat la VERIFY.
  • Scurgere PII e riscul central. Mitigare: redactare la SCRIERE (nu la afisare), testata adversarial (US-007); nimic din payload integral nu intra in jurnal.
  • Volum jurnal poate umfla DB-ul. Mitigare: retentie + nivel (US-008), INFO default.
  • Dublu canal divergent (DB vs fisier). Mitigare: un singur log_event ca sursa unica (US-003), ca dry-run/erori la PRD 5.2/5.4 — imposibil de divergat.
  • Migrare schema app_events. Mitigare: migrare defensiva idempotenta in _migrate (ca accounts.active/override_json).
  • US-012 schimba semantica deduped la enqueue. Risc: re-activare nedorita a unui rand trimis. Mitigare: re-activarea e strict pe error (nu sent/queued/sending); teste explicite pe fiecare stare; UNIQUE(idempotency_key) garanteaza un singur rand per continut.
  • Re-pune in coada cu creds gresite (US-009/010/011): daca creds-urile contului sunt inca gresite, randul re-intra in error. Acceptat — actiunea nu garanteaza succesul, doar reda dreptul la o noua incercare; jurnalul (US-005) arata de ce a reesuat.

5. Decizii (rezolvate cu utilizatorul — poarta de aprobare PRD)

Rezolvate 2026-06-23. Sunt obligatorii pentru executie.

  • Retentie jurnal: 90 zile (aliniat cu submissions/import_batches). [US-008]
  • Tipuri de evenimente: lista extensibila, nu fixata acum — tip e text liber documentat, adaugam tipuri pe parcurs fara migrare. [US-003]
  • Log text: RotatingFileHandler in aplicatie (rotatie pe dimensiune; nu depindem de deploy). [US-008]
  • Vizibilitate jurnal: non-admin vede DOAR evenimentele contului sau; adminul vede tot, cu filtru pe cont. [US-006]
  • Resubmit peste blocate (US-012): doar error se re-activeaza (re-ruleaza classify
    • actualizeaza creds). needs_data/needs_mapping raman deduped — corectia exclusiv prin UI (corectia inline existenta). sent/queued/sending raman deduped (neschimbat).
  • Retentie randuri blocate (US-013): 30 zile (mai scurt decat cele 90 ale sent; un blocat n-are valoare de audit ca o trimitere reusita). Configurabil prin AUTOPASS_BLOCKED_RETENTION_DAYS, default 30.
  • Stergere din UI (US-011): confirmare simpla (dialog) + actiune in bloc pe lista (selectie multipla + "Sterge selectate"), pe modelul panoului admin (PRD 5.5).

6. Valuri de executie (graful de dependente)

Val 1: [US-002 request_id] [US-003 logger+schema]   ← fundatii, fisiere disjuncte → paralel
Val 2: [US-001 handler 500] [US-004 audit API] [US-005 audit worker] [US-007 redactare]
                                                     ← deblocate de US-003 (+US-002)
Val 3: [US-006 tab Jurnal] [US-008 retentie]         ← consuma datele/coloanele din Val 1-2

--- Lifecycle trimiteri blocate (independent de observabilitate; poate rula in paralel) ---
Val A: [US-009 helper sterge/repune] [US-012 dedup peste error] [US-013 retentie blocate]
                                                     ← fisiere disjuncte, fara dependente
Val B: [US-010 API lifecycle] [US-011 UI lifecycle] [US-014 banner actionabil]
                                                     ← deblocate de US-009 (US-014 indep., dar grupat cu UI)

Nota scope: 5 stories de lifecycle (US-009..US-013) in loc de 3 din schita initiala — regula proiectului separa backend + UI in stories distincte (helper / API / UI). Daca vrei livrare mai mica, US-010 (API) e optional pentru un MVP "doar dashboard".


Anexa /autoplan — Raport de review (2026-06-23)

Generat de /autoplan (CEO -> Design -> Eng -> DX), commit f48346d, branch main. Voci: Claude subagent per faza + Codex. Codex INDISPONIBIL (usage limit la runtime) -> toate fazele ruleaza [subagent-only]. Premisa "app_events table + tab Jurnal" confirmata de utilizator la poarta de premise (vs alternativa stdout-first). Restore point: vezi comentariul HTML din capul fisierului.

Consensus tables (Codex = N/A, subagent-only)

CEO:    1 premise flagged (substrate, CONFIRMAT keep) · 3 right-problem/scope · 4 alt-uri necomparate
DESIGN: 3 high (poll vs select, deep-link inexistent, banner->panel) · stari lipsa
ENG:    2 CRITICAL (US-012 race+JWT stale, purge_after) · 0 concurrency tests · WAL contention
DX:     5 high (500 envelope 6 chei, 403/404 oracle, deduped breaking, docs, rar_error allowlist)

Diagrame

US-012 reactivare error — masina de stari + cursa (fix necesar T1):

  POST /v1/prezentari (acelasi payload, parola corectata)
        |
        v
  SELECT status WHERE idempotency_key=?  ---- error ----> UPDATE ... SET status='queued',
        |                                                  rar_creds_enc=<nou>, retry=0,
        | sent/queued/sending/needs_*                       next_attempt_at=NULL,
        v                                                   purge_after=NULL
   deduped:true (neschimbat)                                      |
                                                                  v
   CURSA (fara CAS):  worker.claim_one (BEGIN IMMEDIATE) queued->sending
   CURSA (JWT):       AccountSessions[account_id] are token vechi (30h) din creds GRESITE
                      -> trimite cu parola veche, ignora corectia  <-- BUG CENTRAL
   FIX: UPDATE ... WHERE id=? AND status='error' (CAS; rowcount 0 -> deduped) +
        la claim, daca randul poarta rar_creds_enc != NULL -> sessions.invalidate(account_id)

Retentie / purjare (fix T2):

  mark(sent)     -> purge_after = now + 90z   (existent)
  mark(blocate)  -> purge_after = now + 30z   (US-013 nou; error/needs_data/needs_mapping)
  reactivare/    -> purge_after = NULL        (US-009/012; ALTFEL purjat inainte de claim)
   re-pune coada
  purge_expired WHERE purge_after<now AND status IN ('sent','error','needs_data','needs_mapping')
                EXCLUDE explicit 'queued'/'sending'

Failure Modes Registry (noi, din review)

  CODEPATH                         | FAILURE MODE                  | RESCUED? | TEST? | USER SEES        | LOGGED?
  ---------------------------------|-------------------------------|----------|-------|------------------|--------
  create_prezentari reactivare     | cursa cu claim_one / 2x POST  | FIX T1   | FIX T3| queued det.      | US-004
  worker JWT cache dupa creds noi  | trimite cu parola veche       | FIX T1   | FIX T3| ramane error     | US-005  <- CRITICAL
  reactivare fara purge_after=NULL | purjat inainte de claim       | FIX T2   | FIX T3| dispare tacit    | US-005  <- CRITICAL
  log_event own-conn pe hot path   | WAL write-lock pana la 15s    | FIX T4   | da    | latenta POST     | -
  RotatingFileHandler 2 procese    | rotatie rename race           | FIX T5   | n/a   | log corupt       | -
  500 envelope 4 chei              | parser client crapa pe 5xx    | FIX T7   | da    | KeyError client  | US-001
  403 sent vs 404 cross-acct       | oracol de existenta           | FIX TD2  | da    | leak             | US-004
  bulk select vs poll 15s          | selectie stearsa mid-actiune  | FIX T12  | da    | frustrare        | -
  deep-link status inexistent      | banner duce la lista nefiltr. | FIX T13  | da    | dead-end         | -

Decision Audit Trail (auto-decis cu cele 6 principii)

# Faza Decizie Clasificare Principiu Rationament
1 CEO Premisa app_events table + tab GATE (user) - Confirmat de utilizator: web-visibility e scop de produs (operator fara SSH)
2 Eng US-012 = CAS guarded + invalidare sesiune worker la creds noi (T1) Mechanical P1 completeness Bug central; fara el US-012 nu-si atinge scopul
3 Eng reactivare/requeue purge_after=NULL; purge exclude queued/sending (T2) Mechanical P1 Altfel randul reactivat e purjat tacit
4 Eng teste concurenta + purge-before-claim (T3) Mechanical P1 well-tested Lista de teste US-012 era single-thread
5 Eng log_event(conn opt) reuse hot-path (T4) Mechanical P3 pragmatic Evita contentie WAL
6 Eng log-uri per-proces api.log/worker.log (T5) Mechanical P5 explicit RotatingFileHandler nu e multiproces-safe
7 Eng vin_partial() + context curat (T6) Mechanical P1 scrub() nu acopera VIN (US-007)
8 DX EROARE_INTERNA in CATALOG; 500 = 6 chei + request_id (T7) Mechanical P1 Contract 6 chei (PRD 5.4)
9 DX X-Request-ID pe TOATE raspunsurile (T8) Mechanical P1 Corelare si pe 422/401/404
10 DX rar_error in _PREZENTARE_FIELDS (T9) Mechanical P6 action Recovery API observabil fara dashboard
11 DX update api-rar-contract.md + reconcile de-scope (T10) Mechanical P1 Sursa de adevar trebuie sa includa endpointurile noi
12 DX DELETE -> 200+JSON, nu 204 (T11) Mechanical P5 Consistent cu restul v1; clienti VFP string-parse
13 Design poll vs bulk-select rezolvat (T12) Mechanical P1 Selectie stearsa la 15s = defect
14 Design plumbing deep-link status (T13) Mechanical P1 Destinatia US-014 nu exista azi
15 Design banner -> panou detaliu (T14) Mechanical P3 Duce direct la butonul de actiune
16 Design stari empty/loading/partial + collision checkbox (T15) Mechanical P1 Acoperire stari = scope, nu afterthought
17 CEO REZOLVAT: DA — resubmit/requeue cu creds noi reimprospateaza si accounts.rar_creds_enc (T16) Taste P1 Utilizator: ambele canale converg pe parola corectata
18 CEO REZOLVAT: pastram bundled, lifecycle (Val A) PRIMUL Taste P6 Utilizator: §6 izoleaza deja valurile; overhead minim pe PRD aprobat
19 DX REZOLVAT: camp aditiv reactivated:true (NU repurpose deduped) Taste P5 Utilizator: backward-compat pentru clienti care testeaza deduped
20 DX REZOLVAT: cross-account 404 INAINTE de verificare status; own-account sent/sending -> 409 Taste(sec) P1 Utilizator: inchide oracolul de existenta (B3)

Decizii /autoplan rezolvate la poarta finala (2026-06-23, obligatorii pentru executie)

  • Bundling [#18]: PRD 5.6 ramane unitar; ordinea de executie pune Val A (lifecycle: US-009/012/013/011/014) inaintea observabilitatii. Un singur VERIFY.
  • US-012 raspuns [#19]: la reactivarea unui rand error se intoarce camp aditiv reactivated: true pe SubmissionResult (NU se repurpose-aza deduped). deduped ramane cu semantica actuala; clientii vechi nu se sparg. Update app/models.py + contract.
  • US-010 coduri [#20]: scope-ul (cross-account) se evalueaza inaintea starii. Cross-account / inexistent -> 404 (acelasi mesaj, B3). Own-account sent/sending -> 409 (conflict de stare, nu 403). Test nou test_delete_cross_account_sent_404.
  • US-009/012 creds [#17]: cand resubmit/requeue aduce creds noi, se reimprospateaza si accounts.rar_creds_enc (canalul web durabil), nu doar submissions.rar_creds_enc. Combinat cu invalidarea sesiunii worker (T1).

Implementation Tasks (auto-generate, vezi JSONL ~/.gstack/projects/romfast-rar-autopass/)

P1 (blocheaza ship): T1 (US-012 CAS+sesiune), T2 (purge_after), T3 (teste concurenta), T4 (log_event conn), T5 (log per-proces), T7 (500 6-chei), T8 (X-Request-ID global), T9 (rar_error allowlist), T10 (docs contract), T12 (poll vs select), T13 (deep-link). P2 (acelasi branch): T6 (vin_partial), T11 (DELETE 200+body), T14 (banner->panou), T15 (stari UI), T16 (creds web).

Completion Summaries

  CEO     | premise 1 (confirmat keep) · right-problem OK (lifecycle=10x) · 1 challenge bundling · F6 creds web
  DESIGN  | 3 high (poll/select, deep-link, banner) · stari lipsa · checkbox collision · AA de verificat
  ENG     | 2 CRITICAL (race+JWT, purge) · 0 concurrency tests · WAL contention · IDOR ordine 404
  DX      | 5 high (500 envelope, oracle, deduped, docs, rar_error) · recovery matrix per-stare de documentat
  Lake    | toate auto-deciziile au ales optiunea completa (16/16 mechanical = ADD/fix complet)

Raport VERIFY

Executie completa 2026-06-23 (TDD, RED->GREEN per story). Toate cele 14 stories livrate.

Rezultat teste

python3 -m pytest -q -> 741 passed, 0 failed (~64s). Baseline inainte de 5.6: 561 teste (restul de 114 "esecuri" de la pornire erau artefact de mediu — .env-ul de testare live are AUTOPASS_REQUIRE_API_KEY=true; rulat cu override-urile standard de test, baseline-ul e verde). Teste noi adaugate (toate verzi):

  • US-001 tests/test_error_handler.py (5) — 500 structurat 6-chei + request_id, fara traceback/creds.
  • US-002 tests/test_request_id.py (4) — X-Request-ID pe toate raspunsurile, contextvar.
  • US-003 tests/test_observ.py (4) — dublu canal DB+fisier, redactare, nivel din env, best-effort.
  • US-004 tests/test_audit_api.py (3) — api_prezentari (count+distributie), api_auth_esuat (IP+prefix).
  • US-005 tests/test_worker_observ.py (3) — rar_login ok/esuat fara parola, tranzitii sent/error.
  • US-007 tests/test_jurnal_redactare.py (4) — parola/token/VIN niciodata integral; fuzz chei sensibile.
  • US-006 tests/test_web_jurnal.py (5) — scope non-admin/admin, filtru tip/nivel/cont, deep-link tab.
  • US-008 tests/test_jurnal_retentie.py (5) — purge_after pe app_events, purjare, RotatingFileHandler.
  • US-009 tests/test_submissions_admin.py (6) — sterge/repune scoped, 404 cross-account, classify la repune.
  • US-010 tests/test_api_lifecycle.py (7) — DELETE/repune 200+JSON, scope-before-state (404 vs 409).
  • US-011 tests/test_web_lifecycle.py (7) — butoane doar pe blocate, CSRF, bulk scoped.
  • US-012 tests/test_dedup_error.py (5) — reactivare peste error + reactivated:true, creds noi; sent/queued/needs_* raman deduped.
  • US-013 tests/test_purge_blocate.py (5) — purge_after pe blocate (30z), purjare exclude queued/sending.
  • US-014 tests/test_web_status_fragment.py (+3) — categorie linkeaza la lista filtrata, identificator partial, scope.

Fix-uri tehnice cheie (din /autoplan)

  • T1 (CRITICAL): reactivarea e UPDATE compare-and-swap (WHERE id=? AND status='error'); worker-ul invalideaza sesiunea RAR cache-uita cand randul claim-uit poarta rar_creds_enc != NULL (JWT vechi 30h din parola gresita nu mai trimite). Creds noi se propaga si in accounts.rar_creds_enc.
  • T2: reactivare/requeue seteaza purge_after=NULL; purge_expired exclude explicit queued/sending.
  • T7: 500 = envelope 6-chei (catalog) + request_id. T8: X-Request-ID pe TOATE raspunsurile (middleware).
  • T9: rar_error in allowlist-ul GET /v1/prezentari/{id} (recovery observabil; test vechi actualizat).

Note

  • Teste modificate intentionat (comportament schimbat de PRD): test_t16_purjare (error primeste acum purge_after — US-013), test_get_scope_prezentari (rar_error expus acum — T9).
  • E2E live pe RAR test: NEPROBAT in aceasta sesiune (necesita creds RAR test + --send). Backend-ul de trimitere e neatins ca logica; modificarile worker sunt aditive (evenimente + invalidare sesiune la creds noi). Recomandat la deploy: o trimitere --send pentru a confirma rar_login ok + submission_sent in jurnal.