Recapitulare incident 2026-07-09: deploy Error la 0s = interpolare env
(variabile ${VAR:?} nesetate in Dokploy), apoi 404 din volum SQLite
detinut de root fata de userul app uid 10001.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8.2 KiB
CLAUDE.md
Ghid Claude Code pentru acest repo.
Ce este
Gateway web (Python/FastAPI) care preia prezentari service-auto si le declara la RAR AUTOPASS (L.142/2023, OM 210/2024). Inlocuieste integrarea VFP RarAutoPass (ROAAUTO), arhivata in legacy-vfp/ (doar referinta).
Stil
- Limba proiectului: romana (cod, comentarii, commit-uri, docs). Fara emoji.
- Comentarii scurte, strict functionale. Doar ce nu reiese din cod. FARA referinte la PRD-uri, stories (US-xxx), review-uri sau istoricul deciziei.
Surse de adevar
docs/api-rar-contract.md— contractul RAR; la divergenta, contractul are dreptate.docs/ROADMAP.md— progres + proces de lucru; sesiunile noi pornesc de aici. Se modifica doar "Stadiu Implementare"; detaliile stau indocs/prd/.
Comenzi
pip3 install -r requirements.txt # Python 3.12+
# Dev: API + worker = procese SEPARATE
uvicorn app.main:app --reload --port 8010 # dashboard /, /docs, /healthz, /metrics
python3 -m app.worker # proceseaza coada
# start.sh = mediu (test/prod) + rol (api/worker/both/finalizate); loguri in .run/
./start.sh test both --send # trimite efectiv la RAR test
./start.sh test finalizate # listeaza prezentarile inregistrate la RAR
./start.sh status | stop
./start-test.sh / ./start-prod.sh # fixeaza mediul, forwardeaza rolul
# Teste (TestClient + SQLite temporar); testele live RAR sunt skip implicit (marker `live`)
python3 -m pytest -q
python3 -m pytest tests/test_worker_reconcile.py::test_x -q
AUTOPASS_LIVE_RAR=1 python3 -m pytest tests/test_live_rar.py -q # LIVE opt-in: creeaza FINALIZATA; cere settings.xml cu creds <test>
# Chei API (admin, doar CLI); cheia rfak_... se afiseaza O SINGURA DATA
python3 -m tools.apikey create --account 2 | list | rotate | revoke
docker compose up --build # deploy: api + worker + autoheal, acelasi image + volum SQLite
# prod = Dokploy cu autodeploy la push; reguli in docs/deploy-dokploy.md
Arhitectura
Doua procese peste acelasi SQLite (WAL), comunica EXCLUSIV prin tabela submissions:
- API (
app.main:app) — API v1 (app/api/v1/), dashboard HTMX (app/web/+templates/),/healthz,/metrics. Worker-ul NU ruleaza aici: un worker mort nu trebuie sa lase containerul "sanatos". - Worker (
app/worker/__main__.py) — bucla: heartbeat → recuperare orfane → claim atomic (BEGIN IMMEDIATE) → login RAR per cont →postPrezentare→ update. Retry/backoff exponential, reconciliere anti-duplicat, lease pesendingorfane, re-login la JWT expirat (TTL 30h). Send OFF implicit (AUTOPASS_WORKER_SEND_ENABLED=false).
Doua canale de intrare in coada: POST /v1/prezentari (ROAAUTO/soft propriu) si import web xlsx/csv → mapare coloane → preview → commit (app/import_parse.py, import_router.py).
Flux: validare (validation.py) → mapare operatie→cod (mapping.py) → enqueue cu PII criptat → worker trimite → dashboard live.
Invariante critice
AUTOPASS_CREDS_KEYidentica intre API si worker (Fernet,crypto.py): chei diferite → worker nu decripteaza creds → trimiterile esueaza.start.sh bothgenereaza cheie efemera partajata; in prod, cheie persistenta in.env.- Idempotenta = hash de continut canonic server-side (
idempotency.py) — RAR accepta duplicate, fara nr. comanda.build_keynormalizeazaaccount_idlaaccount_or_default(None == 1) INAINTE de hash, altfel acelasi rand primeste chei diferite pe API vs import.canonicalize_rownormeaza VIN/nr/odometru (strip ".0" din Excel) inainte de validare si cheie. FINALIZATAe terminal la RAR (fara anulare/corectie). Pe eroare ambigua (timeout/TransportError/502/503/504/429/408) sausendingorfan, worker-ul cauta in finalizate (vin+dataPrestatie+odometruFinal) si marcheazasentfara re-trimitere (reconcile.py). EXCEPTIE: RAR 500 cu mesaj (RarError.rar_message, ex.ORA-12899) = esec DEFINITIV → fara reconciliere/retry,errorcu mesajul RAR (RAR_EROARE_SERVER); altfel ar marca falssentun record PARTIAL lasat de RAR (ne-tranzactional).- Creds RAR per cont: durabile in
accounts(canal web, fallback re-login) SAU efemere insubmissions.rar_creds_enc(canal API, sterse dupa primul login reusit). Worker incearca submission-ul intai, apoi contul. Purjarea sterge DOAR creds efemere dinsubmissions. - Auth API-key (
auth.py): identifica contul ROAAUTO (separat de creds RAR); stocam doar SHA-256.AUTOPASS_REQUIRE_API_KEY:false(dev) → fara cheie = cont id=1, cheie invalida → 401;true(prod) → obligatorie pe/v1/*protejat. POST-urile, importul si GET-urile de listare (API + fragmente dashboard) sunt account-scoped, 404-before-leak pe id strain.GET /v1/nomenclatorpublic intentionat (coduri RAR publice, fara PII). - Admin bootstrap: primul user din TOATA baza la
/signup(count_admins()==0, in tranzactie) devineis_admin=1; restul nu.is_adminda acces la/admin(panou global) si e independent de contul id=1 (acela e doar fallback dev pentru API neautentificat). Fix manual:python3 -m tools.account set-admin --account N. - Mapare coloane memorata per
(account_id, signature_coloane)(column_mappings): reaplicata automat la fisiere cu aceleasi coloane; un cont poate avea mai multe formate. - Mapare operatie→cod: prestatia vine cu
cod_prestatie(cod RAR) saucod_op_service+denumire. Nerezolvat →needs_mapping(nu se trimite), editor web cu sugestie fuzzy; salvarea maparii re-rezolva automat submission-urile blocate. - Excludere de la declarare (
operations_mapping.exclus=1; optiunea "Nu se declara la RAR" = sentinel__NEDECLARAT__in select-urile de mapare): operatia NU pleaca la RAR. Precedenta inresolve_prestatii: cod explicit valid pe rand > exclus > mapare exacta > reguli text. Itemii adnotatiexclussunt scosi din payload/cheie cusplit_prestatii_excluseinainte de enqueue; rand de import cu toate operatiile excluse → stareexcluded("Nedeclarat", nu se comite); submission cu toate excluse →needs_datacu motiv explicit.save_mappingpeste o regula exclusa reseteazaexclus=0. cod_prestatieVALIDAT fata de nomenclator la ingestie (resolve_prestatii(..., valid_codes)): cod necunoscut NU se trimite raw — e promovat lacod_op_service(denumire=cod) si intra la mapat. Motiv (confirmat live): RAR accepta doar coduri din nomenclator (max 5 car.); cod necunoscut → HTTP 500ORA-12899, iar RAR ne-tranzactional lasa record PARTIALFINALIZATApe care reconcilierea l-ar marca falssent. La cod nemapat:on_unmapped_error(boolean top-level pePOST /v1/prezentari+/valideaza) =false→ editor/needs_mapping;true→ respins fara enqueue (submission_id=null+erori). Precedenta: cerere >accounts.on_unmapped_error_default>false.- WAF RAR da 403 fara User-Agent de browser — httpx trimite mereu
User-Agent: Mozilla/5.0(config.py). - 422 fara echo de credentiale: handler-ul din
main.pypastreaza type/loc/msg dar DROP-ainput/ctx(altfel reflectarar_credentials.password). - Retentie:
submissionssent +import_batches→purge_after = now + 90 zile; worker purjeaza orar (GDPR/L.142).
Masina de stari submissions
queued → sending → sent (cu id_prezentare de la RAR). Ramuri: needs_mapping, needs_data (RAR 400), error (max retries / 4xx nerecuperabil / RAR 500 cu mesaj / creds invalide sau login 401 — fara retry pe creds gresite). Backoff: next_attempt_at = now + base*2^retry, plafonat. Schema: app/schema.sql.
Mod non-interactiv
Vezi /workspace/CLAUDE.md: cu claude -p, fisierele noi merg DOAR in /workspace/.claude-work/<task>/; modificarile la fisiere existente raman in locatia originala.
Skill routing
Cand cererea se potriveste unui skill, invoca-l prin Skill tool; la indoiala, invoca-l.
- Idei produs → /office-hours · Strategie → /plan-ceo-review · Arhitectura → /plan-eng-review
- Design system/plan → /design-consultation, /plan-design-review · Pipeline complet → /autoplan
- Bug-uri → /investigate · QA site → /qa, /qa-only · Code review → /review · Polish vizual → /design-review
- Ship/deploy/PR → /ship, /land-and-deploy · Salvare/reluare context → /context-save, /context-restore · Spec → /spec