Implementare completa U5 din plan-treapta2.md (sectiunea 13):
- _upload.html: drop zone + buton accesibil (a11y: drag nu e la tastatura),
drag-and-drop JS, mesaj 'NU se trimite nimic pana confirmi',
selector foi pt multi-sheet xlsx, stari eroare/mesaj
- _mapcoloane.html: formular mapare coloane cu .maprow/.mapcol.grow,
sugestii fuzzy pre-selectate, etiichete <label> vizibile, sample values,
format data configurabil
- _preview_import.html: tabel 6 stari, pills rezumat, filtre pe stare,
.chk per-rand pe needs_review (D11), banner declarant .banner.warn
direct deasupra input-ului N (D12), bara confirmare sticky,
text 'dubla cu randul N' pe duplicate_in_file (D10 daltonism),
link export CSV randuri esuate
- base.html: .s-needs_review (warn), .s-already_sent/.s-duplicate_in_file
(muted), .drop-zone, .banner.warn, .sticky-bar, .htmx-indicator
- routes.py: rute /_import/upload/mapare-coloane/preview/reset/confirma;
helper _web_compute_preview refoloseste _resolve_row_for_preview,
_already_sent_lookup, _signature din import_router (fara a-l edita);
commit ON CONFLICT DO NOTHING (TOCTOU); log atestare
- tests/test_import_ui.py: 15 teste (dashboard, upload, mapare, preview,
confirmare N corect/gresit, reset, erori, multi-sheet, a11y D10/D11/D12)
279 teste total, 0 esecuri.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- mark(sent): seteaza purge_after = now + 90 zile (GDPR/L.142)
- purge_expired(conn): sterge submissions sent expirate + import_batches expirate
(import_rows via ON DELETE CASCADE). NULL purge_after = nu expira.
- run(): tick de purjare odata pe ora (guard _last_purge_time + _PURGE_INTERVAL_S)
NU mai agresiv, nu blocheaza trimiterea
- 8 teste: purge_after la sent, alte stari fara purge, expirati vs neexpirat,
queued neatins, cascade import_rows, null purge_after pastrat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dashboard-ul incarca htmx din unpkg.com. Gateway-ul ruleaza offline (LXC/VPS
+ Cloudflare Tunnel); cand CDN-ul e blocat/inaccesibil, htmx nu se incarca si
dashboard-ul ramane static: zero polling (banner alerta blocate, coada,
mapari), butoanele de mapare nu trimit. Aceeasi decizie offline-first ca fontul
UI (FINDING-002, fara dependinta CDN).
Fix: vendorizez htmx.org@1.9.12 minificat in app/web/static/, montez StaticFiles
la /static in main.py, base.html refera /static/htmx.min.js. Verificat live:
window.htmx.version=1.9.12, fragment polls 200, zero regresie vizuala.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Link-ul 'tot' era criptic. 'export CSV: trimise' / 'toate' spune ce descarca
fiecare (don't-make-me-think).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tabelul submissions (7 coloane, 591px) iesea din card pe 390px -> toata pagina
scrolla orizontal (docScrollW 636 > 390). Wrap in .tablewrap{overflow-x:auto} +
white-space:nowrap pe celule -> scroll IN card, pagina nu mai deborda. tabular-nums
pe coloanele numerice.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
T5 (tools/import_dbf.py): citire prestatii_rar.DBF / mapare_prestatii.DBF cu
dbfread, raport dry-run (randuri valide/duplicate/goale, mapari orfane = cod
necunoscut in nomenclator), --commit cu upsert idempotent in tranzactie.
Dashboard: browser nomenclator, indicator stare RAR (indisponibil? derivat din
ultimul login < 30h, coada arata ultima stare locala), export audit CSV
(/v1/audit/export?status=sent|all&date_from&date_to, b64Image exclus,
coloana purge_after pentru retentia 90z).
Verify: 11 teste noi (test_import_dbf 6, test_dashboard 5), suita 111 pass,
dry-run real pe DBF-urile din repo + smoke live dashboard/CSV.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
T6 — worker supravegheat:
- app/worker/healthcheck.py: probe pe heartbeat-ul din DB (beat invechit -> exit 1).
Prinde worker-ul agatat (proces viu, beat inghetat) pe care restart:always nu-l
vede. Cablat ca healthcheck pe serviciul worker in compose.
- sidecar autoheal: restarteaza efectiv containerul unhealthy (compose simplu doar
marcheaza, nu restarteaza la unhealthy).
T7 — deploy:
- tools/backup.py: backup ONLINE via Connection.backup (WAL nu se copiaza sigur cu
cp); --keep N roteste snapshot-urile.
- .env.example documenteaza env-urile; volum persistent numit deja in compose.
Fix critic (split api/worker in 2 containere): AUTOPASS_CREDS_KEY trebuie PARTAJATA
api<->worker, altfel worker nu decripteaza creds-urile criptate de API -> submission
blocate. Acum impusa in compose (${...:?} -> fail explicit daca lipseste).
.gitignore: exceptie !.env.example.
5 teste noi (tests/test_deploy.py). 100 pass total.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan sect.5: parola RAR vine per-cerere, stocata CRIPTATA in submission pana la
primul login reusit pe cont, apoi stearsa; JWT 30h acopera restul.
- app/crypto.py: Fernet, cheie din AUTOPASS_creds_key (nesetata -> efemera la
runtime, creds nu supravietuiesc restartului). encrypt/decrypt_creds.
- schema + migrare: submissions.rar_creds_enc (creds criptate).
- ingestie: cripteaza rar_credentials, le lipeste de fiecare submission nou.
Niciodata in clar in DB.
- worker: AccountSessions (login per-cont cu creds decriptate, cache JWT in
memorie, sterge creds-urile contului dupa primul login + refresh nomenclator).
401 creds gresite -> error fara retry; token expirat -> invalidare + requeue;
fara creds (restart) -> requeue "indisponibile" (ROAAUTO re-trimite).
claim_one intoarce account_id + creds_enc; recover_orphans filtrabil pe cont.
- requirements: cryptography==46.0.5.
Nota: refresh nomenclator e acum lazy la primul login per-cont (nu la pornire);
seed-ul fallback acopera editorul offline.
10 teste noi (tests/test_creds_delivery.py). 95 pass total.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redactare:
- handler RequestValidationError dropeaza input/ctx din 422 (vectorul de
scurgere a rar_credentials.password pe /v1/prezentari); pastreaza type/loc/msg
- app/security.py: scrub/scrub_text + CredentialRedactingFilter pe root+uvicorn
- models.py: password cu repr=False
Auth API-key:
- app/auth.py: hash SHA-256 in api_keys (cheia in clar emisa o singura data),
header X-API-Key / Authorization: Bearer, dependency resolve_account_id
- enforcement pe flag AUTOPASS_require_api_key (prod on->401, dev off->cont
default id=1; cheie prezenta invalida->401 mereu)
- account_id real curge din cheie in ingestie + mapare
- tools/apikey.py: CLI create/rotate/revoke/list (fara endpoint HTTP admin)
16 teste noi (tests/test_security.py). 85 pass total.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
T5 reinterpretat: nu import DBF, ci editor web al maparii operatie ROAAUTO ->
cod RAR, cu fuzzy lookup si validare de catre utilizator.
- Contract hibrid: item prestatie accepta cod_prestatie (RAR direct, back-compat)
SAU cod_op_service+denumire (mapat de gateway prin operations_mapping).
- Ingestie: op intern necunoscut -> submission needs_mapping (nu pleaca la RAR);
codul rezolvat se scrie inapoi in payload_json -> payload builder + worker neatinse.
- Editor HTMX (_mapari.html + GET /_fragments/mapari, POST /mapari): listeaza
op-urile nemapate, fuzzy preselecteaza codul RAR, save -> re-rezolvare automata
(queued / needs_data).
- Fuzzy: rapidfuzz.token_sort_ratio pe denumire normalizata (fara diacritice).
- Nomenclator: seed fallback 18 coduri la boot (offline) + refresh live din worker.
- Cont default id=1 cat timp auth API-key (CORE) nu exista (account_id NULL).
- Endpointuri API: GET /v1/mapari/pending, POST /v1/mapari (respinge cod inexistent).
- 15 teste noi (tests/test_mapping.py); 69 pass total.
- Contract actualizat (docs/api-rar-contract.md), rapidfuzz==3.14.5 in requirements.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>